From 3076b6c8eaa6b8cc5dd091ce80a63268a3ca943d Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Wed, 23 Nov 2022 12:00:48 +0200 Subject: [PATCH] send invitations impr --- .../Crm/Business/Event/Invitations.php | 1 + .../Espo/Modules/Crm/Controllers/Call.php | 48 ++++- .../Espo/Modules/Crm/Controllers/Meeting.php | 48 ++++- .../Crm/EntryPoints/EventConfirmation.php | 11 +- .../Crm/Resources/i18n/en_US/Meeting.json | 2 +- .../templates/invitation/de_DE/body.tpl | 6 +- .../templates/invitation/en_US/body.tpl | 6 +- .../templates/invitation/es_ES/body.tpl | 4 +- .../templates/invitation/fr_FR/body.tpl | 6 +- .../templates/invitation/uk_UA/body.tpl | 6 +- .../Crm/Tools/Meeting/InvitationService.php | 99 ++++----- .../Modules/Crm/Tools/Meeting/Invitee.php | 19 ++ .../event-confirmation/confirmation.tpl | 26 ++- .../views/event-confirmation/confirmation.js | 48 ++++- .../modules/crm/src/views/meeting/detail.js | 44 ++-- .../views/meeting/modals/send-invitations.js | 198 ++++++++++++++++++ client/src/collection.js | 4 +- client/src/multi-collection.js | 5 +- client/src/views/fields/enum.js | 2 +- client/src/views/record/list.js | 58 ++--- frontend/less/espo/elements/type.less | 14 ++ 21 files changed, 528 insertions(+), 127 deletions(-) create mode 100644 application/Espo/Modules/Crm/Tools/Meeting/Invitee.php create mode 100644 client/modules/crm/src/views/meeting/modals/send-invitations.js diff --git a/application/Espo/Modules/Crm/Business/Event/Invitations.php b/application/Espo/Modules/Crm/Business/Event/Invitations.php index 5b38f3ac45..1fd0c50232 100644 --- a/application/Espo/Modules/Crm/Business/Event/Invitations.php +++ b/application/Espo/Modules/Crm/Business/Event/Invitations.php @@ -107,6 +107,7 @@ class Invitations 'inviteeId' => $invitee->getId(), 'inviteeType' => $invitee->getEntityType(), 'link' => $link, + 'dateStart' => $entity->get('dateStart'), ]); if ($entity->get('dateEnd')) { diff --git a/application/Espo/Modules/Crm/Controllers/Call.php b/application/Espo/Modules/Crm/Controllers/Call.php index 5218368cb2..474b5b0122 100644 --- a/application/Espo/Modules/Crm/Controllers/Call.php +++ b/application/Espo/Modules/Crm/Controllers/Call.php @@ -31,22 +31,27 @@ namespace Espo\Modules\Crm\Controllers; use Espo\Core\Api\Response; use Espo\Core\Controllers\Record; +use Espo\Core\Exceptions\Error; use Espo\Core\Exceptions\Forbidden; use Espo\Core\Exceptions\BadRequest; use Espo\Core\Exceptions\ForbiddenSilent; use Espo\Core\Exceptions\NotFound; use Espo\Core\Api\Request; +use Espo\Core\Mail\Exceptions\SendingError; use Espo\Core\Utils\Json; use Espo\Modules\Crm\Entities\Call as CallEntity; use Espo\Modules\Crm\Tools\Meeting\InvitationService; +use Espo\Modules\Crm\Tools\Meeting\Invitee; use Espo\Modules\Crm\Tools\Meeting\Service; +use stdClass; class Call extends Record { /** * @throws BadRequest * @throws Forbidden - * @throws ForbiddenSilent + * @throws Error + * @throws SendingError * @throws NotFound */ public function postActionSendInvitations(Request $request): bool @@ -57,13 +62,52 @@ class Call extends Record throw new BadRequest(); } + $invitees = $this->fetchInvitees($request); + $resultList = $this->injectableFactory ->create(InvitationService::class) - ->send(CallEntity::ENTITY_TYPE, $id); + ->send(CallEntity::ENTITY_TYPE, $id, $invitees); return $resultList !== 0; } + /** + * @param Request $request + * @return ?Invitee[] + * @throws BadRequest + */ + private function fetchInvitees(Request $request): ?array + { + $targets = $request->getParsedBody()->targets ?? null; + + if ($targets === null) { + return null; + } + + if (!is_array($targets)) { + throw new BadRequest(); + } + + $invitees = []; + + foreach ($targets as $target) { + if (!$target instanceof stdClass) { + throw new BadRequest(); + } + + $targetEntityType = $target->entityType ?? null; + $targetId = $target->id ?? null; + + if (!is_string($targetEntityType) || !is_string($targetId)) { + throw new BadRequest(); + } + + $invitees[] = new Invitee($targetEntityType, $targetId); + } + + return $invitees; + } + /** * @throws BadRequest * @throws Forbidden diff --git a/application/Espo/Modules/Crm/Controllers/Meeting.php b/application/Espo/Modules/Crm/Controllers/Meeting.php index ece4751f7d..ff435dddd9 100644 --- a/application/Espo/Modules/Crm/Controllers/Meeting.php +++ b/application/Espo/Modules/Crm/Controllers/Meeting.php @@ -31,6 +31,7 @@ namespace Espo\Modules\Crm\Controllers; use Espo\Core\Api\Response; use Espo\Core\Controllers\Record; +use Espo\Core\Exceptions\Error; use Espo\Core\Exceptions\Forbidden; use Espo\Core\Exceptions\BadRequest; use Espo\Core\Exceptions\ForbiddenSilent; @@ -38,17 +39,21 @@ use Espo\Core\Exceptions\NotFound; use Espo\Core\Api\Request; +use Espo\Core\Mail\Exceptions\SendingError; use Espo\Core\Utils\Json; use Espo\Modules\Crm\Entities\Meeting as MeetingEntity; use Espo\Modules\Crm\Tools\Meeting\InvitationService; +use Espo\Modules\Crm\Tools\Meeting\Invitee; use Espo\Modules\Crm\Tools\Meeting\Service; +use stdClass; class Meeting extends Record { /** * @throws BadRequest * @throws Forbidden - * @throws ForbiddenSilent + * @throws Error + * @throws SendingError * @throws NotFound */ public function postActionSendInvitations(Request $request): bool @@ -59,13 +64,52 @@ class Meeting extends Record throw new BadRequest(); } + $invitees = $this->fetchInvitees($request); + $resultList = $this->injectableFactory ->create(InvitationService::class) - ->send(MeetingEntity::ENTITY_TYPE, $id); + ->send(MeetingEntity::ENTITY_TYPE, $id, $invitees); return $resultList !== 0; } + /** + * @param Request $request + * @return ?Invitee[] + * @throws BadRequest + */ + private function fetchInvitees(Request $request): ?array + { + $targets = $request->getParsedBody()->targets ?? null; + + if ($targets === null) { + return null; + } + + if (!is_array($targets)) { + throw new BadRequest(); + } + + $invitees = []; + + foreach ($targets as $target) { + if (!$target instanceof stdClass) { + throw new BadRequest(); + } + + $targetEntityType = $target->entityType ?? null; + $targetId = $target->id ?? null; + + if (!is_string($targetEntityType) || !is_string($targetId)) { + throw new BadRequest(); + } + + $invitees[] = new Invitee($targetEntityType, $targetId); + } + + return $invitees; + } + /** * @throws BadRequest * @throws Forbidden diff --git a/application/Espo/Modules/Crm/EntryPoints/EventConfirmation.php b/application/Espo/Modules/Crm/EntryPoints/EventConfirmation.php index 524349c3e8..724ff63b9b 100644 --- a/application/Espo/Modules/Crm/EntryPoints/EventConfirmation.php +++ b/application/Espo/Modules/Crm/EntryPoints/EventConfirmation.php @@ -34,6 +34,7 @@ use Espo\Core\Api\Response; use Espo\Core\EntryPoint\EntryPoint; use Espo\Core\EntryPoint\Traits\NoAuth; use Espo\Core\Exceptions\BadRequest; +use Espo\Core\Exceptions\Error; use Espo\Core\Exceptions\NotFound; use Espo\Core\HookManager; use Espo\Core\ORM\EntityManager; @@ -67,6 +68,7 @@ class EventConfirmation implements EntryPoint /** * @throws BadRequest * @throws NotFound + * @throws Error */ public function run(Request $request, Response $response): void { @@ -100,6 +102,7 @@ class EventConfirmation implements EntryPoint $inviteeType = $data->inviteeType ?? null; $inviteeId = $data->inviteeId ?? null; $link = $data->link ?? null; + $sentDateStart = $data->dateStart ?? null; $toProcess = $eventType && @@ -109,7 +112,11 @@ class EventConfirmation implements EntryPoint $link; if (!$toProcess) { - throw new BadRequest(); + throw new Error(); + } + + if ($sentDateStart !== null && !is_string($sentDateStart)) { + throw new Error(); } $event = $this->entityManager->getEntityById($eventType, $eventId); @@ -187,6 +194,8 @@ class EventConfirmation implements EntryPoint $actionData['translatedStatus'] = $this->language->translateOption($status, 'acceptanceStatus', $eventType); $actionData['style'] = $this->metadata ->get(['entityDefs', $eventType, 'fields', 'acceptanceStatus', 'style', $status]); + $actionData['sentDateStart'] = $sentDateStart; + $actionData['statusTranslation'] = $this->language->get([Meeting::ENTITY_TYPE, 'options', 'acceptanceStatus']); $this->actionRenderer->write( $response, diff --git a/application/Espo/Modules/Crm/Resources/i18n/en_US/Meeting.json b/application/Espo/Modules/Crm/Resources/i18n/en_US/Meeting.json index 7c71998207..a9cd9a183a 100644 --- a/application/Espo/Modules/Crm/Resources/i18n/en_US/Meeting.json +++ b/application/Espo/Modules/Crm/Resources/i18n/en_US/Meeting.json @@ -53,7 +53,7 @@ "todays": "Today's" }, "messages": { - "sendInvitationsConfirmation": "Send invitation emails to attendees?", + "sendInvitationsToSelectedAttendees": "Invitation emails will be sent to the selected attendees.", "selectAcceptanceStatus": "Set your acceptance status.", "nothingHasBeenSent": "Nothing were sent" } diff --git a/application/Espo/Modules/Crm/Resources/templates/invitation/de_DE/body.tpl b/application/Espo/Modules/Crm/Resources/templates/invitation/de_DE/body.tpl index 46a4311160..8b9bc5fbe8 100644 --- a/application/Espo/Modules/Crm/Resources/templates/invitation/de_DE/body.tpl +++ b/application/Espo/Modules/Crm/Resources/templates/invitation/de_DE/body.tpl @@ -6,8 +6,10 @@ {{/if}} {{/if}}

-Annehmen, Decline, mit Vorbehalt + Annehmen · + mit Vorbehalt · + Decline

{{#if isUser}}

Eintrag öffnen

-{{/if}} \ No newline at end of file +{{/if}} diff --git a/application/Espo/Modules/Crm/Resources/templates/invitation/en_US/body.tpl b/application/Espo/Modules/Crm/Resources/templates/invitation/en_US/body.tpl index cfe4d056d2..59927f5232 100644 --- a/application/Espo/Modules/Crm/Resources/templates/invitation/en_US/body.tpl +++ b/application/Espo/Modules/Crm/Resources/templates/invitation/en_US/body.tpl @@ -6,8 +6,10 @@ {{/if}} {{/if}}

-Accept, Decline, Tentative + Accept · + Tentative · + Decline

{{#if isUser}}

View record

-{{/if}} \ No newline at end of file +{{/if}} diff --git a/application/Espo/Modules/Crm/Resources/templates/invitation/es_ES/body.tpl b/application/Espo/Modules/Crm/Resources/templates/invitation/es_ES/body.tpl index 734499a80c..fad98c879b 100644 --- a/application/Espo/Modules/Crm/Resources/templates/invitation/es_ES/body.tpl +++ b/application/Espo/Modules/Crm/Resources/templates/invitation/es_ES/body.tpl @@ -6,7 +6,9 @@ {{/if}} {{/if}}

-Aceptar, Declinar, Provisional + Aceptar · + Provisional · + Declinar

{{#if isUser}}

Ver registro

diff --git a/application/Espo/Modules/Crm/Resources/templates/invitation/fr_FR/body.tpl b/application/Espo/Modules/Crm/Resources/templates/invitation/fr_FR/body.tpl index 0b3fe1e36b..e502da56f3 100644 --- a/application/Espo/Modules/Crm/Resources/templates/invitation/fr_FR/body.tpl +++ b/application/Espo/Modules/Crm/Resources/templates/invitation/fr_FR/body.tpl @@ -6,8 +6,10 @@ {{/if}} {{/if}}

-Accepter, Décliner, Tentative + Accepter · + Tentative · + Décliner

{{#if isUser}}

Voir la fiche

-{{/if}} \ No newline at end of file +{{/if}} diff --git a/application/Espo/Modules/Crm/Resources/templates/invitation/uk_UA/body.tpl b/application/Espo/Modules/Crm/Resources/templates/invitation/uk_UA/body.tpl index d943fe5a10..7d80dea923 100644 --- a/application/Espo/Modules/Crm/Resources/templates/invitation/uk_UA/body.tpl +++ b/application/Espo/Modules/Crm/Resources/templates/invitation/uk_UA/body.tpl @@ -6,8 +6,10 @@ {{/if}} {{/if}}

-Прийняти, Відхилити, Не впевнений + Прийняти · + Не впевнений · + Відхилити

{{#if isUser}}

Відкрити запис

-{{/if}} \ No newline at end of file +{{/if}} diff --git a/application/Espo/Modules/Crm/Tools/Meeting/InvitationService.php b/application/Espo/Modules/Crm/Tools/Meeting/InvitationService.php index 25855fcd63..6ae6cfe371 100644 --- a/application/Espo/Modules/Crm/Tools/Meeting/InvitationService.php +++ b/application/Espo/Modules/Crm/Tools/Meeting/InvitationService.php @@ -41,10 +41,7 @@ use Espo\Core\Record\ServiceContainer as RecordServiceContainer; use Espo\Core\Utils\Config; use Espo\Entities\User; use Espo\Modules\Crm\Business\Event\Invitations; -use Espo\Modules\Crm\Entities\Contact; -use Espo\Modules\Crm\Entities\Lead; use Espo\Modules\Crm\Entities\Meeting; -use Espo\ORM\Collection; use Espo\ORM\Entity; use Espo\ORM\EntityManager; use Espo\Tools\Email\SendService; @@ -81,13 +78,14 @@ class InvitationService /** * Send invitations for a meeting (or call). Checks access. Uses user's SMTP if available. * + * @param ?Invitee[] $targets * @return Entity[] Entities an invitation was sent to. * @throws NotFound * @throws Forbidden * @throws Error * @throws SendingError */ - public function send(string $entityType, string $id): array + public function send(string $entityType, string $id, ?array $targets = null): array { $entity = $this->recordServiceContainer ->get($entityType) @@ -101,53 +99,50 @@ class InvitationService throw new Forbidden("No edit access."); } + $linkList = [ + 'users', + 'contacts', + 'leads', + ]; + $sender = $this->getSender(); $sentAddressList = []; $resultEntityList = []; - foreach ($this->getUsers($entity) as $user) { - $emailAddress = $user->getEmailAddress(); + foreach ($linkList as $link) { + $builder = $this->entityManager + ->getRDBRepository($entityType) + ->getRelation($entity, $link); - if ($emailAddress) { - $sender->sendInvitation($entity, $user, 'users'); - - $sentAddressList[] = $emailAddress; - $resultEntityList[] = $user; + if ($targets === null) { + $builder->where([ + '@relation.status=' => Meeting::ATTENDEE_STATUS_NONE, + ]); } - } - /** @var Collection $contacts */ - $contacts = $this->entityManager - ->getRDBRepository($entity->getEntityType()) - ->getRelation($entity, 'contacts') - ->find(); + $collection = $builder->find(); - /** @var Collection $leads */ - $leads = $this->entityManager - ->getRDBRepository($entity->getEntityType()) - ->getRelation($entity, 'leads') - ->find(); + foreach ($collection as $attendee) { + if ($targets && !self::isInTargets($attendee, $targets)) { + continue; + } - foreach ($contacts as $contact) { - $emailAddress = $contact->getEmailAddress(); + $emailAddress = $attendee->get('emailAddress'); - if ($emailAddress && !in_array($emailAddress, $sentAddressList)) { - $sender->sendInvitation($entity, $contact, 'contacts'); + if (!$emailAddress || in_array($emailAddress, $sentAddressList)) { + continue; + } + + $sender->sendInvitation($entity, $attendee, $link); $sentAddressList[] = $emailAddress; - $resultEntityList[] = $contact; - } - } + $resultEntityList[] = $attendee; - foreach ($leads as $lead) { - $emailAddress = $lead->getEmailAddress(); - - if ($emailAddress && !in_array($emailAddress, $sentAddressList)) { - $sender->sendInvitation($entity, $lead, 'leads'); - - $sentAddressList[] = $emailAddress; - $resultEntityList[] = $lead; + $this->entityManager + ->getRDBRepository($entityType) + ->getRelation($entity, $link) + ->updateColumns($attendee, ['status' => Meeting::ATTENDEE_STATUS_NONE]); } } @@ -155,26 +150,20 @@ class InvitationService } /** - * @return Collection + * @param Invitee[] $targets */ - private function getUsers(Entity $entity): Collection + private static function isInTargets(Entity $entity, array $targets): bool { - /** @var Collection */ - return $this->entityManager - ->getRDBRepository($entity->getEntityType()) - ->getRelation($entity, 'users') - ->where([ - 'OR' => [ - [ - 'id=' => $this->user->getId(), - '@relation.status!=' => Meeting::ATTENDEE_STATUS_ACCEPTED, - ], - [ - 'id!=' => $this->user->getId(), - ] - ] - ]) - ->find(); + foreach ($targets as $target) { + if ( + $entity->getEntityType() === $target->getEntityType() && + $entity->getId() === $target->getId() + ) { + return true; + } + } + + return false; } private function getSender(): Invitations diff --git a/application/Espo/Modules/Crm/Tools/Meeting/Invitee.php b/application/Espo/Modules/Crm/Tools/Meeting/Invitee.php new file mode 100644 index 0000000000..cc818c1595 --- /dev/null +++ b/application/Espo/Modules/Crm/Tools/Meeting/Invitee.php @@ -0,0 +1,19 @@ +entityType; + } + + public function getId(): string + { + return $this->id; + } +} diff --git a/client/modules/crm/res/templates/event-confirmation/confirmation.tpl b/client/modules/crm/res/templates/event-confirmation/confirmation.tpl index b0cf7e5f56..56f3b22deb 100644 --- a/client/modules/crm/res/templates/event-confirmation/confirmation.tpl +++ b/client/modules/crm/res/templates/event-confirmation/confirmation.tpl @@ -2,10 +2,30 @@
-

{{actionData.translatedEntityType}}: {{actionData.eventName}}

-

+

{{actionData.translatedEntityType}}: {{actionData.eventName}}

+
+ {{#if dateStartChanged}} +
{{sentDateStart}}
+ {{/if}} +
{{dateStart}}
+
+
{{actionData.translatedStatus}} -

+  
+ + + + +
+
diff --git a/client/modules/crm/src/views/event-confirmation/confirmation.js b/client/modules/crm/src/views/event-confirmation/confirmation.js index 5b601bbbde..6b12e74e48 100644 --- a/client/modules/crm/src/views/event-confirmation/confirmation.js +++ b/client/modules/crm/src/views/event-confirmation/confirmation.js @@ -26,19 +26,61 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -define('crm:views/event-confirmation/confirmation', ['view'], function (Dep) { +define('crm:views/event-confirmation/confirmation', ['view', 'lib!moment'], function (Dep, moment) { return Dep.extend({ template: 'crm:event-confirmation/confirmation', data: function () { - let style = this.options.actionData.style || 'default'; + let style = this.actionData.style || 'default'; return { - actionData: this.options.actionData, + actionData: this.actionData, style: style, + dateStart: this.actionData.dateStart ? + this.convertDateTime(this.actionData.dateStart) : null, + sentDateStart: this.actionData.sentDateStart ? + this.convertDateTime(this.actionData.sentDateStart) : null, + dateStartChanged: this.actionData.sentDateStart && + this.actionData.dateStart !== this.actionData.sentDateStart, + actionDataList: this.getActionDataList(), }; }, + + setup: function () { + this.actionData = this.options.actionData; + }, + + getActionDataList: function () { + let actionMap = { + 'Accepted': 'accept', + 'Declined': 'decline', + 'Tentative': 'tentative', + }; + + let statusList = ['Accepted', 'Tentative', 'Declined']; + + let url = window.location.href.replace('action=' + actionMap[this.actionData.status], 'action={action}'); + + return statusList.map(item => { + let active = item === this.actionData.status; + return { + active: active, + link: active ? '' : url.replace('{action}', actionMap[item]), + label: this.actionData.statusTranslation[item], + }; + }); + }, + + convertDateTime: function (value) { + let timezone = this.getConfig().get('timeZone'); + + let m = this.getDateTime().toMoment(value) + .tz(timezone); + + return m.format(this.getDateTime().getDateTimeFormat()) + ' ' + + m.format('Z z'); + }, }); }); diff --git a/client/modules/crm/src/views/meeting/detail.js b/client/modules/crm/src/views/meeting/detail.js index 929ea5323b..dddc5159d7 100644 --- a/client/modules/crm/src/views/meeting/detail.js +++ b/client/modules/crm/src/views/meeting/detail.js @@ -128,7 +128,7 @@ define('crm:views/meeting/detail', ['views/detail', 'lib!moment'], function (Dep if (!contactIdList.length && !leadIdList.length && !userIdList.length) { show = false; } - else if ( + /*else if ( !contactIdList.length && !leadIdList.length && userIdList.length === 1 && @@ -136,6 +136,17 @@ define('crm:views/meeting/detail', ['views/detail', 'lib!moment'], function (Dep this.model.getLinkMultipleColumn('users', 'status', this.getUser().id) === 'Accepted' ) { show = false; + }*/ + } + + if (show) { + let dateEnd = this.model.get('dateEnd'); + + if ( + dateEnd && + this.getDateTime().toMoment(dateEnd).isBefore(moment.now()) + ) { + show = false; } } @@ -164,30 +175,17 @@ define('crm:views/meeting/detail', ['views/detail', 'lib!moment'], function (Dep }, actionSendInvitations: function () { - this.confirm({ - message: this.translate('sendInvitationsConfirmation', 'messages', 'Meeting'), - }) - .then(() => { - this.disableMenuItem('sendInvitations'); - this.notify('Sending...'); + Espo.Ui.notify(' ... '); - Espo.Ajax - .postRequest(this.model.entityType + '/action/sendInvitations', { - id: this.model.id, - }) - .then(result => { - if (result) { - this.notify('Sent', 'success'); - } else { - Espo.Ui.warning(this.translate('nothingHasBeenSent', 'messages', 'Meeting')); - } + this.createView('dialog', 'crm:views/meeting/modals/send-invitations', { + model: this.model, + }).then(view => { + Espo.Ui.notify(false); - this.enableMenuItem('sendInvitations'); - }) - .catch(() => { - this.enableMenuItem('sendInvitations'); - }); - }); + view.render(); + + this.listenToOnce(view, 'sent', () => this.model.fetch()); + }); }, actionSetAcceptanceStatus: function () { diff --git a/client/modules/crm/src/views/meeting/modals/send-invitations.js b/client/modules/crm/src/views/meeting/modals/send-invitations.js new file mode 100644 index 0000000000..e513fe5f59 --- /dev/null +++ b/client/modules/crm/src/views/meeting/modals/send-invitations.js @@ -0,0 +1,198 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko + * Website: https://www.espocrm.com + * + * EspoCRM is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EspoCRM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EspoCRM. If not, see http://www.gnu.org/licenses/. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU General Public License version 3. + * + * In accordance with Section 7(b) of the GNU General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +define('crm:views/meeting/modals/send-invitations', ['views/modal', 'collection'], function (Dep, Collection) { + /** + * @module crm_views/meeting/modals/send-invitations + */ + + /** + * @class + * @name Class + * @extends module:views/modal.Class + * @memberOf module:crm_views/meeting/modals/send-invitations + */ + return Dep.extend(/** @lends module:crm_views/meeting/modals/send-invitations.Class# */{ + + backdrop: true, + + templateContent: ` +
+

{{message}}

+
+
{{{list}}}
+ `, + + data: function () { + return { + message: this.translate('sendInvitationsToSelectedAttendees', 'messages', 'Meeting'), + }; + }, + + setup: function () { + Dep.prototype.setup.call(this); + + this.shortcutKeys = {}; + this.shortcutKeys['Control+Enter'] = e => { + if (!this.hasAvailableActionItem('send')) { + return; + } + + e.preventDefault(); + + this.actionSend(); + }; + + this.$header = $('').append( + $('') + .text(this.translate(this.model.entityType, 'scopeNames')), + ' ', + $('') + .text(this.model.get('name')), + ' ', + $('') + .text(this.translate('Send Invitations', 'labels', 'Meeting')) + ); + + this.addButton({ + label: 'Send', + name: 'send', + style: 'danger', + disabled: true, + }); + + this.addButton({ + label: 'Cancel', + name: 'cancel', + }); + + this.collection = new Collection(); + this.collection.url = this.model.entityType + `/${this.model.id}/attendees`; + + this.wait( + this.collection.fetch() + .then(() => { + Espo.Utils.clone(this.collection.models).forEach(model => { + model.entityType = model.get('_scope'); + + if (!model.get('emailAddress')) { + this.collection.remove(model.id); + } + }); + + return this.createView('list', 'views/record/list', { + selector: '.list-container', + collection: this.collection, + rowActionsDisabled: true, + massActionsDisabled: true, + checkAllResultDisabled: true, + selectable: true, + buttonsDisabled: true, + listLayout: [ + { + name: 'name', + customLabel: this.translate('name', 'fields'), + notSortable: true, + }, + { + name: 'acceptanceStatus', + width: 40, + customLabel: this.translate('acceptanceStatus', 'fields', 'Meeting'), + notSortable: true, + view: 'views/fields/enum', + params: { + options: this.model.getFieldParam('acceptanceStatus', 'options'), + style: this.model.getFieldParam('acceptanceStatus', 'style'), + }, + }, + ], + }) + }) + .then(view => { + this.collection.models + .filter(model => { + let status = model.get('acceptanceStatus'); + + return !status || status === 'None'; + }) + .forEach(model => { + this.getListView().checkRecord(model.id); + }); + + this.listenTo(view, 'check', () => this.controlSendButton()); + + this.controlSendButton(); + }) + ); + }, + + controlSendButton: function () { + this.getListView().checkedList.length ? + this.enableButton('send') : + this.disableButton('send'); + }, + + /** + * @return {module:views/record/list.Class} + */ + getListView: function () { + return this.getView('list'); + }, + + actionSend: function () { + this.disableButton('send'); + + Espo.Ui.notify(' ... '); + + let targets = this.getListView().checkedList.map(id => { + return { + entityType: this.collection.get(id).entityType, + id: id, + }; + }); + + Espo.Ajax + .postRequest(this.model.entityType + '/action/sendInvitations', { + id: this.model.id, + targets: targets, + }) + .then(result => { + result ? + Espo.Ui.success(this.translate('Sent')) : + Espo.Ui.warning(this.translate('nothingHasBeenSent', 'messages', 'Meeting')); + + this.trigger('sent'); + + this.close(); + }) + .catch(() => { + this.enableButton('send'); + }); + }, + }); +}); diff --git a/client/src/collection.js b/client/src/collection.js index 71d1cde533..311d40e92d 100644 --- a/client/src/collection.js +++ b/client/src/collection.js @@ -26,7 +26,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -define('collection', [], function () { +define('collection', ['model'], function (Model) { /** * On sync with backend. @@ -204,6 +204,8 @@ define('collection', [], function () { this.defaultOrderBy = this.orderBy; this.data = {}; + + this.model = options.model || Model; }, /** diff --git a/client/src/multi-collection.js b/client/src/multi-collection.js index a0f9b73b0e..71540ea293 100644 --- a/client/src/multi-collection.js +++ b/client/src/multi-collection.js @@ -39,7 +39,10 @@ define('multi-collection', ['collection'], function (Collection) { return Collection.extend(/** @lends module:multi-collection.Class# */{ /** - * @private + * A model seed map. + * + * @public + * @type {Object.} */ seeds: null, diff --git a/client/src/views/fields/enum.js b/client/src/views/fields/enum.js index 985a23b652..e6613ab14a 100644 --- a/client/src/views/fields/enum.js +++ b/client/src/views/fields/enum.js @@ -115,7 +115,7 @@ function (Dep, /** module:ui/multi-select*/MultiSelect, /** module:ui/select*/Se ); } - this.styleMap = this.model.getFieldParam(this.name, 'style') || {}; + this.styleMap = this.params.style || this.model.getFieldParam(this.name, 'style') || {}; this.setupOptions(); diff --git a/client/src/views/record/list.js b/client/src/views/record/list.js index 2aef16d34a..3aa87c9dc5 100644 --- a/client/src/views/record/list.js +++ b/client/src/views/record/list.js @@ -108,7 +108,7 @@ function (Dep, MassActionHelper, ExportHelper, RecordModal) { /** * A scope. Set automatically. * - * @type {string|null} + * @type {?string} */ scope: null, @@ -1913,13 +1913,13 @@ function (Dep, MassActionHelper, ExportHelper, RecordModal) { this.checkAllResultMassActionList = checkAllResultMassActionList; - let metadataCheckkAllMassActionList = ( + let metadataCheckAllMassActionList = ( this.getMetadata().get(['clientDefs', this.scope, 'checkAllResultMassActionList']) || [] ).concat( this.getMetadata().get(['clientDefs', 'Global', 'checkAllResultMassActionList']) || [] ); - metadataCheckkAllMassActionList.forEach(item => { + metadataCheckAllMassActionList.forEach(item => { if (this.collection.url !== this.entityType) { return; } @@ -1941,7 +1941,7 @@ function (Dep, MassActionHelper, ExportHelper, RecordModal) { }); metadataMassActionList - .concat(metadataCheckkAllMassActionList) + .concat(metadataCheckAllMassActionList) .forEach(action => { let defs = this.getMetadata() .get(['clientDefs', this.scope, 'massActionDefs', action]) || {}; @@ -2216,7 +2216,9 @@ function (Dep, MassActionHelper, ExportHelper, RecordModal) { _loadListLayout: function (callback) { this.layoutLoadCallbackList.push(callback); - if (this.layoutIsBeingLoaded) return; + if (this.layoutIsBeingLoaded) { + return; + } this.layoutIsBeingLoaded = true; @@ -2257,7 +2259,7 @@ function (Dep, MassActionHelper, ExportHelper, RecordModal) { return; } - this._loadListLayout((listLayout) => { + this._loadListLayout(listLayout => { this.listLayout = listLayout; let attributeList = this.fetchAttributeListFromLayout(); @@ -2433,6 +2435,16 @@ function (Dep, MassActionHelper, ExportHelper, RecordModal) { item.options.defs.align = col.align; } + if (col.options) { + for (let optionName in col.options) { + if (typeof item.options[optionName] !== 'undefined') { + continue; + } + + item.options[optionName] = col.options[optionName]; + } + } + layout.push(item); } if (this.rowActionsView && !this.rowActionsDisabled) { @@ -2452,20 +2464,17 @@ function (Dep, MassActionHelper, ExportHelper, RecordModal) { checkRecord: function (id, $target, isSilent) { $target = $target || this.$el.find('.record-checkbox[data-id="' + id + '"]'); - if (!$target.length) { - return; + if ($target.length) { + $target.get(0).checked = true; + $target.closest('tr').addClass('active'); } - $target.get(0).checked = true; - - var index = this.checkedList.indexOf(id); + let index = this.checkedList.indexOf(id); if (index === -1) { this.checkedList.push(id); } - $target.closest('tr').addClass('active'); - this.handleAfterCheck(isSilent); }, @@ -2481,18 +2490,15 @@ function (Dep, MassActionHelper, ExportHelper, RecordModal) { if ($target.get(0)) { $target.get(0).checked = false; + $target.closest('tr').removeClass('active'); } - var index = this.checkedList.indexOf(id); + let index = this.checkedList.indexOf(id); if (index !== -1) { this.checkedList.splice(index, 1); } - if ($target.get(0)) { - $target.closest('tr').removeClass('active'); - } - this.handleAfterCheck(isSilent); }, @@ -2587,17 +2593,19 @@ function (Dep, MassActionHelper, ExportHelper, RecordModal) { * @protected */ getInternalLayout: function (callback, model) { - if (this.scope === null || this.rowHasOwnLayout) { + if ( + (this.scope === null || this.rowHasOwnLayout) && + !Array.isArray(this.listLayout) + ) { if (!model) { callback(null); return; } - else { - this.getInternalLayoutForModel(callback, model); - return; - } + this.getInternalLayoutForModel(callback, model); + + return; } if (this._internalLayout !== null) { @@ -2614,7 +2622,7 @@ function (Dep, MassActionHelper, ExportHelper, RecordModal) { return; } - this._loadListLayout((listLayout) => { + this._loadListLayout(listLayout => { this.listLayout = listLayout; this._internalLayout = this._convertLayout(listLayout); @@ -2652,7 +2660,7 @@ function (Dep, MassActionHelper, ExportHelper, RecordModal) { this.rowList.push(key); - this.getInternalLayout((internalLayout) => { + this.getInternalLayout(internalLayout => { internalLayout = Espo.Utils.cloneDeep(internalLayout); this.prepareInternalLayout(internalLayout, model); diff --git a/frontend/less/espo/elements/type.less b/frontend/less/espo/elements/type.less index 6780a5b744..a758afbe18 100644 --- a/frontend/less/espo/elements/type.less +++ b/frontend/less/espo/elements/type.less @@ -224,6 +224,20 @@ a.text-muted { } } +a.text-soft { + &:hover, + &:focus { + color: var(--gray-soft); + } + + &:hover { + > .fas, + > .far { + color: var(--gray-soft); + } + } +} + .text-primary { .text-emphasis-variant-espo(var(--state-primary-text), var(--brand-primary-10)); }