From 39b461a618c3699f375af014c4558ead45cb5447 Mon Sep 17 00:00:00 2001 From: Yurii Date: Sat, 21 Feb 2026 16:50:52 +0200 Subject: [PATCH] enable collaborators for tasks and cases --- .../Metadata/AdditionalBuilder/Fields.php | 5 ++ .../layouts/Case/defaultSidePanel.json | 3 + .../Resources/metadata/entityDefs/Case.json | 21 +++++ .../Resources/metadata/entityDefs/Task.json | 21 +++++ .../Crm/Resources/metadata/scopes/Case.json | 3 +- .../Crm/Resources/metadata/scopes/Task.json | 3 +- .../Espo/ORM/Defs/Params/RelationParam.php | 7 ++ .../Hook/Hooks/CollaboratorsUpdateHook.php | 90 +++++++++++++++---- 8 files changed, 132 insertions(+), 21 deletions(-) diff --git a/application/Espo/Core/Utils/Metadata/AdditionalBuilder/Fields.php b/application/Espo/Core/Utils/Metadata/AdditionalBuilder/Fields.php index 84f8e09e96..e78b012184 100644 --- a/application/Espo/Core/Utils/Metadata/AdditionalBuilder/Fields.php +++ b/application/Espo/Core/Utils/Metadata/AdditionalBuilder/Fields.php @@ -33,6 +33,7 @@ use Espo\Core\Utils\DataUtil; use Espo\Core\Utils\Metadata\AdditionalBuilder; use Espo\Core\Utils\Metadata\BuilderHelper; use Espo\Core\Utils\Util; +use RuntimeException; use stdClass; class Fields implements AdditionalBuilder @@ -75,6 +76,10 @@ class Fields implements AdditionalBuilder } foreach (get_object_vars($entityDefsItem->fields) as $field => $fieldDefsItem) { + if (!is_object($fieldDefsItem)) { + throw new RuntimeException("Bad definition for $entityType.$field field."); + } + $additionalFields = $this->builderHelper->getAdditionalFields( field: $field, params: Util::objectToArray($fieldDefsItem), diff --git a/application/Espo/Modules/Crm/Resources/layouts/Case/defaultSidePanel.json b/application/Espo/Modules/Crm/Resources/layouts/Case/defaultSidePanel.json index 92a421f1fa..26613d29b6 100644 --- a/application/Espo/Modules/Crm/Resources/layouts/Case/defaultSidePanel.json +++ b/application/Espo/Modules/Crm/Resources/layouts/Case/defaultSidePanel.json @@ -5,6 +5,9 @@ { "name": "teams" }, + { + "name": "collaborators" + }, { "name": "isInternal" } diff --git a/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Case.json b/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Case.json index 7f3d8419ff..0d965785c8 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Case.json +++ b/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Case.json @@ -122,6 +122,21 @@ "type": "linkMultiple", "view": "views/fields/teams" }, + "collaborators": { + "type": "linkMultiple", + "view": "views/fields/collaborators", + "maxCount": 30, + "fieldManagerParamList": [ + "readOnly", + "readOnlyAfterCreate", + "audited", + "autocompleteOnEmpty", + "maxCount", + "inlineEditDisabled", + "tooltipText" + ], + "dynamicLogicVisibleDisabled": true + }, "attachments": { "type": "attachmentMultiple" } @@ -145,6 +160,12 @@ "relationName": "entityTeam", "layoutRelationshipsDisabled": true }, + "collaborators": { + "type": "hasMany", + "entity": "User", + "relationName": "entityCollaborator", + "layoutRelationshipsDisabled": true + }, "inboundEmail": { "type": "belongsTo", "entity": "InboundEmail" diff --git a/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Task.json b/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Task.json index e618cf98cd..d4c993ccf1 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Task.json +++ b/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Task.json @@ -155,6 +155,21 @@ "type": "linkMultiple", "view": "views/fields/teams" }, + "collaborators": { + "type": "linkMultiple", + "view": "views/fields/collaborators", + "maxCount": 30, + "fieldManagerParamList": [ + "readOnly", + "readOnlyAfterCreate", + "audited", + "autocompleteOnEmpty", + "maxCount", + "inlineEditDisabled", + "tooltipText" + ], + "dynamicLogicVisibleDisabled": true + }, "attachments": { "type": "attachmentMultiple", "sourceList": ["Document"] @@ -180,6 +195,12 @@ "relationName": "entityTeam", "layoutRelationshipsDisabled": true }, + "collaborators": { + "type": "hasMany", + "entity": "User", + "relationName": "entityCollaborator", + "layoutRelationshipsDisabled": true + }, "parent": { "type": "belongsToParent", "foreign": "tasks" diff --git a/application/Espo/Modules/Crm/Resources/metadata/scopes/Case.json b/application/Espo/Modules/Crm/Resources/metadata/scopes/Case.json index 22c6e513a4..7e0399c996 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/scopes/Case.json +++ b/application/Espo/Modules/Crm/Resources/metadata/scopes/Case.json @@ -17,5 +17,6 @@ "importable": true, "notifications": true, "object": true, - "statusField": "status" + "statusField": "status", + "collaborators": true } diff --git a/application/Espo/Modules/Crm/Resources/metadata/scopes/Task.json b/application/Espo/Modules/Crm/Resources/metadata/scopes/Task.json index f115041e43..721daf6875 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/scopes/Task.json +++ b/application/Espo/Modules/Crm/Resources/metadata/scopes/Task.json @@ -19,5 +19,6 @@ "statusField": "status", "stream": true, "kanbanStatusIgnoreList": ["Canceled", "Deferred"], - "statusFieldLocked": true + "statusFieldLocked": true, + "collaborators": true } diff --git a/application/Espo/ORM/Defs/Params/RelationParam.php b/application/Espo/ORM/Defs/Params/RelationParam.php index 5ca8c41a74..71c3a03a0b 100644 --- a/application/Espo/ORM/Defs/Params/RelationParam.php +++ b/application/Espo/ORM/Defs/Params/RelationParam.php @@ -112,4 +112,11 @@ class RelationParam * @since 9.4.0 */ public const READ_ONLY = 'readOnly'; + + /** + * Disabled. + * + * @since 9.4.0 + */ + public const DISABLED = 'disabled'; } diff --git a/application/Espo/Tools/EntityManager/Hook/Hooks/CollaboratorsUpdateHook.php b/application/Espo/Tools/EntityManager/Hook/Hooks/CollaboratorsUpdateHook.php index b7be6c10c5..b8b0ca86dc 100644 --- a/application/Espo/Tools/EntityManager/Hook/Hooks/CollaboratorsUpdateHook.php +++ b/application/Espo/Tools/EntityManager/Hook/Hooks/CollaboratorsUpdateHook.php @@ -36,6 +36,8 @@ use Espo\Core\ORM\Type\FieldType; use Espo\Core\Utils\Log; use Espo\Core\Utils\Metadata; use Espo\Entities\User; +use Espo\Modules\Crm\Entities\CaseObj; +use Espo\ORM\Defs\Params\FieldParam; use Espo\ORM\Defs\Params\RelationParam; use Espo\ORM\Type\RelationType; use Espo\Tools\EntityManager\Hook\UpdateHook; @@ -52,6 +54,13 @@ class CollaboratorsUpdateHook implements UpdateHook private const DEFAULT_MAX_COUNT = 30; + /** + * @var string[] + */ + private array $enabledByDefaultEntityTypeList = [ + CaseObj::ENTITY_TYPE, + ]; + public function __construct( private Metadata $metadata, private Log $log, @@ -81,6 +90,59 @@ class CollaboratorsUpdateHook implements UpdateHook return; } + if ($this->isEnabledByDefault($entityType)) { + $this->addEnabledByDefault($entityType); + } else { + $this->addInternal($entityType); + } + + $this->metadata->save(); + $this->dataManager->rebuild([$entityType]); + } + + private function remove(string $entityType): void + { + $field = self::FIELD; + + if ( + $this->metadata->get("entityDefs.$entityType.links.$field.isCustom") && + $this->metadata->get("entityDefs.$entityType.links.$field.relationName") !== self::RELATION_NAME + ) { + return; + } + + $this->metadata->delete('entityDefs', $entityType, [ + 'fields.' . self::FIELD, + 'links.' . self::FIELD, + ]); + + $this->metadata->delete('entityAcl', $entityType, [ + 'links.' . self::FIELD, + ]); + + $this->metadata->save(); + + // Must be after metadata is saved. + if ($this->isEnabledByDefault($entityType)) { + $this->metadata->set('entityDefs', $entityType, [ + 'fields' => [ + self::FIELD => [ + FieldParam::DISABLED => true, + ], + ], + 'links' => [ + self::FIELD => [ + RelationParam::DISABLED => true, + ], + ], + ]); + + $this->metadata->save(); + } + } + + private function addInternal(string $entityType): void + { $this->metadata->set('entityDefs', $entityType, [ 'fields' => [ self::FIELD => [ @@ -116,32 +178,22 @@ class CollaboratorsUpdateHook implements UpdateHook ], ], ]); - - $this->metadata->save(); - - $this->dataManager->rebuild([$entityType]); } - private function remove(string $entityType): void + private function addEnabledByDefault(string $entityType): void { - $field = self::FIELD; - - if ( - $this->metadata->get("entityDefs.$entityType.links.$field.isCustom") && - $this->metadata->get("entityDefs.$entityType.links.$field.relationName") !== self::RELATION_NAME - ) { - return; - } - $this->metadata->delete('entityDefs', $entityType, [ 'fields.' . self::FIELD, 'links.' . self::FIELD, ]); + } - $this->metadata->delete('entityAcl', $entityType, [ - 'links.' . self::FIELD, - ]); - - $this->metadata->save(); + /** + * @param string $entityType + * @return bool + */ + private function isEnabledByDefault(string $entityType): bool + { + return in_array($entityType, $this->enabledByDefaultEntityTypeList); } }