From 0b7b9599d3a78ca75e2e425fff97f4221ef27adb Mon Sep 17 00:00:00 2001 From: yuri Date: Tue, 24 Apr 2018 16:18:50 +0300 Subject: [PATCH] multiple assigned users support --- application/Espo/Core/Acl/Base.php | 2 +- application/Espo/Core/ORM/Entity.php | 2 + application/Espo/Core/SelectManagers/Base.php | 24 +++--- application/Espo/Hooks/Common/Stream.php | 41 ++++++++++ .../Espo/Modules/Crm/Services/Activities.php | 11 ++- .../Resources/metadata/clientDefs/User.json | 4 +- application/Espo/Services/Email.php | 5 ++ application/Espo/Services/Record.php | 74 ++++++++++++++++++- .../crm/src/views/record/panels/tasks.js | 14 ++-- .../crm/src/views/user/record/panels/tasks.js | 64 ++++++++++++++++ .../views/admin/link-manager/modals/edit.js | 13 +++- client/src/views/email/record/detail-side.js | 2 - client/src/views/record/base.js | 22 ++++-- client/src/views/record/detail-side.js | 29 +++++++- client/src/views/record/edit-side.js | 3 +- .../src/views/record/panels/relationship.js | 2 +- 16 files changed, 272 insertions(+), 40 deletions(-) create mode 100644 client/modules/crm/src/views/user/record/panels/tasks.js diff --git a/application/Espo/Core/Acl/Base.php b/application/Espo/Core/Acl/Base.php index 369883117b..18c4561dd9 100644 --- a/application/Espo/Core/Acl/Base.php +++ b/application/Espo/Core/Acl/Base.php @@ -217,7 +217,7 @@ class Base implements Injectable } } - if ($entity->hasAttribute('assignedUsersIds') && $entity->hasRelation('assignedUsers')) { + if ($entity->hasLinkMultipleField('assignedUsers')) { if ($entity->hasLinkMultipleId('assignedUsers', $user->id)) { return true; } diff --git a/application/Espo/Core/ORM/Entity.php b/application/Espo/Core/ORM/Entity.php index 069a8e9d32..1844671956 100644 --- a/application/Espo/Core/ORM/Entity.php +++ b/application/Espo/Core/ORM/Entity.php @@ -143,6 +143,8 @@ class Entity extends \Espo\ORM\Entity } $this->set($idsAttribute, $ids); + $this->setFetched($idsAttribute, $ids); + $this->set($field . 'Names', $names); if ($hasType) { $this->set($field . 'Types', $types); diff --git a/application/Espo/Core/SelectManagers/Base.php b/application/Espo/Core/SelectManagers/Base.php index 77e25d5cf3..86f1f453be 100644 --- a/application/Espo/Core/SelectManagers/Base.php +++ b/application/Espo/Core/SelectManagers/Base.php @@ -484,9 +484,9 @@ class Base { if ($this->hasAssignedUsersField()) { $this->setDistinct(true, $result); - $this->addLeftJoin('assignedUsers', $result); + $this->addLeftJoin(['assignedUsers', 'assignedUsersAccess'], $result); $result['whereClause'][] = array( - 'assignedUsers.id' => $this->getUser()->id + 'assignedUsersAccess.id' => $this->getUser()->id ); return; } @@ -1558,19 +1558,25 @@ class Base protected function boolFilterOnlyMy(&$result) { if (!$this->checkIsPortal()) { - if ($this->hasAssignedUserField()) { - $result['whereClause'][] = array( + if ($this->hasAssignedUsersField()) { + $this->setDistinct(true, $result); + $this->addLeftJoin(['assignedUsers', 'assignedUsersAccess'], $result); + $result['whereClause'][] = [ + 'assignedUsersAccess.id' => $this->getUser()->id + ]; + } else if ($this->hasAssignedUserField()) { + $result['whereClause'][] = [ 'assignedUserId' => $this->getUser()->id - ); + ]; } else { - $result['whereClause'][] = array( + $result['whereClause'][] = [ 'createdById' => $this->getUser()->id - ); + ]; } } else { - $result['whereClause'][] = array( + $result['whereClause'][] = [ 'createdById' => $this->getUser()->id - ); + ]; } } diff --git a/application/Espo/Hooks/Common/Stream.php b/application/Espo/Hooks/Common/Stream.php index 1ba022e504..cb9cec8a28 100644 --- a/application/Espo/Hooks/Common/Stream.php +++ b/application/Espo/Hooks/Common/Stream.php @@ -183,12 +183,23 @@ class Stream extends \Espo\Core\Hooks\Base $entityType = $entity->getEntityType(); if ($this->checkHasStream($entity)) { + + $hasAssignedUsersField = false; + if ($entity->hasLinkMultipleField('assignedUsers')) { + $hasAssignedUsersField = true; + } + if ($entity->isNew()) { $userIdList = []; $assignedUserId = $entity->get('assignedUserId'); $createdById = $entity->get('createdById'); + $assignedUserIdList = []; + if ($hasAssignedUsersField) { + $assignedUserIdList = $entity->getLinkMultipleIdList('assignedUsers'); + } + if ( !$this->getUser()->isSystem() && @@ -210,6 +221,15 @@ class Stream extends \Espo\Core\Hooks\Base ) { $userIdList[] = $createdById; } + + if ($hasAssignedUsersField) { + foreach ($assignedUserIdList as $userId) { + if (!empty($userId) && !in_array($userId, $userIdList)) { + $userIdList[] = $userId; + } + } + } + if (!empty($assignedUserId) && !in_array($assignedUserId, $userIdList)) { $userIdList[] = $assignedUserId; } @@ -273,6 +293,27 @@ class Stream extends \Espo\Core\Hooks\Base $this->getStreamService()->noteStatus($entity, $field); } } + + $assignedUserIdList = []; + if ($hasAssignedUsersField) { + $assignedUserIdList = $entity->getLinkMultipleIdList('assignedUsers'); + } + + if ($hasAssignedUsersField) { + $fetchedAssignedUserIdList = $entity->getFetched('assignedUsersIds'); + if (!is_array($fetchedAssignedUserIdList)) { + $fetchedAssignedUserIdList = []; + } + foreach ($assignedUserIdList as $userId) { + if (in_array($userId, $fetchedAssignedUserIdList)) { + continue; + } + $this->getStreamService()->followEntity($entity, $userId); + if ($this->getUser()->id === $userId) { + $entity->set('isFollowed', true); + } + } + } } $methodName = 'isChangedWithAclAffect'; diff --git a/application/Espo/Modules/Crm/Services/Activities.php b/application/Espo/Modules/Crm/Services/Activities.php index 2780e85d17..4777aa8ae1 100644 --- a/application/Espo/Modules/Crm/Services/Activities.php +++ b/application/Espo/Modules/Crm/Services/Activities.php @@ -836,7 +836,6 @@ class Activities extends \Espo\Core\Services\Base 'createdAt' ], 'whereClause' => array( - 'assignedUserId' => $userId, array( 'OR' => array( array( @@ -858,6 +857,14 @@ class Activities extends \Espo\Core\Services\Base ) ); + if ($this->getMetadata()->get(['entityDefs', 'Task', 'fields', 'assignedUsers', 'type']) === 'linkMultiple') { + $selectManager->setDistinct(true, $selectParams); + $selectManager->addLeftJoin(['assignedUsers', 'assignedUsers'], $selectParams); + $selectParams['whereClause'][] = ['assignedUsers.id' => $userId]; + } else { + $selectParams['whereClause'][] = ['assignedUserId' => $userId]; + } + return $this->getEntityManager()->getQuery()->createSelectQuery('Task', $selectParams); } @@ -890,6 +897,8 @@ class Activities extends \Espo\Core\Services\Base } if ($seed->hasRelation('assignedUsers')) { + $selectManager->setDistinct(true, $selectParams); + $selectManager->addLeftJoin(['assignedUsers', 'assignedUsers'], $selectParams); $wherePart['assignedUsersMiddle.userId'] = $userId; } diff --git a/application/Espo/Resources/metadata/clientDefs/User.json b/application/Espo/Resources/metadata/clientDefs/User.json index 140c883a38..bd39eb3a36 100644 --- a/application/Espo/Resources/metadata/clientDefs/User.json +++ b/application/Espo/Resources/metadata/clientDefs/User.json @@ -76,7 +76,7 @@ { "name":"tasks", "label":"Tasks", - "view":"crm:views/record/panels/tasks", + "view":"crm:views/user/record/panels/tasks", "aclScope": "Task" } ], @@ -96,7 +96,7 @@ { "name":"tasks", "label":"Tasks", - "view":"crm:views/record/panels/tasks", + "view":"crm:views/user/record/panels/tasks", "aclScope": "Task" } ] diff --git a/application/Espo/Services/Email.php b/application/Espo/Services/Email.php index 4c72e8b50a..2fab648380 100644 --- a/application/Espo/Services/Email.php +++ b/application/Espo/Services/Email.php @@ -813,4 +813,9 @@ class Email extends Record return $data; } + + public function isPermittedAssignedUsers(Entity $entity) + { + return true; + } } diff --git a/application/Espo/Services/Record.php b/application/Espo/Services/Record.php index 481812951f..2128afc521 100644 --- a/application/Espo/Services/Record.php +++ b/application/Espo/Services/Record.php @@ -403,6 +403,66 @@ class Record extends \Espo\Core\Services\Base if (!$this->isPermittedTeams($entity)) { return false; } + if ($entity->hasLinkMultipleField('assignedUsers')) { + if (!$this->isPermittedAssignedUsers($entity)) { + return false; + } + } + return true; + } + + public function isPermittedAssignedUsers(Entity $entity) + { + if (!$entity->hasLinkMultipleField('assignedUsers')) { + return true; + } + + $assignmentPermission = $this->getAcl()->get('assignmentPermission'); + + if ($assignmentPermission === true || $assignmentPermission === 'yes' || !in_array($assignmentPermission, ['team', 'no'])) { + return true; + } + + $toProcess = false; + + if (!$entity->isNew()) { + $userIdList = $entity->getLinkMultipleIdList('assignedUsers'); + if ($entity->isAttributeChanged('assignedUsersIds')) { + $toProcess = true; + } + } else { + $toProcess = true; + } + + $userIdList = $entity->getLinkMultipleIdList('assignedUsers'); + + if ($toProcess) { + if (empty($userIdList)) { + if ($assignmentPermission == 'no') { + return false; + } + return true; + } + $fetchedAssignedUserIdList = $entity->getFetched('assignedUsersIds'); + + if ($assignmentPermission == 'no') { + foreach ($userIdList as $userId) { + if (!$entity->isNew() && in_array($userId, $fetchedAssignedUserIdList)) continue; + if ($this->getUser()->id != $userId) { + return false; + } + } + } else if ($assignmentPermission == 'team') { + $teamIdList = $this->getUser()->getLinkMultipleIdList('teams'); + foreach ($userIdList as $userId) { + if (!$entity->isNew() && in_array($userId, $fetchedAssignedUserIdList)) continue; + if (!$this->getEntityManager()->getRepository('User')->checkBelongsToAnyOfTeams($userId, $teamIdList)) { + return false; + } + } + } + } + return true; } @@ -466,8 +526,15 @@ class Record extends \Espo\Core\Services\Base $teamIdList = $entity->getLinkMultipleIdList('teams'); if (empty($teamIdList)) { if ($assignmentPermission === 'team') { - if (!$entity->get('assignedUserId')) { - return false; + if ($entity->hasLinkMultipleField('assignedUsers')) { + $assignedUserIdList = $entity->getLinkMultipleIdList('assignedUsers'); + if (empty($assignedUserIdList)) { + return false; + } + } else if ($entity->hasAttribute('assignedUserId')) { + if (!$entity->get('assignedUserId')) { + return false; + } } } return true; @@ -993,6 +1060,9 @@ class Record extends \Espo\Core\Services\Base if (!$this->getAcl()->check($entity, 'read')) { throw new Forbidden(); } + if (empty($link)) { + throw new Error(); + } $methodName = 'findLinkedEntities' . ucfirst($link); if (method_exists($this, $methodName)) { diff --git a/client/modules/crm/src/views/record/panels/tasks.js b/client/modules/crm/src/views/record/panels/tasks.js index f59a616d8a..58b37c7343 100644 --- a/client/modules/crm/src/views/record/panels/tasks.js +++ b/client/modules/crm/src/views/record/panels/tasks.js @@ -111,6 +111,12 @@ Espo.define('crm:views/record/panels/tasks', 'views/record/panels/relationship', setup: function () { this.scope = this.model.name; + this.link = 'tasks'; + + if (this.scope == 'Account') { + this.link = 'tasksPrimary'; + } + this.currentTab = this.getStorage().get('state', this.getStorageKey()) || this.defaultTab; this.where = [ @@ -122,12 +128,8 @@ Espo.define('crm:views/record/panels/tasks', 'views/record/panels/relationship', }, afterRender: function () { - var link = 'tasks'; - if (this.scope == 'Account') { - link = 'tasksPrimary'; - } - var url = this.model.name + '/' + this.model.id + '/' + link; + var url = this.model.name + '/' + this.model.id + '/' + this.link; if (!this.getAcl().check('Task', 'read')) { this.$el.find('.list-container').html(this.translate('No Access')); @@ -165,7 +167,7 @@ Espo.define('crm:views/record/panels/tasks', 'views/record/panels/relationship', actionCreateTask: function (data) { var self = this; - var link = 'tasks'; + var link = this.link; var scope = 'Task'; var foreignLink = this.model.defs['links'][link].foreign; diff --git a/client/modules/crm/src/views/user/record/panels/tasks.js b/client/modules/crm/src/views/user/record/panels/tasks.js new file mode 100644 index 0000000000..b125f9ea47 --- /dev/null +++ b/client/modules/crm/src/views/user/record/panels/tasks.js @@ -0,0 +1,64 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko + * Website: http://www.espocrm.com + * + * EspoCRM is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EspoCRM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EspoCRM. If not, see http://www.gnu.org/licenses/. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU General Public License version 3. + * + * In accordance with Section 7(b) of the GNU General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +Espo.define('crm:views/user/record/panels/tasks', 'crm:views/record/panels/tasks', function (Dep) { + + return Dep.extend({ + + listLayout: { + rows: [ + [ + { + name: 'name', + link: true, + }, + { + name: 'isOverdue' + } + ], + [ + {name: 'status'}, + {name: 'dateEnd'} + ] + ] + }, + + setup: function () { + Dep.prototype.setup.call(this); + + if (this.getMetadata().get(['entityDefs', 'Task', 'fields', 'assignedUsers'])) { + var foreignLink = this.getMetadata().get(['entityDefs', 'Task', 'links', 'assignedUsers', 'foreign']); + if (foreignLink) { + this.link = foreignLink; + } + } + } + + }); + +}); diff --git a/client/src/views/admin/link-manager/modals/edit.js b/client/src/views/admin/link-manager/modals/edit.js index fd49a21216..2f87b6acce 100644 --- a/client/src/views/admin/link-manager/modals/edit.js +++ b/client/src/views/admin/link-manager/modals/edit.js @@ -539,10 +539,15 @@ Espo.define('views/admin/link-manager/modals/edit', ['views/modal', 'views/admin url: url, type: 'POST', data: JSON.stringify(attributes), - error: function (x) { - if (x.status == 409) { - Espo.Ui.error(this.translate('linkConflict', 'messages', 'EntityManager')); - x.errorIsHandled = true; + error: function (xhr) { + if (xhr.status == 409) { + var msg = this.translate('linkConflict', 'messages', 'EntityManager'); + var statusReasonHeader = xhr.getResponseHeader('X-Status-Reason'); + if (statusReasonHeader) { + console.error(statusReasonHeader); + } + Espo.Ui.error(msg); + xhr.errorIsHandled = true; } this.$el.find('button[data-name="save"]').removeClass('disabled').removeAttr('disabled'); }.bind(this) diff --git a/client/src/views/email/record/detail-side.js b/client/src/views/email/record/detail-side.js index 57c10daab2..43d78a1bd9 100644 --- a/client/src/views/email/record/detail-side.js +++ b/client/src/views/email/record/detail-side.js @@ -48,6 +48,4 @@ Espo.define('views/email/record/detail-side', 'views/record/detail-side', functi } }); - }); - diff --git a/client/src/views/record/base.js b/client/src/views/record/base.js index 5e2ea69d00..a2daa63c81 100644 --- a/client/src/views/record/base.js +++ b/client/src/views/record/base.js @@ -548,23 +548,33 @@ Espo.define('views/record/base', ['view', 'view-record-helper', 'dynamic-logic'] var defaultHash = {}; if (!this.getUser().get('portalId')) { - if (this.model.hasField('assignedUser')) { + if (this.model.hasField('assignedUser') || this.model.hasField('assignedUsers')) { + var assignedUserField = 'assignedUser'; + if (this.model.hasField('assignedUsers')) { + assignedUserField = 'assignedUsers'; + } var fillAssignedUser = true; if (this.getPreferences().get('doNotFillAssignedUserIfNotRequired')) { fillAssignedUser = false; - if (this.model.getFieldParam('assignedUser', 'required')) { + if (this.model.getFieldParam(assignedUserField, 'required')) { fillAssignedUser = true; } else if (this.getAcl().get('assignmentPermission') === 'no') { fillAssignedUser = true; } else if (this.getAcl().get('assignmentPermission') === 'team' && !this.getUser().get('defaultTeamId')) { fillAssignedUser = true; - } else if (~this.getAcl().getScopeForbiddenFieldList(this.model.name, 'edit').indexOf('assignedUser')) { + } else if (~this.getAcl().getScopeForbiddenFieldList(this.model.name, 'edit').indexOf(assignedUserField)) { fillAssignedUser = true; } } if (fillAssignedUser) { - defaultHash['assignedUserId'] = this.getUser().id; - defaultHash['assignedUserName'] = this.getUser().get('name'); + if (assignedUserField === 'assignedUsers') { + defaultHash['assignedUsersIds'] = [this.getUser().id]; + defaultHash['assignedUsersNames'] = {}; + defaultHash['assignedUsersNames'][this.getUser().id] = this.getUser().get('name'); + } else { + defaultHash['assignedUserId'] = this.getUser().id; + defaultHash['assignedUserName'] = this.getUser().get('name'); + } } } var defaultTeamId = this.getUser().get('defaultTeamId'); @@ -572,7 +582,7 @@ Espo.define('views/record/base', ['view', 'view-record-helper', 'dynamic-logic'] if (this.model.hasField('teams') && !this.model.getFieldParam('teams', 'default')) { defaultHash['teamsIds'] = [defaultTeamId]; defaultHash['teamsNames'] = {}; - defaultHash['teamsNames'][defaultTeamId] = this.getUser().get('defaultTeamName') + defaultHash['teamsNames'][defaultTeamId] = this.getUser().get('defaultTeamName'); } } } diff --git a/client/src/views/record/detail-side.js b/client/src/views/record/detail-side.js index c7f4123e7a..d5c79de262 100644 --- a/client/src/views/record/detail-side.js +++ b/client/src/views/record/detail-side.js @@ -50,12 +50,10 @@ Espo.define('views/record/detail-side', ['view'], function (Dep) { options: { fieldList: [ { - name: 'assignedUser', - view: 'views/fields/assigned-user' + name: ':assignedUser' }, { - name: 'teams', - view: 'views/fields/teams' + name: 'teams' } ] } @@ -210,6 +208,29 @@ Espo.define('views/record/detail-side', ['view'], function (Dep) { defaultPanelDefs.options.fieldList = fieldList; } + if (defaultPanelDefs.options.fieldList && defaultPanelDefs.options.fieldList.length) { + defaultPanelDefs.options.fieldList.forEach(function (item, i) { + if (typeof item !== 'object') { + item = { + name: item + } + defaultPanelDefs.options.fieldList[i] = item; + } + if (item.name === ':assignedUser') { + if (this.model.hasField('assignedUsers')) { + item.name = 'assignedUsers'; + if (!this.model.getFieldParam('assignedUsers', 'view')) { + item.view = 'views/fields/assigned-users'; + } + } else if (this.model.hasField('assignedUser')) { + item.name = 'assignedUser'; + } else { + defaultPanelDefs.options.fieldList[i] = {}; + } + } + }, this); + } + this.panelList.unshift(defaultPanelDefs); }, diff --git a/client/src/views/record/edit-side.js b/client/src/views/record/edit-side.js index 6461eb4232..2b898e41be 100644 --- a/client/src/views/record/edit-side.js +++ b/client/src/views/record/edit-side.js @@ -40,8 +40,7 @@ Espo.define('views/record/edit-side', 'views/record/detail-side', function (Dep) options: { fieldList: [ { - name: 'assignedUser', - view: 'views/fields/assigned-user' + name: ':assignedUser' }, { name: 'teams', diff --git a/client/src/views/record/panels/relationship.js b/client/src/views/record/panels/relationship.js index 1f5b866abb..55a358ca1e 100644 --- a/client/src/views/record/panels/relationship.js +++ b/client/src/views/record/panels/relationship.js @@ -49,7 +49,7 @@ Espo.define('views/record/panels/relationship', ['views/record/panels/bottom', ' setup: function () { Dep.prototype.setup.call(this); - this.link = this.defs.link || this.panelName; + this.link = this.link || this.defs.link || this.panelName; if (!this.scope && !(this.link in this.model.defs.links)) { throw new Error('Link \'' + this.link + '\' is not defined in model \'' + this.model.name + '\'');