From 6e6d3e15eafe77c7614d542ad786c84263fcba04 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Mon, 24 Jun 2024 12:44:35 +0300 Subject: [PATCH] emails archive --- .../Email/FolderDataLoader.php | 66 +++++++++ .../Email/UserColumnsLoader.php | 16 +-- .../Classes/MassAction/Email/MoveToFolder.php | 6 +- .../Select/Email/AdditionalAppliers/Main.php | 1 + .../Where/ItemConverters/InArchiveIsFalse.php | 54 ++++++++ .../Where/ItemConverters/InArchiveIsTrue.php | 54 ++++++++ .../Email/Where/ItemConverters/InFolder.php | 36 +++-- application/Espo/Entities/Email.php | 1 + application/Espo/Entities/EmailFolder.php | 9 +- .../Espo/Resources/i18n/en_US/Email.json | 14 +- .../layouts/Email/defaultSidePanel.json | 3 + .../Resources/metadata/clientDefs/Email.json | 14 ++ .../Resources/metadata/entityDefs/Email.json | 35 ++++- .../Resources/metadata/recordDefs/Email.json | 1 + .../Resources/metadata/selectDefs/Email.json | 2 + application/Espo/Tools/Email/Folder.php | 1 + application/Espo/Tools/Email/InboxService.php | 92 ++++++++++--- .../Espo/Tools/EmailFolder/Service.php | 1 + .../email-folder/modals/select-folder.js | 22 ++- client/src/views/email/detail.js | 37 +++++- .../src/views/email/fields/folder-string.js | 102 ++++++++++++++ client/src/views/email/fields/subject.js | 125 +++++++++--------- client/src/views/email/list.js | 18 ++- client/src/views/email/record/detail.js | 35 ++++- client/src/views/email/record/list.js | 62 +++++++-- 25 files changed, 681 insertions(+), 126 deletions(-) create mode 100644 application/Espo/Classes/FieldProcessing/Email/FolderDataLoader.php create mode 100644 application/Espo/Classes/Select/Email/Where/ItemConverters/InArchiveIsFalse.php create mode 100644 application/Espo/Classes/Select/Email/Where/ItemConverters/InArchiveIsTrue.php create mode 100644 client/src/views/email/fields/folder-string.js diff --git a/application/Espo/Classes/FieldProcessing/Email/FolderDataLoader.php b/application/Espo/Classes/FieldProcessing/Email/FolderDataLoader.php new file mode 100644 index 0000000000..7113388e98 --- /dev/null +++ b/application/Espo/Classes/FieldProcessing/Email/FolderDataLoader.php @@ -0,0 +1,66 @@ +. + * + * 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 Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Classes\FieldProcessing\Email; + +use Espo\Core\FieldProcessing\Loader; +use Espo\Core\FieldProcessing\Loader\Params; +use Espo\Entities\Email; +use Espo\Entities\EmailFolder; +use Espo\ORM\Entity; +use Espo\ORM\EntityManager; + +/** + * @implements Loader + */ +class FolderDataLoader implements Loader +{ + public function __construct(private EntityManager $entityManager) {} + + public function process(Entity $entity, Params $params): void + { + $folderId = $entity->get(Email::USERS_COLUMN_FOLDER_ID); + + if (!$folderId) { + return; + } + + $folder = $this->entityManager + ->getRDBRepositoryByClass(EmailFolder::class) + ->select(['id', 'name']) + ->where(['id' => $folderId]) + ->findOne(); + + if (!$folder) { + return; + } + + $entity->set('folderName', $folder->getName()); + } +} diff --git a/application/Espo/Classes/FieldProcessing/Email/UserColumnsLoader.php b/application/Espo/Classes/FieldProcessing/Email/UserColumnsLoader.php index 5423c3a1b5..e1f52577a6 100644 --- a/application/Espo/Classes/FieldProcessing/Email/UserColumnsLoader.php +++ b/application/Espo/Classes/FieldProcessing/Email/UserColumnsLoader.php @@ -41,14 +41,10 @@ use Espo\Entities\User; */ class UserColumnsLoader implements Loader { - private EntityManager $entityManager; - private User $user; - - public function __construct(EntityManager $entityManager, User $user) - { - $this->entityManager = $entityManager; - $this->user = $user; - } + public function __construct( + private EntityManager $entityManager, + private User $user + ) {} public function process(Entity $entity, Params $params): void { @@ -58,6 +54,7 @@ class UserColumnsLoader implements Loader Email::USERS_COLUMN_IS_READ, Email::USERS_COLUMN_IS_IMPORTANT, Email::USERS_COLUMN_IN_TRASH, + Email::USERS_COLUMN_IN_ARCHIVE, ]) ->where([ 'deleted' => false, @@ -70,6 +67,7 @@ class UserColumnsLoader implements Loader $entity->set(Email::USERS_COLUMN_IS_READ, null); $entity->clear(Email::USERS_COLUMN_IS_IMPORTANT); $entity->clear(Email::USERS_COLUMN_IN_TRASH); + $entity->clear(Email::USERS_COLUMN_IN_ARCHIVE); return; } @@ -78,6 +76,8 @@ class UserColumnsLoader implements Loader Email::USERS_COLUMN_IS_READ => $emailUser->get(Email::USERS_COLUMN_IS_READ), Email::USERS_COLUMN_IS_IMPORTANT => $emailUser->get(Email::USERS_COLUMN_IS_IMPORTANT), Email::USERS_COLUMN_IN_TRASH => $emailUser->get(Email::USERS_COLUMN_IN_TRASH), + Email::USERS_COLUMN_IN_ARCHIVE => $emailUser->get(Email::USERS_COLUMN_IN_ARCHIVE), + 'isUsersSent' => $entity->getSentBy()?->getId() === $this->user->getId(), ]); } } diff --git a/application/Espo/Classes/MassAction/Email/MoveToFolder.php b/application/Espo/Classes/MassAction/Email/MoveToFolder.php index 7e52d20edc..963a281afa 100644 --- a/application/Espo/Classes/MassAction/Email/MoveToFolder.php +++ b/application/Espo/Classes/MassAction/Email/MoveToFolder.php @@ -68,7 +68,11 @@ class MoveToFolder implements MassAction throw new BadRequest("No folder ID."); } - if ($folderId !== Folder::INBOX && !str_starts_with($folderId, 'group:')) { + if ( + $folderId !== Folder::INBOX && + $folderId !== Folder::ARCHIVE && + !str_starts_with($folderId, 'group:') + ) { $folder = $this->entityManager ->getRDBRepositoryByClass(EmailFolder::class) ->where([ diff --git a/application/Espo/Classes/Select/Email/AdditionalAppliers/Main.php b/application/Espo/Classes/Select/Email/AdditionalAppliers/Main.php index 300145cd0b..44219ba9ab 100644 --- a/application/Espo/Classes/Select/Email/AdditionalAppliers/Main.php +++ b/application/Espo/Classes/Select/Email/AdditionalAppliers/Main.php @@ -93,6 +93,7 @@ class Main implements AdditionalApplier Email::USERS_COLUMN_IS_READ, Email::USERS_COLUMN_IS_IMPORTANT, Email::USERS_COLUMN_IN_TRASH, + Email::USERS_COLUMN_IN_ARCHIVE, Email::USERS_COLUMN_FOLDER_ID, ]; diff --git a/application/Espo/Classes/Select/Email/Where/ItemConverters/InArchiveIsFalse.php b/application/Espo/Classes/Select/Email/Where/ItemConverters/InArchiveIsFalse.php new file mode 100644 index 0000000000..3c39ae57c9 --- /dev/null +++ b/application/Espo/Classes/Select/Email/Where/ItemConverters/InArchiveIsFalse.php @@ -0,0 +1,54 @@ +. + * + * 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 Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Classes\Select\Email\Where\ItemConverters; + +use Espo\Core\Select\Where\Item; +use Espo\Core\Select\Where\ItemConverter; +use Espo\Classes\Select\Email\Helpers\JoinHelper; +use Espo\Entities\Email; +use Espo\Entities\User; +use Espo\ORM\Query\Part\WhereClause; +use Espo\ORM\Query\Part\WhereItem as WhereClauseItem; +use Espo\ORM\Query\SelectBuilder as QueryBuilder; + +class InArchiveIsFalse implements ItemConverter +{ + public function __construct(private User $user, private JoinHelper $joinHelper) + {} + + public function convert(QueryBuilder $queryBuilder, Item $item): WhereClauseItem + { + $this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId()); + + return WhereClause::fromRaw([ + Email::ALIAS_INBOX . '.inArchive' => false, + ]); + } +} diff --git a/application/Espo/Classes/Select/Email/Where/ItemConverters/InArchiveIsTrue.php b/application/Espo/Classes/Select/Email/Where/ItemConverters/InArchiveIsTrue.php new file mode 100644 index 0000000000..8648d1d417 --- /dev/null +++ b/application/Espo/Classes/Select/Email/Where/ItemConverters/InArchiveIsTrue.php @@ -0,0 +1,54 @@ +. + * + * 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 Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Classes\Select\Email\Where\ItemConverters; + +use Espo\Core\Select\Where\Item; +use Espo\Core\Select\Where\ItemConverter; +use Espo\Classes\Select\Email\Helpers\JoinHelper; +use Espo\Entities\Email; +use Espo\Entities\User; +use Espo\ORM\Query\Part\WhereClause; +use Espo\ORM\Query\Part\WhereItem as WhereClauseItem; +use Espo\ORM\Query\SelectBuilder as QueryBuilder; + +class InArchiveIsTrue implements ItemConverter +{ + public function __construct(private User $user, private JoinHelper $joinHelper) + {} + + public function convert(QueryBuilder $queryBuilder, Item $item): WhereClauseItem + { + $this->joinHelper->joinEmailUser($queryBuilder, $this->user->getId()); + + return WhereClause::fromRaw([ + Email::ALIAS_INBOX . '.inArchive' => true, + ]); + } +} diff --git a/application/Espo/Classes/Select/Email/Where/ItemConverters/InFolder.php b/application/Espo/Classes/Select/Email/Where/ItemConverters/InFolder.php index 85d679774e..fa7fefba88 100644 --- a/application/Espo/Classes/Select/Email/Where/ItemConverters/InFolder.php +++ b/application/Espo/Classes/Select/Email/Where/ItemConverters/InFolder.php @@ -41,6 +41,9 @@ use Espo\Entities\User; use Espo\Classes\Select\Email\Helpers\JoinHelper; use Espo\Tools\Email\Folder; +/** + * @noinspection PhpUnused + */ class InFolder implements ItemConverter { public function __construct( @@ -59,17 +62,19 @@ class InFolder implements ItemConverter Folder::IMPORTANT => $this->convertImportant($queryBuilder), Folder::SENT => $this->convertSent($queryBuilder), Folder::TRASH => $this->convertTrash($queryBuilder), - Folder::DRAFTS => $this->convertDraft($queryBuilder), + Folder::ARCHIVE => $this->convertArchive($queryBuilder), + Folder::DRAFTS => $this->convertDraft(), default => $this->convertFolderId($queryBuilder, $folderId), }; } - protected function convertInbox(QueryBuilder $queryBuilder): WhereClauseItem + private function convertInbox(QueryBuilder $queryBuilder): WhereClauseItem { $this->joinEmailUser($queryBuilder); $whereClause = [ Email::ALIAS_INBOX . '.inTrash' => false, + Email::ALIAS_INBOX . '.inArchive' => false, Email::ALIAS_INBOX . '.folderId' => null, Email::ALIAS_INBOX . '.userId' => $this->user->getId(), [ @@ -83,7 +88,7 @@ class InFolder implements ItemConverter $emailAddressIdList = $this->getEmailAddressIdList(); - if (!empty($emailAddressIdList)) { + if ($emailAddressIdList !== []) { $whereClause['fromEmailAddressId!='] = $emailAddressIdList; $whereClause[] = [ @@ -92,8 +97,7 @@ class InFolder implements ItemConverter 'createdById!=' => $this->user->getId(), ], ]; - } - else { + } else { $whereClause[] = [ 'status' => Email::STATUS_ARCHIVED, 'createdById!=' => $this->user->getId(), @@ -103,7 +107,7 @@ class InFolder implements ItemConverter return WhereClause::fromRaw($whereClause); } - protected function convertSent(QueryBuilder $queryBuilder): WhereClauseItem + private function convertSent(QueryBuilder $queryBuilder): WhereClauseItem { $this->joinEmailUser($queryBuilder); @@ -122,7 +126,7 @@ class InFolder implements ItemConverter ]); } - protected function convertImportant(QueryBuilder $queryBuilder): WhereClauseItem + private function convertImportant(QueryBuilder $queryBuilder): WhereClauseItem { $this->joinEmailUser($queryBuilder); @@ -132,7 +136,7 @@ class InFolder implements ItemConverter ]); } - protected function convertTrash(QueryBuilder $queryBuilder): WhereClauseItem + private function convertTrash(QueryBuilder $queryBuilder): WhereClauseItem { $this->joinEmailUser($queryBuilder); @@ -142,7 +146,17 @@ class InFolder implements ItemConverter ]); } - protected function convertDraft(QueryBuilder $queryBuilder): WhereClauseItem + private function convertArchive(QueryBuilder $queryBuilder): WhereClauseItem + { + $this->joinEmailUser($queryBuilder); + + return WhereClause::fromRaw([ + Email::ALIAS_INBOX . '.userId' => $this->user->getId(), + Email::ALIAS_INBOX . '.inArchive' => true, + ]); + } + + private function convertDraft(): WhereClauseItem { return WhereClause::fromRaw([ 'status' => Email::STATUS_DRAFT, @@ -150,7 +164,7 @@ class InFolder implements ItemConverter ]); } - protected function convertFolderId(QueryBuilder $queryBuilder, string $folderId): WhereClauseItem + private function convertFolderId(QueryBuilder $queryBuilder, string $folderId): WhereClauseItem { $this->joinEmailUser($queryBuilder); @@ -166,12 +180,14 @@ class InFolder implements ItemConverter 'OR' => [ Email::ALIAS_INBOX . '.id' => null, Email::ALIAS_INBOX . '.inTrash' => false, + Email::ALIAS_INBOX . '.inArchive' => false, ] ]); } return WhereClause::fromRaw([ Email::ALIAS_INBOX . '.inTrash' => false, + Email::ALIAS_INBOX . '.inArchive' => false, Email::ALIAS_INBOX . '.folderId' => $folderId, 'groupFolderId' => null, ]); diff --git a/application/Espo/Entities/Email.php b/application/Espo/Entities/Email.php index acd331fcb1..76c0fc9c9d 100644 --- a/application/Espo/Entities/Email.php +++ b/application/Espo/Entities/Email.php @@ -55,6 +55,7 @@ class Email extends Entity public const USERS_COLUMN_IS_READ = 'isRead'; public const USERS_COLUMN_IN_TRASH = 'inTrash'; + public const USERS_COLUMN_IN_ARCHIVE = 'inArchive'; public const USERS_COLUMN_FOLDER_ID = 'folderId'; public const USERS_COLUMN_IS_IMPORTANT = 'isImportant'; diff --git a/application/Espo/Entities/EmailFolder.php b/application/Espo/Entities/EmailFolder.php index 46ba2dc5fb..da1084a2f8 100644 --- a/application/Espo/Entities/EmailFolder.php +++ b/application/Espo/Entities/EmailFolder.php @@ -29,8 +29,15 @@ namespace Espo\Entities; -class EmailFolder extends \Espo\Core\ORM\Entity +use Espo\Core\ORM\Entity; + +class EmailFolder extends Entity { public const ENTITY_TYPE = 'EmailFolder'; + + public function getName(): string + { + return $this->get('name') ?? ''; + } } diff --git a/application/Espo/Resources/i18n/en_US/Email.json b/application/Espo/Resources/i18n/en_US/Email.json index a31cdb4ebd..01b5d69278 100644 --- a/application/Espo/Resources/i18n/en_US/Email.json +++ b/application/Espo/Resources/i18n/en_US/Email.json @@ -30,7 +30,9 @@ "isReplied": "Is Replied", "isNotReplied": "Is Not Replied", "isUsers": "Is User's", + "isUsersSent": "Is User's Sent", "inTrash": "In Trash", + "inArchive": "In Archive", "folder": "Folder", "inboundEmails": "Group Accounts", "emailAccounts": "Personal Accounts", @@ -44,6 +46,7 @@ "messageId": "Message Id", "messageIdInternal": "Message Id (Internal)", "folderId": "Folder Id", + "folderString": "Folder", "fromName": "From Name", "fromString": "From String", "fromAddress": "From Address", @@ -79,7 +82,7 @@ "Draft": "Draft", "Sending": "Sending", "Sent": "Sent", - "Archived": "Archived", + "Archived": "Imported", "Received": "Received", "Failed": "Failed" } @@ -111,7 +114,7 @@ "Move to Trash": "Move to Trash", "Retrieve from Trash": "Retrieve from Trash", "Move to Folder": "Move to Folder", - "Moving to folder": "Moving to Folder", + "Moved to Archive": "Moved to Archive", "Filters": "Filters", "Folders": "Folders", "Group Folders": "Group Folders", @@ -141,12 +144,16 @@ }, "presetFilters": { "sent": "Sent", - "archived": "Archived", + "archived": "Imported", "inbox": "Inbox", "drafts": "Drafts", "trash": "Trash", + "archive": "Archive", "important": "Important" }, + "actions": { + "moveToArchive": "Archive" + }, "massActions": { "markAsRead": "Mark as Read", "markAsNotRead": "Mark as Not Read", @@ -154,6 +161,7 @@ "markAsNotImportant": "Unmark Importance", "moveToTrash": "Move to Trash", "moveToFolder": "Move to Folder", + "moveToArchive": "Archive", "retrieveFromTrash": "Retrieve from Trash" }, "otherFields": { diff --git a/application/Espo/Resources/layouts/Email/defaultSidePanel.json b/application/Espo/Resources/layouts/Email/defaultSidePanel.json index 7c670fd340..a6c18ff3f9 100644 --- a/application/Espo/Resources/layouts/Email/defaultSidePanel.json +++ b/application/Espo/Resources/layouts/Email/defaultSidePanel.json @@ -5,6 +5,9 @@ { "name": "status" }, + { + "name": "folderString" + }, { "name": "replied" }, diff --git a/application/Espo/Resources/metadata/clientDefs/Email.json b/application/Espo/Resources/metadata/clientDefs/Email.json index f728161526..3596961636 100644 --- a/application/Espo/Resources/metadata/clientDefs/Email.json +++ b/application/Espo/Resources/metadata/clientDefs/Email.json @@ -144,6 +144,20 @@ } ] } + }, + "folderString": { + "visible": { + "conditionGroup": [ + { + "type": "isTrue", + "attribute": "isUsers" + }, + { + "type": "isFalse", + "attribute": "isUsersSent" + } + ] + } } }, "panels": { diff --git a/application/Espo/Resources/metadata/entityDefs/Email.json b/application/Espo/Resources/metadata/entityDefs/Email.json index c1a859196f..0858ff558f 100644 --- a/application/Espo/Resources/metadata/entityDefs/Email.json +++ b/application/Espo/Resources/metadata/entityDefs/Email.json @@ -198,6 +198,13 @@ "customizationDisabled": true, "layoutMassUpdateDisabled": true }, + "inArchive": { + "type": "bool", + "notStorable": true, + "default": false, + "customizationDisabled": true, + "layoutMassUpdateDisabled": true + }, "folderId": { "type": "varchar", "notStorable": true, @@ -213,13 +220,32 @@ "customizationDisabled": true, "layoutMassUpdateDisabled": true }, + "isUsersSent": { + "type": "bool", + "notStorable": true, + "readOnly": true, + "utility": true + }, "folder": { "type": "link", "notStorable": true, "orderDisabled": true, "readOnly": true, "entity": "EmailFolder", - "customizationDisabled": true + "customizationDisabled": true, + "layoutAvailabilityList": [] + }, + "folderString": { + "type": "link", + "notStorable": true, + "orderDisabled": true, + "readOnly": true, + "entity": "EmailFolder", + "customizationDisabled": true, + "view": "views/email/fields/folder-string", + "layoutAvailabilityList": [ + "defaultSidePanel" + ] }, "nameHash": { "type": "text", @@ -320,7 +346,8 @@ "tooltipText" ], "inlineEditDisabled": true, - "layoutMassUpdateDisabled": true + "layoutMassUpdateDisabled": true, + "layoutDefaultSidePanelDisabled": true }, "status": { "type": "enum", @@ -585,6 +612,10 @@ "type": "bool", "default": false }, + "inArchive": { + "type": "bool", + "default": false + }, "folderId": { "type": "foreignId", "default": null diff --git a/application/Espo/Resources/metadata/recordDefs/Email.json b/application/Espo/Resources/metadata/recordDefs/Email.json index e31b3eaa92..50dc069d3f 100644 --- a/application/Espo/Resources/metadata/recordDefs/Email.json +++ b/application/Espo/Resources/metadata/recordDefs/Email.json @@ -3,6 +3,7 @@ "readLoaderClassNameList": [ "Espo\\Classes\\FieldProcessing\\Email\\AddressDataLoader", "Espo\\Classes\\FieldProcessing\\Email\\UserColumnsLoader", + "Espo\\Classes\\FieldProcessing\\Email\\FolderDataLoader", "Espo\\Classes\\FieldProcessing\\Email\\IcsDataLoader" ], "listLoaderClassNameList": [ diff --git a/application/Espo/Resources/metadata/selectDefs/Email.json b/application/Espo/Resources/metadata/selectDefs/Email.json index 06eeda5751..206caf0e92 100644 --- a/application/Espo/Resources/metadata/selectDefs/Email.json +++ b/application/Espo/Resources/metadata/selectDefs/Email.json @@ -13,6 +13,8 @@ "isRead_isFalse": "Espo\\Classes\\Select\\Email\\Where\\ItemConverters\\IsNotReadIsTrue", "inTrash_isTrue": "Espo\\Classes\\Select\\Email\\Where\\ItemConverters\\InTrashIsFalse", "inTrash_isFalse": "Espo\\Classes\\Select\\Email\\Where\\ItemConverters\\InTrashIsTrue", + "inArchive_isTrue": "Espo\\Classes\\Select\\Email\\Where\\ItemConverters\\InArchiveIsFalse", + "inArchive_isFalse": "Espo\\Classes\\Select\\Email\\Where\\ItemConverters\\InArchiveIsTrue", "isImportant_isTrue": "Espo\\Classes\\Select\\Email\\Where\\ItemConverters\\IsImportantIsTrue", "isImportant_isFalse": "Espo\\Classes\\Select\\Email\\Where\\ItemConverters\\IsImportantIsFalse" }, diff --git a/application/Espo/Tools/Email/Folder.php b/application/Espo/Tools/Email/Folder.php index 438aacdafb..5b8820744a 100644 --- a/application/Espo/Tools/Email/Folder.php +++ b/application/Espo/Tools/Email/Folder.php @@ -36,5 +36,6 @@ class Folder public const SENT = 'sent'; public const DRAFTS = 'drafts'; public const IMPORTANT = 'important'; + public const ARCHIVE = 'archive'; public const TRASH = 'trash'; } diff --git a/application/Espo/Tools/Email/InboxService.php b/application/Espo/Tools/Email/InboxService.php index d94926ca40..3b92008fab 100644 --- a/application/Espo/Tools/Email/InboxService.php +++ b/application/Espo/Tools/Email/InboxService.php @@ -100,17 +100,7 @@ class InboxService $previousFolderLink = $email->getGroupFolder(); if ($previousFolderLink) { - $previousFolderId = $previousFolderLink->getId(); - - $previousFolder = $this->entityManager->getEntityById(GroupEmailFolder::ENTITY_TYPE, $previousFolderId); - - if ($previousFolder && !$this->aclManager->checkEntityRead($user, $previousFolder)) { - throw new Forbidden("No access to current group folder."); - } - - if (!$this->aclManager->checkField($user, Email::ENTITY_TYPE, 'groupFolder', Table::ACTION_EDIT)) { - throw new Forbidden("No access to `groupFolder` field."); - } + $this->checkCurrentGroupFolder($previousFolderLink->getId(), $user); } if ($folderId && str_starts_with($folderId, 'group:')) { @@ -118,7 +108,7 @@ class InboxService $this->moveToGroupFolder($email, substr($folderId, 6), $user); } catch (Exception $e) { - $this->log->debug("Move to group folder exception: " . $e->getMessage()); + $this->log->debug("Could not move email to group folder. " . $e->getMessage()); throw $e; } @@ -126,8 +116,14 @@ class InboxService return; } + if ($folderId === Folder::ARCHIVE) { + $this->moveToArchive($email, $user); + + return; + } + if ($previousFolderLink) { - $email->set('groupFolderId', null); + $email->setGroupFolderId(null); if (!$this->aclManager->checkEntityRead($user, $email)) { throw new Forbidden("No read access to email to unset group folder."); @@ -143,6 +139,7 @@ class InboxService ->set([ 'folderId' => $folderId, 'inTrash' => false, + 'inArchive' => false, ]) ->where([ 'deleted' => false, @@ -178,9 +175,10 @@ class InboxService throw new Forbidden("No access to `groupFolder` field."); } - $email->set('groupFolderId', $folderId); - + $email->setGroupFolderId($folderId); $this->entityManager->saveEntity($email); + + $this->retrieveFromArchive($email, $user); } /** @@ -496,4 +494,68 @@ class InboxService return $data; } + + /** + * @throws Forbidden + */ + public function moveToArchive(Email $email, User $user): void + { + if (!$this->aclManager->checkEntityRead($user, $email)) { + throw new Forbidden("No 'read' access"); + } + + $update = $this->entityManager + ->getQueryBuilder() + ->update() + ->in(Email::RELATIONSHIP_EMAIL_USER) + ->set([ + 'folderId' => null, + 'inArchive' => true, + 'inTrash' => false, + ]) + ->where([ + 'deleted' => false, + 'userId' => $user->getId(), + 'emailId' => $email->getId(), + ]) + ->build(); + + $this->entityManager->getQueryExecutor()->execute($update); + } + + public function retrieveFromArchive(Email $email, User $user): void + { + $update = $this->entityManager + ->getQueryBuilder() + ->update() + ->in(Email::RELATIONSHIP_EMAIL_USER) + ->set([ + 'folderId' => null, + 'inArchive' => false, + ]) + ->where([ + 'deleted' => false, + 'userId' => $user->getId(), + 'emailId' => $email->getId(), + ]) + ->build(); + + $this->entityManager->getQueryExecutor()->execute($update); + } + + /** + * @throws Forbidden + */ + private function checkCurrentGroupFolder(string $folderId, User $user): void + { + $folder = $this->entityManager->getEntityById(GroupEmailFolder::ENTITY_TYPE, $folderId); + + if ($folder && !$this->aclManager->checkEntityRead($user, $folder)) { + throw new Forbidden("No access to current group folder."); + } + + if (!$this->aclManager->checkField($user, Email::ENTITY_TYPE, 'groupFolder', Table::ACTION_EDIT)) { + throw new Forbidden("No access to `groupFolder` field."); + } + } } diff --git a/application/Espo/Tools/EmailFolder/Service.php b/application/Espo/Tools/EmailFolder/Service.php index ac2556c5ae..f938e0dcfd 100644 --- a/application/Espo/Tools/EmailFolder/Service.php +++ b/application/Espo/Tools/EmailFolder/Service.php @@ -55,6 +55,7 @@ class Service /** @var string[] */ protected $systemFolderEndList = [ Folder::DRAFTS, + Folder::ARCHIVE, Folder::TRASH, ]; diff --git a/client/src/views/email-folder/modals/select-folder.js b/client/src/views/email-folder/modals/select-folder.js index f3966312ca..373dfc0ede 100644 --- a/client/src/views/email-folder/modals/select-folder.js +++ b/client/src/views/email-folder/modals/select-folder.js @@ -46,10 +46,10 @@ define('views/email-folder/modals/select-folder', ['views/modal'], function (Dep events: { 'click a[data-action="selectFolder"]': function (e) { - let $target = $(e.currentTarget); + const $target = $(e.currentTarget); - let id = $target.attr('data-id'); - let name = $target.attr('data-name'); + const id = $target.attr('data-id'); + const name = $target.attr('data-name'); this.trigger('select', id, name); this.close(); @@ -75,7 +75,14 @@ define('views/email-folder/modals/select-folder', ['views/modal'], function (Dep this.folderDataList = data.list .filter(item => { - return ['inbox', 'important', 'sent', 'drafts', 'trash'].indexOf(item.id) === -1; + return [ + 'inbox', + 'important', + 'sent', + 'drafts', + 'trash', + 'archive', + ].indexOf(item.id) === -1; }) .map(item => { return { @@ -88,7 +95,12 @@ define('views/email-folder/modals/select-folder', ['views/modal'], function (Dep this.folderDataList.unshift({ id: 'inbox', name: this.translate('inbox', 'presetFilters', 'Email'), - }) + }); + + 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 ef65482920..ee2459f1aa 100644 --- a/client/src/views/email/detail.js +++ b/client/src/views/email/detail.js @@ -117,7 +117,11 @@ class EmailDetailView extends DetailView { return; } - if (!this.model.hasChanged('isImportant') && !this.model.hasChanged('inTrash')) { + if ( + !this.model.hasChanged('isImportant') && + !this.model.hasChanged('inTrash') && + !this.model.hasChanged('inArchive') + ) { return; } @@ -128,6 +132,23 @@ class EmailDetailView extends DetailView { } }); + this.shortcutKeys['Control+Backspace'] = e => { + if ($(e.target).hasClass('note-editable')) { + return; + } + + const recordView = /** @type {module:views/email/record/detail} */ this.getRecordView(); + + if (!this.model.get('isUsers') || this.model.get('inArchive')) { + return; + } + + e.preventDefault(); + e.stopPropagation(); + + recordView.actionMoveToArchive(); + }; + this.shortcutKeys['Control+Delete'] = e => { if ($(e.target).hasClass('note-editable')) { return; @@ -541,6 +562,7 @@ class EmailDetailView extends DetailView { const isImportant = this.model.get('isImportant'); const inTrash = this.model.get('inTrash'); + const inArchive = this.model.get('inArchive'); const rootUrl = this.options.rootUrl || this.options.params.rootUrl || '#' + this.scope; @@ -560,12 +582,21 @@ class EmailDetailView extends DetailView { .get(0).innerHTML; } + let styleClass = ''; + + if (isImportant) { + styleClass = 'text-warning' + } else if (inTrash) { + styleClass = 'text-muted'; + } else if (inArchive) { + styleClass = 'text-info'; + } + return this.buildHeaderHtml([ $root, $('') .addClass('font-size-flexible title') - .addClass(isImportant ? 'text-warning' : '') - .addClass(inTrash ? 'text-muted' : '') + .addClass(styleClass) .text(name), ]); } diff --git a/client/src/views/email/fields/folder-string.js b/client/src/views/email/fields/folder-string.js new file mode 100644 index 0000000000..a6751a9a5d --- /dev/null +++ b/client/src/views/email/fields/folder-string.js @@ -0,0 +1,102 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM – Open Source CRM application. + * Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko + * Website: https://www.espocrm.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * 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 Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +import BaseFieldView from 'views/fields/base'; + +class EmailFolderStringFieldView extends BaseFieldView { + + // language=Handlebars + detailTemplateContent = ` + {{#if valueIsSet}} + {{#if value}} + {{value}} + {{else}} + {{translate 'None'}} + {{/if}} + {{else}} + + {{/if}} + ` + + // noinspection JSCheckFunctionSignatures + data() { + if (!this.model.has('folderId')) { + return {valueIsSet: false}; + } + + return { + valueIsSet: true, + value: this.getFolderString(), + } + } + + getAttributeList() { + return [ + 'folderId', + 'folderName', + 'groupFolderId', + 'groupFolderName', + 'inArchive', + 'inTrash', + 'isUsersSent', + ]; + } + + /** + * @return {string} + */ + getFolderString() { + if (this.model.get('inTrash')) { + return this.translate('trash', 'presetFilters', 'Email'); + } + + if (this.model.get('isUsersSent')) { + return this.translate('sent', 'presetFilters', 'Email'); + } + + if (this.model.get('inArchive')) { + return this.translate('archive', 'presetFilters', 'Email'); + } + + if (this.model.get('folderName')) { + return this.model.get('folderName'); + } + + if (this.model.get('groupFolderName')) { + return this.model.get('groupFolderName'); + } + + if (this.model.get('isUsers')) { + return this.translate('inbox', 'presetFilters', 'Email'); + } + + return undefined; + } +} + +export default EmailFolderStringFieldView; diff --git a/client/src/views/email/fields/subject.js b/client/src/views/email/fields/subject.js index 16edbe66d9..0eaa8af6f4 100644 --- a/client/src/views/email/fields/subject.js +++ b/client/src/views/email/fields/subject.js @@ -26,83 +26,82 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -define('views/email/fields/subject', ['views/fields/varchar'], function (Dep) { +import VarcharFieldView from 'views/fields/varchar'; - return Dep.extend({ +class EmailSubjectFieldView extends VarcharFieldView { - listLinkTemplate: 'email/fields/subject/list-link', + listLinkTemplate = 'email/fields/subject/list-link' - data: function () { - let data = Dep.prototype.data.call(this); + data() { + const data = super.data(); - data.isRead = (this.model.get('sentById') === this.getUser().id) || this.model.get('isRead'); - 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.isRead = (this.model.get('sentById') === this.getUser().id) || this.model.get('isRead'); + 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'); - if (!data.isRead && !this.model.has('isRead')) { - data.isRead = true; + if (!data.isRead && !this.model.has('isRead')) { + data.isRead = true; + } + + if (!data.isNotEmpty) { + if ( + this.model.get('name') !== null && + this.model.get('name') !== '' && + this.model.has('name') + ) { + data.isNotEmpty = true; } + } - if (!data.isNotEmpty) { - if ( - this.model.get('name') !== null && - this.model.get('name') !== '' && - this.model.has('name') - ) { - data.isNotEmpty = true; + return data; + } + + getValueForDisplay() { + return this.model.get('name'); + } + + getAttributeList() { + return ['name', 'subject', 'isRead', 'isImportant', 'hasAttachment', 'inTrash']; + } + + setup() { + super.setup(); + + this.events['click [data-action="showAttachments"]'] = e => { + e.stopPropagation(); + + this.showAttachments(); + } + + this.listenTo(this.model, 'change', () => { + if (this.mode === this.MODE_LIST || this.mode === this.MODE_LIST_LINK) { + if (this.model.hasChanged('isRead') || this.model.hasChanged('isImportant')) { + this.reRender(); } } + }); + } - return data; - }, + fetch() { + const data = super.fetch(); - getValueForDisplay: function () { - return this.model.get('name'); - }, + data.name = data.subject; - getAttributeList: function () { - return ['name', 'subject', 'isRead', 'isImportant', 'hasAttachment', 'inTrash']; - }, + return data; + } - setup: function () { - Dep.prototype.setup.call(this); + showAttachments() { + Espo.Ui.notify(' ... '); - this.events['click [data-action="showAttachments"]'] = e => { - e.stopPropagation(); + this.createView('dialog', 'views/email/modals/attachments', {model: this.model}) + .then(view => { + view.render(); - this.showAttachments(); - } - - this.listenTo(this.model, 'change', () => { - if (this.mode === 'list' || this.mode === 'listLink') { - if (this.model.hasChanged('isRead') || this.model.hasChanged('isImportant')) { - this.reRender(); - } - } + Espo.Ui.notify(false); }); - }, + } +} - afterRender: function () { - Dep.prototype.afterRender.call(this); - }, - - fetch: function () { - var data = Dep.prototype.fetch.call(this); - data.name = data.subject; - return data; - }, - - showAttachments: function () { - Espo.Ui.notify(' ... '); - - this.createView('dialog', 'views/email/modals/attachments', {model: this.model}) - .then(view => { - view.render(); - - Espo.Ui.notify(false); - }); - }, - }); -}); +export default EmailSubjectFieldView; diff --git a/client/src/views/email/list.js b/client/src/views/email/list.js index e8c27da27a..9155237eb6 100644 --- a/client/src/views/email/list.js +++ b/client/src/views/email/list.js @@ -51,6 +51,8 @@ class EmailListView extends ListView { FOLDER_DRAFTS = 'drafts' /** @const */ FOLDER_TRASH = 'trash' + /** @const */ + FOLDER_ARCHIVE = 'archive' noDropFolderIdList = [ 'sent', @@ -227,7 +229,7 @@ class EmailListView extends ListView { return true; } - if (folderId === this.FOLDER_TRASH) { + if (folderId === this.FOLDER_TRASH || folderId === this.FOLDER_ARCHIVE) { return false; } @@ -321,6 +323,17 @@ class EmailListView extends ListView { this.getEmailRecordView().massActionMoveToTrash(); }; + this.shortcutKeys['Control+Backspace'] = e => { + if (!this.hasSelectedRecords()) { + return; + } + + e.preventDefault(); + e.stopPropagation(); + + this.getEmailRecordView().massActionMoveToArchive(); + }; + this.shortcutKeys['Control+KeyI'] = e => { if (!this.hasSelectedRecords()) { return; @@ -418,6 +431,7 @@ class EmailListView extends ListView { this.FOLDER_INBOX, this.FOLDER_IMPORTANT, this.FOLDER_SENT, + this.FOLDER_ARCHIVE, ]; this.getFolderCollection(collection => { @@ -462,7 +476,7 @@ class EmailListView extends ListView { .then(() => Espo.Ui.notify(false)); if (id !== this.defaultFolderId) { - this.getRouter().navigate('#Email/list/folder=' + id); + this.getRouter().navigate(`#Email/list/folder=${id}`); } else { this.getRouter().navigate('#Email'); } diff --git a/client/src/views/email/record/detail.js b/client/src/views/email/record/detail.js index 2d2c9d0e38..55c348ee54 100644 --- a/client/src/views/email/record/detail.js +++ b/client/src/views/email/record/detail.js @@ -156,6 +156,14 @@ class EmailDetailRecordView extends DetailRecordView { groupIndex: 2, }); + this.addDropdownItem({ + labelTranslation: 'Email.actions.moveToArchive', + name: 'moveToArchive', + groupIndex: 2, + hidden: this.model.get('inArchive'), + onClick: () => this.actionMoveToArchive(), + }); + this.addDropdownItem({ label: 'Move to Folder', name: 'moveToFolder', @@ -196,6 +204,14 @@ class EmailDetailRecordView extends DetailRecordView { } }); + this.listenTo(this.model, 'change:inArchive', () => { + if (this.model.get('inArchive')) { + this.hideActionItem('moveToArchive'); + } else { + this.showActionItem('moveToArchive'); + } + }); + this.handleTasksField(); this.listenTo(this.model, 'change:tasksIds', () => this.handleTasksField()); @@ -312,7 +328,9 @@ class EmailDetailRecordView extends DetailRecordView { Espo.Ajax.postRequest(`Email/inbox/folders/${folderId}`, {id: this.model.id}) .then(() => { - if (folderId === 'inbox') { + this.model.set('inArchive', folderId === 'archive'); + + if (folderId === 'inbox' || folderId === 'archive') { folderId = null; } @@ -324,6 +342,21 @@ class EmailDetailRecordView extends DetailRecordView { }); } + actionMoveToArchive() { + Espo.Ui.notify(' ... '); + + Espo.Ajax.postRequest(`Email/inbox/folders/archive`, {id: this.model.id}) + .then(() => { + this.model.set('inArchive', true); + + Espo.Ui.info(this.translate('Moved to Archive', 'labels', 'Email')); + + if (this.model.collection) { + this.model.collection.trigger('moving-to-archive', this.model.id); + } + }); + } + // noinspection JSUnusedGlobalSymbols actionShowBodyPlain() { this.createView('bodyPlain', 'views/email/modals/body-plain', { diff --git a/client/src/views/email/record/list.js b/client/src/views/email/record/list.js index 21be77b58b..0f095dcc8a 100644 --- a/client/src/views/email/record/list.js +++ b/client/src/views/email/record/list.js @@ -35,7 +35,10 @@ class EmailListRecordView extends ListRecordView { rowActionsView = 'views/email/record/row-actions/default' - massActionList = ['remove', 'massUpdate'] + massActionList = [ + 'remove', + 'massUpdate', + ] setup() { super.setup(); @@ -43,6 +46,7 @@ class EmailListRecordView extends ListRecordView { if (this.collection.url === this.entityType) { this.addMassAction('retrieveFromTrash', false, true); this.addMassAction('moveToFolder', true, true); + this.addMassAction('moveToArchive', true, true); this.addMassAction('markAsNotImportant', false, true); this.addMassAction('markAsImportant', false, true); this.addMassAction('markAsNotRead', false, true); @@ -78,6 +82,18 @@ class EmailListRecordView extends ListRecordView { this.removeRecordFromList(id); } }); + + this.listenTo(this.collection, 'moving-to-archive', id => { + const model = this.collection.get(id); + + if (model) { + model.set('inArchive', true); + } + + if (this.collection.selectedFolderId !== 'archive') { + this.removeRecordFromList(id); + } + }); } // noinspection JSUnusedGlobalSymbols @@ -225,22 +241,31 @@ class EmailListRecordView extends ListRecordView { Espo.Ui.notify(false); if (result.id) { - helper - .process(result.id, 'moveToFolder') + helper.process(result.id, 'moveToFolder') .then(view => { this.listenToOnce(view, 'close:success', () => { - this.collection.fetch().then(() => { - Espo.Ui.success(this.translate('Done')); - }); + this.collection.fetch() + .then(() => Espo.Ui.success(this.translate('Done'))); }); }); return; } - this.collection.fetch().then(() => { - Espo.Ui.success(this.translate('Done')); - }); + if (folderId === 'archive') { + [...this.checkedList].forEach(id => { + this.collection.trigger('moving-to-archive', id, this.collection.get(id)); + + this.uncheckRecord(id, null, true); + }); + + Espo.Ui.info(this.translate('Moved to Archive', 'labels', 'Email')); + + return; + } + + this.collection.fetch() + .then(() => Espo.Ui.success(this.translate('Done'))); }); } @@ -259,6 +284,11 @@ class EmailListRecordView extends ListRecordView { }); } + // noinspection JSUnusedGlobalSymbols + massActionMoveToArchive() { + this.massMoveToFolder('archive'); + } + actionMarkAsImportant(data) { data = data || {}; @@ -395,9 +425,16 @@ class EmailListRecordView extends ListRecordView { this.moveToFolder(id, folderId) .then(() => { - this.collection.fetch().then(() => { - Espo.Ui.success(this.translate('Done')); - }); + if (folderId === 'archive') { + this.collection.trigger('moving-to-archive', id, this.collection.get(id)); + + Espo.Ui.info(this.translate('Moved to Archive', 'labels', 'Email')); + + return; + } + + this.collection.fetch() + .then(() => Espo.Ui.success(this.translate('Done'))); }); return; @@ -423,6 +460,7 @@ class EmailListRecordView extends ListRecordView { }); } + // noinspection JSUnusedGlobalSymbols /** * @private * @param {{id: string}} data