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: `
+
+ {{{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));
}