From 4e0d5a22676d02af6332d00fa5de62b22da9333e Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sat, 25 Oct 2025 10:22:56 +0300 Subject: [PATCH 1/5] schema --- schema/metadata/entityDefs.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/schema/metadata/entityDefs.json b/schema/metadata/entityDefs.json index 1c9bc4abf0..6ad4d546e8 100644 --- a/schema/metadata/entityDefs.json +++ b/schema/metadata/entityDefs.json @@ -986,6 +986,10 @@ "type": "boolean", "description": "Disables the ability to customize the field in the Entity Manager tool." }, + "dynamicLogicDisabled": { + "type": "boolean", + "description": "Dynamic logic cannot be customized." + }, "fieldManagerParamList": { "type": "array", "items": { From 599646c397f4155e587c3e8f9560cbf928dbce13 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sat, 25 Oct 2025 12:47:21 +0300 Subject: [PATCH 2/5] metadata aclDependency anyScopeList --- .../Tools/App/Metadata/AclDependencyItem.php | 19 +++++++++-- .../App/Metadata/AclDependencyProvider.php | 14 +++++--- .../Espo/Tools/App/MetadataService.php | 32 +++++++++++++------ schema/metadata/app/metadata.json | 8 ++--- .../Espo/Tools/App/MetadataTest.php | 18 ++++++++++- 5 files changed, 68 insertions(+), 23 deletions(-) diff --git a/application/Espo/Tools/App/Metadata/AclDependencyItem.php b/application/Espo/Tools/App/Metadata/AclDependencyItem.php index 3dc0ac360c..2e26c5d8e7 100644 --- a/application/Espo/Tools/App/Metadata/AclDependencyItem.php +++ b/application/Espo/Tools/App/Metadata/AclDependencyItem.php @@ -31,10 +31,14 @@ namespace Espo\Tools\App\Metadata; class AclDependencyItem { + /** + * @param ?string[] $anyScopeList + */ public function __construct( private string $target, - private string $scope, - private ?string $field + private ?string $scope, + private ?string $field, + private ?array $anyScopeList = null, ) {} /** @@ -45,7 +49,7 @@ class AclDependencyItem return $this->target; } - public function getScope(): string + public function getScope(): ?string { return $this->scope; } @@ -54,4 +58,13 @@ class AclDependencyItem { return $this->field; } + + /** + * @return ?string[] + * @since 9.2.5 + */ + public function getAnyScopeList(): ?array + { + return $this->anyScopeList; + } } diff --git a/application/Espo/Tools/App/Metadata/AclDependencyProvider.php b/application/Espo/Tools/App/Metadata/AclDependencyProvider.php index 2996b818bd..8791b788d4 100644 --- a/application/Espo/Tools/App/Metadata/AclDependencyProvider.php +++ b/application/Espo/Tools/App/Metadata/AclDependencyProvider.php @@ -95,15 +95,13 @@ class AclDependencyProvider $data = []; foreach (($this->metadata->get(['app', 'metadata', 'aclDependencies']) ?? []) as $target => $item) { + $anyScopeList = $item['anyScopeList'] ?? null; $scope = $item['scope'] ?? null; $field = $item['field'] ?? null; - if (!$scope) { - continue; - } - $data[] = [ 'target' => $target, + 'anyScopeList' => $anyScopeList, 'scope' => $scope, 'field' => $field, ]; @@ -196,8 +194,14 @@ class AclDependencyProvider $target = $rawItem['target'] ?? null; $scope = $rawItem['scope'] ?? null; $field = $rawItem['field'] ?? null; + $anyScopeList = $rawItem['anyScopeList'] ?? null; - $list[] = new AclDependencyItem($target, $scope, $field); + $list[] = new AclDependencyItem( + target: $target, + scope: $scope, + field: $field, + anyScopeList: $anyScopeList, + ); } return $list; diff --git a/application/Espo/Tools/App/MetadataService.php b/application/Espo/Tools/App/MetadataService.php index 256d53f50e..6d6401559d 100644 --- a/application/Espo/Tools/App/MetadataService.php +++ b/application/Espo/Tools/App/MetadataService.php @@ -178,20 +178,32 @@ class MetadataService foreach ($this->aclDependencyProvider->get() as $dependencyItem) { $aclScope = $dependencyItem->getScope(); $aclField = $dependencyItem->getField(); + $anyScopeList = $dependencyItem->getAnyScopeList(); - if (!$aclScope) { - continue; + if ($anyScopeList) { + $skip = true; + + foreach ($anyScopeList as $itemScope) { + if ($this->acl->tryCheck($itemScope)) { + $skip = false; + + break; + } + } + + if ($skip) { + continue; + } } - if (!$this->acl->tryCheck($aclScope)) { - continue; - } + if ($aclScope) { + if (!$this->acl->tryCheck($aclScope)) { + continue; + } - if ( - $aclField && - in_array($aclField, $this->acl->getScopeForbiddenFieldList($aclScope)) - ) { - continue; + if ($aclField && in_array($aclField, $this->acl->getScopeForbiddenFieldList($aclScope))) { + continue; + } } $targetArr = explode('.', $dependencyItem->getTarget()); diff --git a/schema/metadata/app/metadata.json b/schema/metadata/app/metadata.json index cd9099eca7..5e321f943e 100644 --- a/schema/metadata/app/metadata.json +++ b/schema/metadata/app/metadata.json @@ -44,21 +44,21 @@ }, "aclDependencies": { "type": "object", - "description": "Rules making a metadata sections available for a user when they don't have access to a scope.", + "description": "Rules making a metadata sections available for the user when they don't have access to the scope.", "additionalProperties": { "description": "A metadata path, items are separated by dots.", "properties": { "scope": { "type": "string", - "description": "If a user have access to the scope, they will have access to a metadata section defined by a key." + "description": "If the user has access to the scope, they will have access to the metadata section defined by the key." }, "field": { "type": "string", - "description": "If a user have access to the field (of a scope), they will have access to a metadata section defined by a key." + "description": "If the user has access to the field (of the scope), they will have access to the metadata section defined by the key." }, "anyScopeList": { "type": "array", - "description": "Not supported. TBD.", + "description": "If the user has access to any of the list scopes, they will have access to the metadata section defined by the key. As of v9.2.5.", "items": { "type": "string" } diff --git a/tests/integration/Espo/Tools/App/MetadataTest.php b/tests/integration/Espo/Tools/App/MetadataTest.php index 4dbaa1b998..da8a23261f 100644 --- a/tests/integration/Espo/Tools/App/MetadataTest.php +++ b/tests/integration/Espo/Tools/App/MetadataTest.php @@ -29,6 +29,7 @@ namespace tests\integration\Espo\Tools\App; +use Espo\Core\Utils\Metadata; use Espo\Tools\App\MetadataService; use tests\integration\Core\BaseTestCase; @@ -36,6 +37,20 @@ class MetadataTest extends BaseTestCase { public function testAclDependency(): void { + $metadata = $this->getContainer()->getByClass(Metadata::class); + + $metadata->set('app', 'metadata', [ + 'aclDependencies' => [ + 'entityDefs.Campaign' => [ + 'anyScopeList' => ['Opportunity'], + ], + ], + ]); + + $metadata->save(); + + $this->reCreateApplication(); + $this->createUser('tester', [ 'data' => [ 'Lead' => false, @@ -55,6 +70,7 @@ class MetadataTest extends BaseTestCase $data = $this->getInjectableFactory()->create(MetadataService::class)->getDataForFrontend(); $this->assertIsArray($data?->entityDefs?->Lead?->fields?->source?->options); - $this->assertNull($data->entityDefs->Lead->fields?->name ?? null); + $this->assertNull($data->entityDefs->Lead->fields->name ?? null); + $this->assertNotNull($data?->entityDefs->Campaign ?? null); } } From d69cfd8b31a8ef8aea51872dbcc514bf176ad175 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sat, 25 Oct 2025 12:51:26 +0300 Subject: [PATCH 3/5] typo --- schema/metadata/app/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schema/metadata/app/metadata.json b/schema/metadata/app/metadata.json index 5e321f943e..9649a30a49 100644 --- a/schema/metadata/app/metadata.json +++ b/schema/metadata/app/metadata.json @@ -58,7 +58,7 @@ }, "anyScopeList": { "type": "array", - "description": "If the user has access to any of the list scopes, they will have access to the metadata section defined by the key. As of v9.2.5.", + "description": "If the user has access to any of the listed scopes, they will have access to the metadata section defined by the key. As of v9.2.5.", "items": { "type": "string" } From 0d4d4dc9ed6316fa4d5a204aaff24203e2ad7a9e Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sat, 25 Oct 2025 17:25:13 +0300 Subject: [PATCH 4/5] category folders fetch on refresh --- client/src/views/list-with-categories.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/client/src/views/list-with-categories.js b/client/src/views/list-with-categories.js index d156a52653..dc32d09d26 100644 --- a/client/src/views/list-with-categories.js +++ b/client/src/views/list-with-categories.js @@ -902,6 +902,16 @@ class ListWithCategories extends ListView { Espo.Ui.notify(); } + + /** + * @protected + */ + async actionFullRefresh() { + await Promise.all([ + super.actionFullRefresh(), + this.nestedCategoriesCollection?.fetch(), + ]); + } } export default ListWithCategories; From 02f1e94c866c53d009df8d29ce03be876c95fbe7 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sat, 25 Oct 2025 17:33:41 +0300 Subject: [PATCH 5/5] fix link category parent field --- client/src/views/fields/link-category-tree.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/src/views/fields/link-category-tree.js b/client/src/views/fields/link-category-tree.js index 70c2b0d237..0ea2afe275 100644 --- a/client/src/views/fields/link-category-tree.js +++ b/client/src/views/fields/link-category-tree.js @@ -34,6 +34,11 @@ class LinkCategoryTreeFieldView extends LinkFieldView { autocompleteDisabled = false getUrl() { + if (this.getMetadata().get(`scopes.${this.entityType}.type`) === 'CategoryTree') { + // Can be used for the 'parent' field of the category entity type. + return super.getUrl(); + } + const id = this.model.get(this.idName); if (!id) {