emails archive

This commit is contained in:
Yuri Kuznetsov
2024-06-24 12:44:35 +03:00
parent eee086ef52
commit 6e6d3e15ea
25 changed files with 681 additions and 126 deletions
@@ -0,0 +1,66 @@
<?php
/************************************************************************
* 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 <https://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 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<Email>
*/
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());
}
}
@@ -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(),
]);
}
}
@@ -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([
@@ -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,
];
@@ -0,0 +1,54 @@
<?php
/************************************************************************
* 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 <https://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 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,
]);
}
}
@@ -0,0 +1,54 @@
<?php
/************************************************************************
* 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 <https://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 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,
]);
}
}
@@ -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,
]);
+1
View File
@@ -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';
+8 -1
View File
@@ -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') ?? '';
}
}
@@ -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": {
@@ -5,6 +5,9 @@
{
"name": "status"
},
{
"name": "folderString"
},
{
"name": "replied"
},
@@ -144,6 +144,20 @@
}
]
}
},
"folderString": {
"visible": {
"conditionGroup": [
{
"type": "isTrue",
"attribute": "isUsers"
},
{
"type": "isFalse",
"attribute": "isUsersSent"
}
]
}
}
},
"panels": {
@@ -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
@@ -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": [
@@ -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"
},
+1
View File
@@ -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';
}
+77 -15
View File
@@ -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.");
}
}
}
@@ -55,6 +55,7 @@ class Service
/** @var string[] */
protected $systemFolderEndList = [
Folder::DRAFTS,
Folder::ARCHIVE,
Folder::TRASH,
];
@@ -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'),
});
})
);
},
+34 -3
View File
@@ -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,
$('<span>')
.addClass('font-size-flexible title')
.addClass(isImportant ? 'text-warning' : '')
.addClass(inTrash ? 'text-muted' : '')
.addClass(styleClass)
.text(name),
]);
}
@@ -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 <https://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 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}}
<span class="none-value">{{translate 'None'}}</span>
{{/if}}
{{else}}
<span class="loading-value"></span>
{{/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;
+62 -63
View File
@@ -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;
+16 -2
View File
@@ -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');
}
+34 -1
View File
@@ -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', {
+50 -12
View File
@@ -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