From 287488f49e6fb970ba049e7054324e4564f64fab Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sun, 6 Oct 2024 15:14:57 +0300 Subject: [PATCH] email move impr --- .../Email/Where/ItemConverters/InFolder.php | 30 ++- application/Espo/Entities/Email.php | 15 ++ .../Espo/Resources/i18n/en_US/Email.json | 10 +- .../Resources/metadata/clientDefs/Email.json | 27 ++- .../Resources/metadata/entityDefs/Email.json | 12 ++ .../Resources/metadata/recordDefs/Email.json | 3 +- application/Espo/Tools/Email/InboxService.php | 171 ++++++++++++++---- .../email-folder/modals/select-folder.js | 42 +++-- client/src/views/email/detail.js | 22 +-- .../src/views/email/fields/folder-string.js | 17 +- client/src/views/email/fields/subject.js | 25 ++- client/src/views/email/list.js | 11 ++ client/src/views/email/record/detail.js | 108 +++++++++-- client/src/views/email/record/list.js | 32 +++- .../views/email/record/row-actions/default.js | 44 +++-- 15 files changed, 451 insertions(+), 118 deletions(-) diff --git a/application/Espo/Classes/Select/Email/Where/ItemConverters/InFolder.php b/application/Espo/Classes/Select/Email/Where/ItemConverters/InFolder.php index fa7fefba88..aca84b3fe6 100644 --- a/application/Espo/Classes/Select/Email/Where/ItemConverters/InFolder.php +++ b/application/Espo/Classes/Select/Email/Where/ItemConverters/InFolder.php @@ -141,8 +141,16 @@ class InFolder implements ItemConverter $this->joinEmailUser($queryBuilder); return WhereClause::fromRaw([ - Email::ALIAS_INBOX . '.userId' => $this->user->getId(), - Email::ALIAS_INBOX . '.inTrash' => true, + 'OR' => [ + [ + Email::ALIAS_INBOX . '.userId' => $this->user->getId(), + Email::ALIAS_INBOX . '.inTrash' => true, + ], + [ + 'groupFolderId!=' => null, + 'groupStatusFolder' => Email::GROUP_STATUS_FOLDER_TRASH, + ], + ] ]); } @@ -151,8 +159,16 @@ class InFolder implements ItemConverter $this->joinEmailUser($queryBuilder); return WhereClause::fromRaw([ - Email::ALIAS_INBOX . '.userId' => $this->user->getId(), - Email::ALIAS_INBOX . '.inArchive' => true, + 'OR' => [ + [ + Email::ALIAS_INBOX . '.userId' => $this->user->getId(), + Email::ALIAS_INBOX . '.inArchive' => true, + ], + [ + 'groupFolderId!=' => null, + 'groupStatusFolder' => Email::GROUP_STATUS_FOLDER_ARCHIVE, + ], + ] ]); } @@ -177,11 +193,7 @@ class InFolder implements ItemConverter return WhereClause::fromRaw([ 'groupFolderId' => $groupFolderId, - 'OR' => [ - Email::ALIAS_INBOX . '.id' => null, - Email::ALIAS_INBOX . '.inTrash' => false, - Email::ALIAS_INBOX . '.inArchive' => false, - ] + 'groupStatusFolder' => null, ]); } diff --git a/application/Espo/Entities/Email.php b/application/Espo/Entities/Email.php index d1ab602571..f6974676f8 100644 --- a/application/Espo/Entities/Email.php +++ b/application/Espo/Entities/Email.php @@ -60,6 +60,9 @@ class Email extends Entity public const USERS_COLUMN_FOLDER_ID = 'folderId'; public const USERS_COLUMN_IS_IMPORTANT = 'isImportant'; + public const GROUP_STATUS_FOLDER_ARCHIVE = 'Archive'; + public const GROUP_STATUS_FOLDER_TRASH = 'Trash'; + /** @noinspection PhpUnused */ protected function _getSubject(): ?string { @@ -727,6 +730,18 @@ class Email extends Entity return $this; } + public function getGroupStatusFolder(): ?string + { + return $this->get('groupStatusFolder'); + } + + public function setGroupStatusFolder(?string $groupStatusFolder): self + { + $this->set('groupStatusFolder', $groupStatusFolder); + + return $this; + } + public function setDateSent(?DateTime $dateSent): self { $this->setValueObject('dateSent', $dateSent); diff --git a/application/Espo/Resources/i18n/en_US/Email.json b/application/Espo/Resources/i18n/en_US/Email.json index 01b5d69278..d6b4b0173e 100644 --- a/application/Espo/Resources/i18n/en_US/Email.json +++ b/application/Espo/Resources/i18n/en_US/Email.json @@ -59,7 +59,8 @@ "createdEvent": "Created Event", "event": "Event", "icsEventDateStart": "ICS Event Date Start", - "groupFolder": "Group Folder" + "groupFolder": "Group Folder", + "groupStatusFolder": "Group Status Folder" }, "links": { "replied": "Replied", @@ -85,6 +86,10 @@ "Archived": "Imported", "Received": "Received", "Failed": "Failed" + }, + "groupStatusFolder": { + "Archive": "Archive", + "Trash": "Trash" } }, "labels": { @@ -126,7 +131,8 @@ "Retrieved from Trash": "Retrieved from Trash" }, "strings": { - "sendingFailed": "Email sending failed" + "sendingFailed": "Email sending failed", + "group": "Group" }, "messages": { "alreadyImported": "The [email]({link}) already exists in the system.", diff --git a/application/Espo/Resources/metadata/clientDefs/Email.json b/application/Espo/Resources/metadata/clientDefs/Email.json index 2c3d2caa34..36c7de9344 100644 --- a/application/Espo/Resources/metadata/clientDefs/Email.json +++ b/application/Espo/Resources/metadata/clientDefs/Email.json @@ -150,13 +150,28 @@ "visible": { "conditionGroup": [ { - "type": "isTrue", - "attribute": "isUsers" - }, - { - "type": "isFalse", - "attribute": "isUsersSent" + "type": "or", + "value": [ + { + "type": "and", + "value": [ + { + "type": "isTrue", + "attribute": "isUsers" + }, + { + "type": "isFalse", + "attribute": "isUsersSent" + } + ] + }, + { + "type": "isNotEmpty", + "attribute": "groupFolderId" + } + ] } + ] } } diff --git a/application/Espo/Resources/metadata/entityDefs/Email.json b/application/Espo/Resources/metadata/entityDefs/Email.json index 4105ddece2..d0c7304dc5 100644 --- a/application/Espo/Resources/metadata/entityDefs/Email.json +++ b/application/Espo/Resources/metadata/entityDefs/Email.json @@ -585,6 +585,18 @@ "groupFolder": { "type": "link", "massUpdateDisabled": true + }, + "groupStatusFolder": { + "type": "enum", + "options": [ + "", + "Archive", + "Trash" + ], + "maxLength": 7, + "readOnly": true, + "customizationDisabled": true, + "index": true } }, "links": { diff --git a/application/Espo/Resources/metadata/recordDefs/Email.json b/application/Espo/Resources/metadata/recordDefs/Email.json index a914bb46f5..899bf5682c 100644 --- a/application/Espo/Resources/metadata/recordDefs/Email.json +++ b/application/Espo/Resources/metadata/recordDefs/Email.json @@ -35,7 +35,8 @@ "sentById", "replyToString", "hasAttachment", - "groupFolderId" + "groupFolderId", + "groupStatusFolder" ], "beforeReadHookClassNameList": [ "Espo\\Classes\\RecordHooks\\Email\\MarkAsRead" diff --git a/application/Espo/Tools/Email/InboxService.php b/application/Espo/Tools/Email/InboxService.php index bd0a77c749..c9b76a4f83 100644 --- a/application/Espo/Tools/Email/InboxService.php +++ b/application/Espo/Tools/Email/InboxService.php @@ -84,33 +84,20 @@ class InboxService $folderId = null; } - $email = $this->entityManager->getRDBRepositoryByClass(Email::class)->getById($id); + $email = $this->getEmail($id); + $user = $this->getUser($userId); - if (!$email) { - throw new NotFound(); - } + $prevGroupFolderLink = $email->getGroupFolder(); - $user = $userId === $this->user->getId() ? - $this->user : - $this->entityManager - ->getRDBRepositoryByClass(User::class) - ->getById($userId); - - if (!$user) { - throw new NotFound("User not found."); - } - - $previousFolderLink = $email->getGroupFolder(); - - if ($previousFolderLink) { - $this->checkCurrentGroupFolder($previousFolderLink->getId(), $user); + if ($prevGroupFolderLink) { + $this->checkCurrentGroupFolder($prevGroupFolderLink->getId(), $user); } if ($folderId && str_starts_with($folderId, 'group:')) { try { $this->moveToGroupFolder($email, substr($folderId, 6), $user); } catch (Exception $e) { - $this->log->debug("Could not move email to group folder. " . $e->getMessage()); + $this->log->debug("Could not move email to group folder. {message}", ['message' => $e->getMessage()]); throw $e; } @@ -124,13 +111,15 @@ class InboxService return; } - if ($previousFolderLink) { - $email->setGroupFolderId(null); - - if (!$this->aclManager->checkEntityRead($user, $email)) { - throw new Forbidden("No read access to email to unset group folder."); + if ($prevGroupFolderLink) { + if (!$this->aclManager->checkEntityEdit($user, $email)) { + throw new Forbidden("Cannot unset group folder. No edit access to email."); } + $email + ->setGroupFolderId(null) + ->setGroupStatusFolder(null); + $this->entityManager->saveEntity($email); } @@ -159,25 +148,31 @@ class InboxService */ private function moveToGroupFolder(Email $email, string $folderId, User $user): void { - $folder = $this->entityManager->getEntityById(GroupEmailFolder::ENTITY_TYPE, $folderId); - - if (!$folder) { - throw new NotFound("Group folder not found."); - } + $folder = $this->getGroupFolder($folderId); if (!$this->aclManager->checkEntityRead($user, $folder)) { - throw new Forbidden("No access to folder."); + throw new Forbidden("Cannot move to group folder. No access to folder."); } if (!$this->aclManager->checkEntityRead($user, $email)) { - throw new Forbidden("No read access to email to unset group folder."); + throw new Forbidden("Cannot move to group folder. No read access to email."); } if (!$this->aclManager->checkField($user, Email::ENTITY_TYPE, 'groupFolder', Table::ACTION_EDIT)) { - throw new Forbidden("No access to `groupFolder` field."); + throw new Forbidden("Cannot move to group folder. No edit access to `groupFolder` field."); } - $email->setGroupFolderId($folderId); + if ( + !$email->getGroupFolder() && + !$this->aclManager->checkEntityEdit($user, $email) + ) { + throw new Forbidden("Cannot move to group folder from All folder. No edit access to email."); + } + + $email + ->setGroupFolderId($folderId) + ->setGroupStatusFolder(null); + $this->entityManager->saveEntity($email); $this->retrieveFromArchive($email, $user); @@ -189,7 +184,9 @@ class InboxService public function moveToTrashIdList(array $idList, ?string $userId = null): bool { foreach ($idList as $id) { - $this->moveToTrash($id, $userId); + try { + $this->moveToTrash($id, $userId); + } catch (Exception) {} } return true; @@ -201,14 +198,41 @@ class InboxService public function retrieveFromTrashIdList(array $idList, ?string $userId = null): void { foreach ($idList as $id) { - $this->retrieveFromTrash($id, $userId); + try { + $this->retrieveFromTrash($id, $userId); + } catch (Exception) {} } } + /** + * @throws NotFound + * @throws Forbidden + */ public function moveToTrash(string $id, ?string $userId = null): void { $userId = $userId ?? $this->user->getId(); + $email = $this->getEmail($id); + $user = $this->getUser($userId); + + if ($email->getGroupFolder()) { + $folder = $this->getGroupFolder($email->getGroupFolder()->getId()); + + if (!$this->aclManager->checkEntityRead($user, $email)) { + throw new Forbidden("Cannot move email from group folder to trash. No access to email."); + } + + if (!$this->aclManager->checkEntityRead($user, $folder)) { + throw new Forbidden("Cannot move email from group folder to trash. No access to group folder."); + } + + $email->setGroupStatusFolder(Email::GROUP_STATUS_FOLDER_TRASH); + + $this->entityManager->saveEntity($email); + + return; + } + $update = $this->entityManager ->getQueryBuilder() ->update() @@ -226,10 +250,35 @@ class InboxService $this->markNotificationAsRead($id, $userId); } + /** + * @throws Forbidden + * @throws NotFound + */ public function retrieveFromTrash(string $id, ?string $userId = null): void { $userId = $userId ?? $this->user->getId(); + $email = $this->getEmail($id); + $user = $this->getUser($userId); + + if ($email->getGroupFolder()) { + $folder = $this->getGroupFolder($email->getGroupFolder()->getId()); + + if (!$this->aclManager->checkEntityRead($user, $email)) { + throw new Forbidden("Cannot retrieve group folder email from trash. No access to email."); + } + + if (!$this->aclManager->checkEntityRead($user, $folder)) { + throw new Forbidden("Cannot retrieve group folder email from trash. No access to group folder."); + } + + $email->setGroupStatusFolder(null); + + $this->entityManager->saveEntity($email); + + return; + } + $update = $this->entityManager ->getQueryBuilder() ->update() @@ -519,6 +568,14 @@ class InboxService throw new Forbidden("No 'read' access"); } + if ($email->getGroupFolder()) { + $email->setGroupStatusFolder(Email::GROUP_STATUS_FOLDER_ARCHIVE); + + $this->entityManager->saveEntity($email); + + return; + } + $update = $this->entityManager ->getQueryBuilder() ->update() @@ -573,4 +630,48 @@ class InboxService throw new Forbidden("No access to `groupFolder` field."); } } + + /** + * @throws NotFound + */ + private function getUser(string $userId): User + { + $user = $userId === $this->user->getId() ? + $this->user : + $this->entityManager->getRDBRepositoryByClass(User::class)->getById($userId); + + if (!$user) { + throw new NotFound("User not found."); + } + + return $user; + } + + /** + * @throws NotFound + */ + private function getEmail(string $id): Email + { + $email = $this->entityManager->getRDBRepositoryByClass(Email::class)->getById($id); + + if (!$email) { + throw new NotFound(); + } + + return $email; + } + + /** + * @throws NotFound + */ + private function getGroupFolder(string $folderId): GroupEmailFolder + { + $folder = $this->entityManager->getRDBRepositoryByClass(GroupEmailFolder::class)->getById($folderId); + + if (!$folder) { + throw new NotFound("Group folder not found."); + } + + return $folder; + } } diff --git a/client/src/views/email-folder/modals/select-folder.js b/client/src/views/email-folder/modals/select-folder.js index db0da50332..d138cccab0 100644 --- a/client/src/views/email-folder/modals/select-folder.js +++ b/client/src/views/email-folder/modals/select-folder.js @@ -33,7 +33,6 @@ export default class extends ModalView { template = 'email-folder/modals/select-folder' cssName = 'select-folder' - fitHeight = true backdrop = true data() { @@ -52,6 +51,8 @@ export default class extends ModalView { }); this.headerText = this.options.headerText || ''; + this.isGroup = this.options.isGroup || false; + this.noArchive = this.options.noArchive || false; if (this.headerText === '') { this.buttonList.push({ @@ -64,19 +65,25 @@ export default class extends ModalView { this.wait( Espo.Ajax.getRequest('EmailFolder/action/listAll') - .then(data => { + .then(/** {list: {id: string, name: string}[]} */data => { Espo.Ui.notify(false); + const builtInFolders = [ + 'inbox', + 'important', + 'sent', + 'drafts', + 'trash', + 'archive', + ]; + this.folderDataList = data.list .filter(item => { - return [ - 'inbox', - 'important', - 'sent', - 'drafts', - 'trash', - 'archive', - ].indexOf(item.id) === -1; + if (this.isGroup && !item.id.startsWith('group:')) { + return false; + } + + return !builtInFolders.includes(item.id); }) .map(item => { return { @@ -88,13 +95,18 @@ export default class extends ModalView { this.folderDataList.unshift({ id: 'inbox', - name: this.translate('inbox', 'presetFilters', 'Email'), + name: this.isGroup ? + this.translate('all', 'presetFilters', 'Email') : + this.translate('inbox', 'presetFilters', 'Email'), }); - this.folderDataList.push({ - id: 'archive', - name: this.translate('archive', 'presetFilters', 'Email'), - }); + + if (!this.noArchive) { + this.folderDataList.push({ + id: 'archive', + name: this.translate('archive', 'presetFilters', 'Email'), + }); + } }) ); } diff --git a/client/src/views/email/detail.js b/client/src/views/email/detail.js index f58ac8d92a..c4ea97d02d 100644 --- a/client/src/views/email/detail.js +++ b/client/src/views/email/detail.js @@ -112,19 +112,11 @@ class EmailDetailView extends DetailView { } } - this.listenTo(this.model, 'change', () => { + this.listenTo(this.model, 'change:isImportant change:inTrash change:inArchive change:groupStatusFolder', () => { if (!this.isRendered()) { return; } - if ( - !this.model.hasChanged('isImportant') && - !this.model.hasChanged('inTrash') && - !this.model.hasChanged('inArchive') - ) { - return; - } - const headerView = this.getHeaderView(); if (headerView) { @@ -559,9 +551,15 @@ class EmailDetailView extends DetailView { getHeader() { const name = this.model.get('name'); - const isImportant = this.model.get('isImportant'); - const inTrash = this.model.get('inTrash'); - const inArchive = this.model.get('inArchive'); + const isImportant = this.model.attributes.isImportant; + + const inTrash = this.model.attributes.groupFolderId ? + this.model.attributes.groupStatusFolder === 'Trash' : + this.model.attributes.inTrash; + + const inArchive = this.model.attributes.groupFolderId ? + this.model.attributes.groupStatusFolder === 'Archive' : + this.model.attributes.inArchive; const rootUrl = this.options.rootUrl || this.options.params.rootUrl || '#' + this.scope; diff --git a/client/src/views/email/fields/folder-string.js b/client/src/views/email/fields/folder-string.js index a6751a9a5d..7e62f075f7 100644 --- a/client/src/views/email/fields/folder-string.js +++ b/client/src/views/email/fields/folder-string.js @@ -45,6 +45,7 @@ class EmailFolderStringFieldView extends BaseFieldView { // noinspection JSCheckFunctionSignatures data() { + console.log(this.getFolderString()); if (!this.model.has('folderId')) { return {valueIsSet: false}; } @@ -57,6 +58,7 @@ class EmailFolderStringFieldView extends BaseFieldView { getAttributeList() { return [ + 'isUsers', 'folderId', 'folderName', 'groupFolderId', @@ -64,6 +66,7 @@ class EmailFolderStringFieldView extends BaseFieldView { 'inArchive', 'inTrash', 'isUsersSent', + 'groupStatusFolder', ]; } @@ -83,12 +86,20 @@ class EmailFolderStringFieldView extends BaseFieldView { return this.translate('archive', 'presetFilters', 'Email'); } - if (this.model.get('folderName')) { - return this.model.get('folderName'); + if (this.model.attributes.folderName && this.model.attributes.folderId) { + return this.model.attributes.folderName; } if (this.model.get('groupFolderName')) { - return this.model.get('groupFolderName'); + let string = this.translate('group', 'strings', 'Email') + ' · ' + this.model.get('groupFolderName'); + + if (this.model.attributes.groupStatusFolder === 'Archive') { + string += ' · ' + this.translate('archive', 'presetFilters', 'Email'); + } else if (this.model.attributes.groupStatusFolder === 'Trash') { + string += ' · ' + this.translate('trash', 'presetFilters', 'Email'); + } + + return string; } if (this.model.get('isUsers')) { diff --git a/client/src/views/email/fields/subject.js b/client/src/views/email/fields/subject.js index 0eaa8af6f4..922cbc235f 100644 --- a/client/src/views/email/fields/subject.js +++ b/client/src/views/email/fields/subject.js @@ -39,7 +39,14 @@ class EmailSubjectFieldView extends VarcharFieldView { data.isImportant = this.model.has('isImportant') && this.model.get('isImportant'); data.hasAttachment = this.model.has('hasAttachment') && this.model.get('hasAttachment'); data.isReplied = this.model.has('isReplied') && this.model.get('isReplied'); - data.inTrash = this.model.has('inTrash') && this.model.get('inTrash'); + + data.inTrash = this.model.attributes.groupFolderId ? + this.model.attributes.groupStatusFolder === 'Trash' : + this.model.attributes.inTrash; + + data.inArchive = this.model.attributes.groupFolderId ? + this.model.attributes.groupStatusFolder === 'Archive' : + this.model.attributes.inArchive; if (!data.isRead && !this.model.has('isRead')) { data.isRead = true; @@ -63,7 +70,15 @@ class EmailSubjectFieldView extends VarcharFieldView { } getAttributeList() { - return ['name', 'subject', 'isRead', 'isImportant', 'hasAttachment', 'inTrash']; + return [ + 'name', + 'subject', + 'isRead', + 'isImportant', + 'hasAttachment', + 'inTrash', + 'groupStatusFolder', + ]; } setup() { @@ -75,11 +90,9 @@ class EmailSubjectFieldView extends VarcharFieldView { this.showAttachments(); } - this.listenTo(this.model, 'change', () => { + this.listenTo(this.model, 'change:isRead change:isImportant change:groupStatusFolder', () => { if (this.mode === this.MODE_LIST || this.mode === this.MODE_LIST_LINK) { - if (this.model.hasChanged('isRead') || this.model.hasChanged('isImportant')) { - this.reRender(); - } + this.reRender(); } }); } diff --git a/client/src/views/email/list.js b/client/src/views/email/list.js index 2cadf2362e..0d812dc1c7 100644 --- a/client/src/views/email/list.js +++ b/client/src/views/email/list.js @@ -236,6 +236,17 @@ class EmailListView extends ListView { return true; } + if (this.selectedFolderId.indexOf('group:') === 0) { + if ( + [this.FOLDER_ALL, this.FOLDER_ARCHIVE, this.FOLDER_TRASH].includes(folderId) || + folderId.startsWith('group:') + ) { + return true; + } + + return false; + } + return true; } diff --git a/client/src/views/email/record/detail.js b/client/src/views/email/record/detail.js index d337584a01..bff172fd22 100644 --- a/client/src/views/email/record/detail.js +++ b/client/src/views/email/record/detail.js @@ -127,7 +127,7 @@ class EmailDetailRecordView extends DetailRecordView { }); } - if (this.model.get('isUsers')) { + if (this.model.attributes.isUsers) { this.addDropdownItem({ label: 'Mark as Important', name: 'markAsImportant', @@ -145,14 +145,14 @@ class EmailDetailRecordView extends DetailRecordView { this.addDropdownItem({ label: 'Move to Trash', name: 'moveToTrash', - hidden: this.model.get('inTrash'), + hidden: this.isInTrash(), groupIndex: 2, }); this.addDropdownItem({ label: 'Retrieve from Trash', name: 'retrieveFromTrash', - hidden: !this.model.get('inTrash'), + hidden: !this.isInTrash(), groupIndex: 2, }); @@ -160,10 +160,45 @@ class EmailDetailRecordView extends DetailRecordView { labelTranslation: 'Email.actions.moveToArchive', name: 'moveToArchive', groupIndex: 2, - hidden: this.model.get('inArchive'), + hidden: this.isInArchive(), onClick: () => this.actionMoveToArchive(), }); + this.addDropdownItem({ + label: 'Move to Folder', + name: 'moveToFolder', + groupIndex: 2, + }); + } else if (this.model.attributes.groupFolderId) { + this.addDropdownItem({ + label: 'Move to Trash', + name: 'moveToTrash', + hidden: this.isInTrash(), + groupIndex: 2, + }); + + this.addDropdownItem({ + label: 'Retrieve from Trash', + name: 'retrieveFromTrash', + hidden: !this.isInTrash(), + groupIndex: 2, + }); + + this.addDropdownItem({ + labelTranslation: 'Email.actions.moveToArchive', + name: 'moveToArchive', + groupIndex: 2, + hidden: this.isInArchive() || this.isInTrash(), + onClick: () => this.actionMoveToArchive(), + }); + + this.addDropdownItem({ + label: 'Move to Folder', + name: 'moveToFolder', + groupIndex: 2, + hidden: this.isInTrash(), + }); + } else { this.addDropdownItem({ label: 'Move to Folder', name: 'moveToFolder', @@ -194,8 +229,8 @@ class EmailDetailRecordView extends DetailRecordView { } }); - this.listenTo(this.model, 'change:inTrash', () => { - if (this.model.get('inTrash')) { + this.listenTo(this.model, 'change:inTrash change:groupStatusFolder', () => { + if (this.isInTrash()) { this.hideActionItem('moveToTrash'); this.showActionItem('retrieveFromTrash'); } else { @@ -204,8 +239,8 @@ class EmailDetailRecordView extends DetailRecordView { } }); - this.listenTo(this.model, 'change:inArchive', () => { - if (this.model.get('inArchive')) { + this.listenTo(this.model, 'change:inArchive change:groupStatusFolder', () => { + if (this.isInArchive()) { this.hideActionItem('moveToArchive'); } else { this.showActionItem('moveToArchive'); @@ -297,7 +332,11 @@ class EmailDetailRecordView extends DetailRecordView { Espo.Ui.warning(this.translate('Moved to Trash', 'labels', 'Email')); }); - this.model.set('inTrash', true); + if (this.model.attributes.groupFolderId) { + this.model.set('groupStatusFolder', 'Trash'); + } else { + this.model.set('inTrash', true); + } if (this.model.collection) { this.model.collection.trigger('moving-to-trash', this.model.id, true); @@ -312,6 +351,10 @@ class EmailDetailRecordView extends DetailRecordView { this.model.set('inTrash', false); + if (this.model.attributes.groupFolderId) { + this.model.set('groupStatusFolder', null); + } + if (this.model.collection) { this.model.collection.trigger('retrieving-from-trash', this.model.id, true); } @@ -320,22 +363,39 @@ class EmailDetailRecordView extends DetailRecordView { actionMoveToFolder() { this.createView('dialog', 'views/email-folder/modals/select-folder', { headerText: this.translate('Move to Folder', 'labels', 'Email'), - }, (view) => { + isGroup: !!this.model.attributes.groupFolderId || !this.model.attributes.isUsers, + noArchive: !this.model.attributes.groupFolderId && !this.model.attributes.isUsers, + }, view => { view.render(); - this.listenToOnce(view, 'select', folderId => { + this.listenToOnce(view, 'select', /** string|null */folderId => { this.clearView('dialog'); Espo.Ajax.postRequest(`Email/inbox/folders/${folderId}`, {id: this.model.id}) .then(() => { - this.model.set('inArchive', folderId === 'archive'); + if (this.model.attributes.groupFolderId) { + if (folderId === 'archive') { + this.model.set('groupStatusFolder', 'Archive'); + } else { + this.model.set('groupStatusFolder', null); + } + } else { + this.model.set('inArchive', folderId === 'archive'); + } if (folderId === 'inbox' || folderId === 'archive') { folderId = null; } + if (!folderId) { + this.model.set('groupFolderId', null); + this.model.set('groupFolderName', null); + } + this.model.set('folderId', folderId); + this.model.fetch(); + Espo.Ui.success(this.translate('Done')); }); }); @@ -347,7 +407,9 @@ class EmailDetailRecordView extends DetailRecordView { Espo.Ajax.postRequest(`Email/inbox/folders/archive`, {id: this.model.id}) .then(() => { - this.model.set('inArchive', true); + this.model.attributes.groupFolderId ? + this.model.set('groupStatusFolder', 'Archive') : + this.model.set('inArchive', true); Espo.Ui.info(this.translate('Moved to Archive', 'labels', 'Email')); @@ -619,6 +681,26 @@ class EmailDetailRecordView extends DetailRecordView { this.actionSaveAndContinueEditing(); } + + /** + * @private + * @return {boolean} + */ + isInTrash() { + return this.model.attributes.groupFolderId ? + this.model.attributes.groupStatusFolder === 'Trash' : + this.model.attributes.inTrash; + } + + /** + * @private + * @return {boolean} + */ + isInArchive() { + return this.model.attributes.groupFolderId ? + this.model.attributes.groupStatusFolder === 'Archive' : + this.model.attributes.inArchive; + } } export default EmailDetailRecordView; diff --git a/client/src/views/email/record/list.js b/client/src/views/email/record/list.js index 2296a6bc2e..91d1277f9b 100644 --- a/client/src/views/email/record/list.js +++ b/client/src/views/email/record/list.js @@ -74,7 +74,9 @@ class EmailListRecordView extends ListRecordView { const model = this.collection.get(id); if (model) { - model.set('inTrash', true); + model.attributes.groupFolderId ? + model.set('groupFolderStatus', 'Trash') : + model.set('inTrash', true); } if (this.rootData.selectedFolderId !== 'trash' && this.rootData.selectedFolderId !== 'all') { @@ -92,7 +94,9 @@ class EmailListRecordView extends ListRecordView { const model = this.collection.get(id); if (model) { - model.set('inTrash', false); + model.attributes.groupFolderId ? + model.set('groupFolderStatus', null) : + model.set('inTrash', false); } if (this.rootData.selectedFolderId === 'trash') { @@ -110,7 +114,9 @@ class EmailListRecordView extends ListRecordView { const model = this.collection.get(id); if (model) { - model.set('inArchive', true); + model.attributes.groupFolderId ? + model.set('groupFolderStatus', 'Archive') : + model.set('inArchive', true); } if (this.rootData.selectedFolderId !== 'archive') { @@ -307,10 +313,22 @@ class EmailListRecordView extends ListRecordView { }); } + /** + * @private + * @return {string|null|undefined} + */ + getSelectedFolderId() { + return this.rootData.selectedFolderId; + } + // noinspection JSUnusedGlobalSymbols massActionMoveToFolder() { + const selectedFolderId = this.getSelectedFolderId(); + this.createView('dialog', 'views/email-folder/modals/select-folder', { headerText: this.translate('Move to Folder', 'labels', 'Email'), + isGroup: selectedFolderId && (selectedFolderId.startsWith('group:') || selectedFolderId === 'all'), + noArchive: selectedFolderId === 'all', }, view => { view.render(); @@ -492,8 +510,16 @@ class EmailListRecordView extends ListRecordView { return; } + const model = this.collection.get(id); + + if (!model) { + return; + } + this.createView('dialog', 'views/email-folder/modals/select-folder', { headerText: this.translate('Move to Folder', 'labels', 'Email'), + isGroup: !!model.attributes.groupFolderId || !model.attributes.isUsers, + noArchive: !model.attributes.groupFolderId && !model.attributes.isUsers, }, view => { view.render(); diff --git a/client/src/views/email/record/row-actions/default.js b/client/src/views/email/record/row-actions/default.js index ddc833f5a0..9a5667dac0 100644 --- a/client/src/views/email/record/row-actions/default.js +++ b/client/src/views/email/record/row-actions/default.js @@ -33,12 +33,8 @@ class EmailDefaultRowActionView extends DefaultRowActionsView { setup() { super.setup(); - this.listenTo(this.model, 'change', (model) => { - if (model.hasChanged('isImportant') || model.hasChanged('inTrash')) { - setTimeout(() => { - this.reRender(); - }, 10); - } + this.listenTo(this.model, 'change:isImportant change:inTrash change:groupStatusFolder', () => { + setTimeout(() => this.reRender(), 10); }); } @@ -55,7 +51,7 @@ class EmailDefaultRowActionView extends DefaultRowActionsView { if ( this.model.get('createdById') === this.getUser().id && this.model.get('status') === 'Draft' && - !this.model.get('inTrash') + !this.model.attributes.inTrash ) { list.push({ action: 'send', @@ -103,7 +99,7 @@ class EmailDefaultRowActionView extends DefaultRowActionsView { } } - if (this.model.get('isUsers') && !this.model.get('isRead')) { + if (this.model.attributes.isUsers && !this.model.attributes.isRead) { list.push({ action: 'markAsRead', label: 'Mark Read', @@ -114,8 +110,19 @@ class EmailDefaultRowActionView extends DefaultRowActionsView { }); } - if (this.model.get('isUsers') && this.model.get('status') !== 'Draft') { - if (!this.model.get('inTrash')) { + if ( + (this.model.attributes.isUsers && this.model.attributes.status !== 'Draft') || + this.model.attributes.groupFolderId + ) { + const inTrash = this.model.attributes.groupFolderId ? + this.model.attributes.groupStatusFolder === 'Trash' : + this.model.attributes.inTrash; + + const inArchive = this.model.attributes.groupFolderId ? + this.model.attributes.groupStatusFolder === 'Archive' : + this.model.attributes.inArchive; + + if (!inTrash) { list.push({ action: 'moveToTrash', label: 'Move to Trash', @@ -135,7 +142,7 @@ class EmailDefaultRowActionView extends DefaultRowActionsView { }); } - if (!this.model.attributes.inArchive) { + if (!inArchive) { list.push({ action: 'moveToArchive', text: this.getLanguage().translatePath('Email.actions.moveToArchive'), @@ -145,9 +152,20 @@ class EmailDefaultRowActionView extends DefaultRowActionsView { groupIndex: 2, }); } - } - if (this.model.get('isUsers') && this.model.get('status') !== 'Draft') { + list.push({ + action: 'moveToFolder', + label: 'Move to Folder', + data: { + id: this.model.id + }, + groupIndex: 2, + }); + } else if ( + !this.model.attributes.isUsers && + !this.model.attributes.groupFolderId && + this.model.attributes.status === 'Archived' + ) { list.push({ action: 'moveToFolder', label: 'Move to Folder',