diff --git a/application/Espo/Core/Select/Helpers/RelationQueryHelper.php b/application/Espo/Core/Select/Helpers/RelationQueryHelper.php index bfb464249a..91eff9b031 100644 --- a/application/Espo/Core/Select/Helpers/RelationQueryHelper.php +++ b/application/Espo/Core/Select/Helpers/RelationQueryHelper.php @@ -29,6 +29,7 @@ namespace Espo\Core\Select\Helpers; +use Espo\Entities\User; use Espo\ORM\Defs; use Espo\ORM\Query\Part\Condition; use Espo\ORM\Query\Part\Expression; @@ -44,27 +45,30 @@ class RelationQueryHelper private Defs $defs, ) {} - /** - * @param string|string[] $id - */ - public function prepareMiddleWhere(string $entityType, string $link, string|array $id): WhereItem + public function prepareAssignedUsersWhere(string $entityType, string $userId): WhereItem { $relationDefs = $this->defs ->getEntity($entityType) - ->getRelation($link); + ->getRelation('assignedUsers'); $middleEntityType = ucfirst($relationDefs->getRelationshipName()); $key1 = $relationDefs->getMidKey(); $key2 = $relationDefs->getForeignMidKey(); + $joinWhere = [ + "m.$key1:" => 'id', + 'm.deleted' => false, + ]; + + if ($middleEntityType === User::RELATIONSHIP_ENTITY_USER) { + $joinWhere['m.entityType'] = $entityType; + } + $subQuery = QueryBuilder::create() ->select('id') ->from($entityType) - ->leftJoin($middleEntityType, 'm', [ - "m.$key1:" => 'id', - 'm.deleted' => false, - ]) - ->where(["m.$key2" => $id]) + ->leftJoin($middleEntityType, 'm', $joinWhere) + ->where(["m.$key2" => $userId]) ->build(); return Condition::in( @@ -72,9 +76,4 @@ class RelationQueryHelper $subQuery ); } - - public function prepareAssignedUsersWhere(string $entityType, string $userId): WhereItem - { - return $this->prepareMiddleWhere($entityType, 'assignedUsers', $userId); - } } diff --git a/application/Espo/Entities/User.php b/application/Espo/Entities/User.php index d418b13fb8..48e27c242a 100644 --- a/application/Espo/Entities/User.php +++ b/application/Espo/Entities/User.php @@ -58,6 +58,8 @@ class User extends Person public const TYPE_API = 'api'; public const TYPE_SUPER_ADMIN = 'super-admin'; + public const RELATIONSHIP_ENTITY_USER = 'EntityUser'; + public function isActive(): bool { return (bool) $this->get('isActive'); diff --git a/application/Espo/Modules/Crm/Resources/metadata/app/entityManagerParams.json b/application/Espo/Modules/Crm/Resources/metadata/app/entityManagerParams.json index eae6ca2eb3..57e0e6f96f 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/app/entityManagerParams.json +++ b/application/Espo/Modules/Crm/Resources/metadata/app/entityManagerParams.json @@ -14,6 +14,13 @@ "tooltip": true, "view": "views/admin/entity-manager/fields/duplicate-check-field-list" } + }, + "assignedUsers": { + "location": "scopes", + "fieldDefs": { + "type": "bool", + "tooltip": true + } } }, "Contact": { @@ -31,6 +38,13 @@ "tooltip": true, "view": "views/admin/entity-manager/fields/duplicate-check-field-list" } + }, + "assignedUsers": { + "location": "scopes", + "fieldDefs": { + "type": "bool", + "tooltip": true + } } }, "Lead": { @@ -48,6 +62,49 @@ "tooltip": true, "view": "views/admin/entity-manager/fields/duplicate-check-field-list" } + }, + "assignedUsers": { + "location": "scopes", + "fieldDefs": { + "type": "bool", + "tooltip": true + } + } + }, + "Opportunity": { + "assignedUsers": { + "location": "scopes", + "fieldDefs": { + "type": "bool", + "tooltip": true + } + } + }, + "Document": { + "assignedUsers": { + "location": "scopes", + "fieldDefs": { + "type": "bool", + "tooltip": true + } + } + }, + "Case": { + "assignedUsers": { + "location": "scopes", + "fieldDefs": { + "type": "bool", + "tooltip": true + } + } + }, + "KnowledgeBaseArticle": { + "assignedUsers": { + "location": "scopes", + "fieldDefs": { + "type": "bool", + "tooltip": true + } } }, "Meeting": { @@ -144,6 +201,13 @@ "tooltip": true, "view": "crm:views/admin/entity-manager/fields/status-list" } + }, + "assignedUsers": { + "location": "scopes", + "fieldDefs": { + "type": "bool", + "tooltip": true + } } }, "@Event": { @@ -182,6 +246,13 @@ "tooltip": true, "view": "crm:views/admin/entity-manager/fields/status-list" } + }, + "assignedUsers": { + "location": "scopes", + "fieldDefs": { + "type": "bool", + "tooltip": true + } } } } diff --git a/application/Espo/Resources/i18n/en_US/EntityManager.json b/application/Espo/Resources/i18n/en_US/EntityManager.json index 4e886a00b8..24ac3c495d 100644 --- a/application/Espo/Resources/i18n/en_US/EntityManager.json +++ b/application/Espo/Resources/i18n/en_US/EntityManager.json @@ -49,7 +49,8 @@ "author": "Author", "module": "Module", "version": "Version", - "primaryFilters": "Primary Filters" + "primaryFilters": "Primary Filters", + "assignedUsers": "Multiple Assigned Users" }, "options": { "type": { @@ -92,6 +93,7 @@ "beforeSaveApiScript": "A script called on create and update API requests before an entity is saved. Use for custom validation and duplicate checking." }, "tooltips": { + "assignedUsers": "The ability to assign multiple users to a record.\n\nNote that after enabling the parameter, existing assigned users won't be transferred to the new *Assigned Users* field.", "duplicateCheckFieldList": "Which fields to check when performing checking for duplicates.", "updateDuplicateCheck": "Perform checking for duplicates when updating a record.", "optimisticConcurrencyControl": "Prevents writing conflicts.", diff --git a/application/Espo/Resources/i18n/en_US/Global.json b/application/Espo/Resources/i18n/en_US/Global.json index 655c72018a..a9814af00a 100644 --- a/application/Espo/Resources/i18n/en_US/Global.json +++ b/application/Espo/Resources/i18n/en_US/Global.json @@ -489,6 +489,7 @@ }, "links": { "assignedUser": "Assigned User", + "assignedUsers": "Assigned Users", "createdBy": "Created By", "modifiedBy": "Modified By", "team": "Team", diff --git a/application/Espo/Resources/metadata/app/entityManager.json b/application/Espo/Resources/metadata/app/entityManager.json index 293a277d17..88772add4e 100644 --- a/application/Espo/Resources/metadata/app/entityManager.json +++ b/application/Espo/Resources/metadata/app/entityManager.json @@ -8,5 +8,7 @@ "Espo\\Tools\\EntityManager\\Hook\\Hooks\\EventDeleteHook", "Espo\\Tools\\EntityManager\\Hook\\Hooks\\DeleteHasChildrenLinks" ], - "updateHookClassNameList": [] + "updateHookClassNameList": [ + "Espo\\Tools\\EntityManager\\Hook\\Hooks\\AssignedUsersUpdateHook" + ] } diff --git a/application/Espo/Resources/metadata/app/entityManagerParams.json b/application/Espo/Resources/metadata/app/entityManagerParams.json index 3ae86b6041..3b4b3de9e1 100644 --- a/application/Espo/Resources/metadata/app/entityManagerParams.json +++ b/application/Espo/Resources/metadata/app/entityManagerParams.json @@ -30,6 +30,13 @@ "tooltip": true, "view": "views/admin/entity-manager/fields/duplicate-check-field-list" } + }, + "assignedUsers": { + "location": "scopes", + "fieldDefs": { + "type": "bool", + "tooltip": true + } } }, "@Person": { @@ -47,6 +54,13 @@ "tooltip": true, "view": "views/admin/entity-manager/fields/duplicate-check-field-list" } + }, + "assignedUsers": { + "location": "scopes", + "fieldDefs": { + "type": "bool", + "tooltip": true + } } }, "@Base": { @@ -64,6 +78,13 @@ "tooltip": true, "view": "views/admin/entity-manager/fields/duplicate-check-field-list" } + }, + "assignedUsers": { + "location": "scopes", + "fieldDefs": { + "type": "bool", + "tooltip": true + } } }, "@BasePlus": { @@ -81,6 +102,13 @@ "tooltip": true, "view": "views/admin/entity-manager/fields/duplicate-check-field-list" } + }, + "assignedUsers": { + "location": "scopes", + "fieldDefs": { + "type": "bool", + "tooltip": true + } } } } diff --git a/application/Espo/Tools/EntityManager/EntityManager.php b/application/Espo/Tools/EntityManager/EntityManager.php index 033c92a32e..2d3eeb785a 100644 --- a/application/Espo/Tools/EntityManager/EntityManager.php +++ b/application/Espo/Tools/EntityManager/EntityManager.php @@ -537,6 +537,7 @@ class EntityManager $this->metadata->delete('clientDefs', $name); $this->metadata->delete('recordDefs', $name); $this->metadata->delete('selectDefs', $name); + $this->metadata->delete('entityAcl', $name); $this->metadata->delete('scopes', $name); foreach ($this->metadata->get(['entityDefs', $name, 'links'], []) as $link => $item) { diff --git a/application/Espo/Tools/EntityManager/Hook/Hooks/AssignedUsersUpdateHook.php b/application/Espo/Tools/EntityManager/Hook/Hooks/AssignedUsersUpdateHook.php new file mode 100644 index 0000000000..c5716f2331 --- /dev/null +++ b/application/Espo/Tools/EntityManager/Hook/Hooks/AssignedUsersUpdateHook.php @@ -0,0 +1,122 @@ +. + * + * 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\Tools\EntityManager\Hook\Hooks; + +use Espo\Core\ORM\Type\FieldType; +use Espo\Core\Utils\Metadata; +use Espo\Entities\User; +use Espo\ORM\Type\RelationType; +use Espo\Tools\EntityManager\Hook\UpdateHook; +use Espo\Tools\EntityManager\Params; + +/** + * @noinspection PhpUnused + */ +class AssignedUsersUpdateHook implements UpdateHook +{ + private const PARAM = 'assignedUsers'; + private const FIELD = 'assignedUsers'; + private const RELATION_NAME = 'entityUser'; + private const FIELD_ASSIGNED_USER = 'assignedUser'; + + public function __construct( + private Metadata $metadata, + ) {} + + public function process(Params $params, Params $previousParams): void + { + if ($params->get(self::PARAM) && !$previousParams->get(self::PARAM)) { + $this->add($params->getName()); + } else if (!$params->get(self::PARAM) && $previousParams->get(self::PARAM)) { + $this->remove($params->getName()); + } + } + + private function add(string $entityType): void + { + $this->metadata->set('entityDefs', $entityType, [ + 'fields' => [ + self::FIELD => [ + 'type' => FieldType::LINK_MULTIPLE, + 'view' => 'views/fields/assigned-users', + ], + ], + 'links' => [ + self::FIELD => [ + 'type' => RelationType::HAS_MANY, + 'entity' => User::ENTITY_TYPE, + 'relationName' => self::RELATION_NAME, + 'layoutRelationshipsDisabled' => true, + ], + ], + ]); + + $this->metadata->set('entityAcl', $entityType, [ + 'links' => [ + self::FIELD => [ + 'readOnly' => true, + ], + ], + ]); + + if ($this->metadata->get("entityDefs.$entityType.'fields.assignedUser")) { + $this->metadata->set('entityDefs', $entityType, [ + 'fields' => [ + self::FIELD_ASSIGNED_USER => [ + 'disabled' => true, + ], + ], + 'links' => [ + self::FIELD_ASSIGNED_USER => [ + 'disabled' => true, + ], + ], + ]); + } + + $this->metadata->save(); + } + + private function remove(string $entityType): void + { + $this->metadata->delete('entityDefs', $entityType, [ + 'fields.' . self::FIELD, + 'links.' . self::FIELD, + 'fields.' . self::FIELD_ASSIGNED_USER . '.disabled', + 'links.' . self::FIELD_ASSIGNED_USER . '.disabled', + ]); + + $this->metadata->delete('entityAcl', $entityType, [ + 'links.' . self::FIELD, + ]); + + $this->metadata->save(); + } +} diff --git a/application/Espo/Tools/EntityManager/NameUtil.php b/application/Espo/Tools/EntityManager/NameUtil.php index 16189a6c3b..b68b6aae5e 100644 --- a/application/Espo/Tools/EntityManager/NameUtil.php +++ b/application/Espo/Tools/EntityManager/NameUtil.php @@ -73,6 +73,9 @@ class NameUtil 'true', 'layout', 'system', + 'teams', + 'assignedUser', + 'assignedUsers', ]; /** diff --git a/application/Espo/Tools/FieldManager/FieldManager.php b/application/Espo/Tools/FieldManager/FieldManager.php index 69c480f2cd..ce7123770b 100644 --- a/application/Espo/Tools/FieldManager/FieldManager.php +++ b/application/Espo/Tools/FieldManager/FieldManager.php @@ -63,6 +63,9 @@ class FieldManager 'null', 'false', 'true', + 'teams', + 'assignedUser', + 'assignedUsers', ]; /** @var string[] */ diff --git a/application/Espo/Tools/LinkManager/Hook/Hooks/AssignedUsersCreate.php b/application/Espo/Tools/LinkManager/Hook/Hooks/AssignedUsersCreate.php deleted file mode 100644 index 4156731b0b..0000000000 --- a/application/Espo/Tools/LinkManager/Hook/Hooks/AssignedUsersCreate.php +++ /dev/null @@ -1,93 +0,0 @@ -. - * - * 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\Tools\LinkManager\Hook\Hooks; - -use Espo\Tools\LinkManager\Hook\CreateHook; -use Espo\Tools\LinkManager\Params; -use Espo\Tools\LinkManager\Type; -use Espo\Core\Utils\Metadata; -use Espo\Entities\User; - -class AssignedUsersCreate implements CreateHook -{ - private const LINK_NAME = 'assignedUsers'; - - public function __construct(private Metadata $metadata) - {} - - public function process(Params $params): void - { - if ($params->getType() !== Type::MANY_TO_MANY) { - return; - } - - $foreignEntityType = $params->getForeignEntityType(); - $entityType = $params->getEntityType(); - - if (!$foreignEntityType || !$entityType) { - return; - } - - if ( - $params->getEntityType() === User::ENTITY_TYPE && - $params->getForeignLink() === self::LINK_NAME - ) { - $this->processInternal($foreignEntityType); - - return; - } - - if ( - $params->getForeignEntityType() === User::ENTITY_TYPE && - $params->getLink() === self::LINK_NAME - ) { - $this->processInternal($entityType); - } - } - - private function processInternal(string $entityType): void - { - $fieldType = $this->metadata->get(['entityDefs', $entityType, 'fields', self::LINK_NAME, 'type']); - - if ($fieldType !== 'linkMultiple') { - return; - } - - $this->metadata->set('entityDefs', $entityType, [ - 'fields' => [ - self::LINK_NAME => [ - 'view' => 'views/fields/assigned-users', - ], - ] - ]); - - $this->metadata->save(); - } -}