diff --git a/application/Espo/Classes/FieldValidators/ArrayType.php b/application/Espo/Classes/FieldValidators/ArrayType.php index 47428a750a..f580f8d6c1 100644 --- a/application/Espo/Classes/FieldValidators/ArrayType.php +++ b/application/Espo/Classes/FieldValidators/ArrayType.php @@ -128,6 +128,14 @@ class ArrayType /** @var ?string $path */ $path = $fieldDefs->getParam('optionsPath'); + /** @var ?string $path */ + $ref = $fieldDefs->getParam('optionsReference'); + + if (!$path && $ref && str_contains($ref, '.')) { + [$refEntityType, $refField] = explode('.', $ref); + + $path = "entityDefs.{$refEntityType}.fields.{$refField}.options"; + } /** @var string[]|null|false $optionList */ $optionList = $path ? diff --git a/application/Espo/Classes/FieldValidators/EnumType.php b/application/Espo/Classes/FieldValidators/EnumType.php index d2955ee443..69086a33db 100644 --- a/application/Espo/Classes/FieldValidators/EnumType.php +++ b/application/Espo/Classes/FieldValidators/EnumType.php @@ -37,7 +37,6 @@ use Espo\ORM\Entity; class EnumType { private Metadata $metadata; - private Defs $defs; private const DEFAULT_MAX_LENGTH = 255; @@ -65,6 +64,14 @@ class EnumType /** @var ?string $path */ $path = $fieldDefs->getParam('optionsPath'); + /** @var ?string $path */ + $ref = $fieldDefs->getParam('optionsReference'); + + if (!$path && $ref && str_contains($ref, '.')) { + [$refEntityType, $refField] = explode('.', $ref); + + $path = "entityDefs.{$refEntityType}.fields.{$refField}.options"; + } /** @var string[]|null|false $optionList */ $optionList = $path ? diff --git a/application/Espo/Modules/Crm/Resources/metadata/app/language.json b/application/Espo/Modules/Crm/Resources/metadata/app/language.json index bdac852b50..8d98cbddc4 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/app/language.json +++ b/application/Espo/Modules/Crm/Resources/metadata/app/language.json @@ -1,13 +1,5 @@ { "aclDependencies": { - "Lead.options.source": { - "scope": "Opportunity", - "field": "leadSource" - }, - "Account.options.industry": { - "scope": "Lead", - "field": "industry" - }, "Meeting": { "anyScopeList": ["Call"] } diff --git a/application/Espo/Modules/Crm/Resources/metadata/app/metadata.json b/application/Espo/Modules/Crm/Resources/metadata/app/metadata.json index 67e977aefe..6dcea92335 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/app/metadata.json +++ b/application/Espo/Modules/Crm/Resources/metadata/app/metadata.json @@ -1,14 +1,5 @@ { - "aclDependencies": { - "entityDefs.Lead.fields.source.options": { - "scope": "Opportunity", - "field": "leadSource" - }, - "entityDefs.Account.fields.industry.options": { - "scope": "Lead", - "field": "industry" - } - }, + "aclDependencies": {}, "frontendHiddenPathList": [ "__APPEND__", ["app", "calendar", "additionalAttributeList"] diff --git a/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Lead.json b/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Lead.json index f3d6fc7152..0b4f7e5097 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Lead.json +++ b/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Lead.json @@ -66,8 +66,7 @@ "type": "enum", "view": "crm:views/lead/fields/industry", "customizationOptionsDisabled": true, - "optionsPath": "entityDefs.Account.fields.industry.options", - "translation": "Account.options.industry", + "optionsReference": "Account.industry", "default": "", "isSorted": true }, diff --git a/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Opportunity.json b/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Opportunity.json index 898167c7b0..02ad439d3e 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Opportunity.json +++ b/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Opportunity.json @@ -201,8 +201,7 @@ "type": "enum", "view": "crm:views/opportunity/fields/lead-source", "customizationOptionsDisabled": true, - "optionsPath": "entityDefs.Lead.fields.source.options", - "translation": "Lead.options.source" + "optionsReference": "Lead.source" }, "closeDate": { "type": "date", diff --git a/application/Espo/Resources/i18n/en_US/Admin.json b/application/Espo/Resources/i18n/en_US/Admin.json index 2937a22972..5ad3e05478 100644 --- a/application/Espo/Resources/i18n/en_US/Admin.json +++ b/application/Espo/Resources/i18n/en_US/Admin.json @@ -162,6 +162,7 @@ "default": "Default", "maxLength": "Max Length", "options": "Options", + "optionsReference": "Options Reference", "after": "After (field)", "before": "Before (field)", "link": "Link", diff --git a/application/Espo/Resources/i18n/en_US/FieldManager.json b/application/Espo/Resources/i18n/en_US/FieldManager.json index cc8b2a494c..73c7f14540 100644 --- a/application/Espo/Resources/i18n/en_US/FieldManager.json +++ b/application/Espo/Resources/i18n/en_US/FieldManager.json @@ -80,6 +80,7 @@ } }, "tooltips": { + "optionsReference": "Re-use options from another field.", "currencyDecimal": "Use the Decimal DB type. In the app, values will be represented as strings. Check this parameter if precision is required.", "cutHeight": "A text higher then a specified value will be cut with a 'show more' button displayed.", "urlStrip": "Strip a protocol and a trailing slash.", diff --git a/application/Espo/Resources/metadata/fields/array.json b/application/Espo/Resources/metadata/fields/array.json index 27cdc410e9..1e6300b66b 100644 --- a/application/Espo/Resources/metadata/fields/array.json +++ b/application/Espo/Resources/metadata/fields/array.json @@ -11,6 +11,12 @@ "view": "views/admin/field-manager/fields/options", "tooltip": "optionsArray" }, + { + "name": "optionsReference", + "type": "varchar", + "view": "views/admin/field-manager/fields/options-reference", + "tooltip": true + }, { "name": "translation", "type": "varchar", diff --git a/application/Espo/Resources/metadata/fields/checklist.json b/application/Espo/Resources/metadata/fields/checklist.json index c710494030..0bdc7b8375 100644 --- a/application/Espo/Resources/metadata/fields/checklist.json +++ b/application/Espo/Resources/metadata/fields/checklist.json @@ -13,6 +13,12 @@ "required": true, "tooltip": true }, + { + "name": "optionsReference", + "type": "varchar", + "view": "views/admin/field-manager/fields/options-reference", + "tooltip": true + }, { "name": "isSorted", "type": "bool" diff --git a/application/Espo/Resources/metadata/fields/enum.json b/application/Espo/Resources/metadata/fields/enum.json index 269160516f..0d9d8378f2 100644 --- a/application/Espo/Resources/metadata/fields/enum.json +++ b/application/Espo/Resources/metadata/fields/enum.json @@ -16,6 +16,12 @@ "type": "enum", "view": "views/admin/field-manager/fields/options/default" }, + { + "name": "optionsReference", + "type": "varchar", + "view": "views/admin/field-manager/fields/options-reference", + "tooltip": true + }, { "name": "isSorted", "type": "bool" diff --git a/application/Espo/Resources/metadata/fields/multiEnum.json b/application/Espo/Resources/metadata/fields/multiEnum.json index a1993a40c9..1de47a457c 100644 --- a/application/Espo/Resources/metadata/fields/multiEnum.json +++ b/application/Espo/Resources/metadata/fields/multiEnum.json @@ -11,6 +11,12 @@ "view": "views/admin/field-manager/fields/options-with-style", "tooltip": true }, + { + "name": "optionsReference", + "type": "varchar", + "view": "views/admin/field-manager/fields/options-reference", + "tooltip": true + }, { "name": "isSorted", "type": "bool" diff --git a/application/Espo/Tools/App/Language/AclDependencyProvider.php b/application/Espo/Tools/App/Language/AclDependencyProvider.php index d112d469a7..3d10c07042 100644 --- a/application/Espo/Tools/App/Language/AclDependencyProvider.php +++ b/application/Espo/Tools/App/Language/AclDependencyProvider.php @@ -32,11 +32,20 @@ namespace Espo\Tools\App\Language; use Espo\Core\Utils\Config; use Espo\Core\Utils\DataCache; use Espo\Core\Utils\Metadata; +use Espo\ORM\Defs; class AclDependencyProvider { private const CACHE_KEY = 'languageAclDependency'; + /** @var string[] */ + private array $enumFieldTypeList = [ + 'enum', + 'multiEnum', + 'array', + 'checklist', + ]; + /** @var ?AclDependencyItem[] */ private ?array $data = null; private bool $useCache; @@ -44,7 +53,8 @@ class AclDependencyProvider public function __construct( private DataCache $dataCache, private Metadata $metadata, - Config $config + Config $config, + private Defs $ormDefs ) { $this->useCache = $config->get('useCache'); } @@ -96,6 +106,35 @@ class AclDependencyProvider ]; } + foreach ($this->ormDefs->getEntityList() as $entityDefs) { + if (!$this->metadata->get(['scopes', $entityDefs->getName(), 'object'])) { + continue; + } + + foreach ($entityDefs->getFieldList() as $fieldDefs) { + if (!in_array($fieldDefs->getType(), $this->enumFieldTypeList)) { + continue; + } + + $optionsReference = $fieldDefs->getParam('optionsReference'); + + if (!$optionsReference || !str_contains($optionsReference, '.')) { + continue; + } + + [$refEntityType, $refField] = explode('.', $optionsReference); + + $target = "{$refEntityType}.options.{$refField}"; + + $data[] = [ + 'target' => $target, + 'anyScopeList' => null, + 'scope' => $entityDefs->getName(), + 'field' => $fieldDefs->getName(), + ]; + } + } + if ($this->useCache) { $this->dataCache->store(self::CACHE_KEY, $data); } diff --git a/application/Espo/Tools/App/Metadata/AclDependencyProvider.php b/application/Espo/Tools/App/Metadata/AclDependencyProvider.php index a4f0fbe4d9..e7efc81c11 100644 --- a/application/Espo/Tools/App/Metadata/AclDependencyProvider.php +++ b/application/Espo/Tools/App/Metadata/AclDependencyProvider.php @@ -32,11 +32,20 @@ namespace Espo\Tools\App\Metadata; use Espo\Core\Utils\Config; use Espo\Core\Utils\DataCache; use Espo\Core\Utils\Metadata; +use Espo\ORM\Defs; class AclDependencyProvider { private const CACHE_KEY = 'metadataAclDependency'; + /** @var string[] */ + private array $enumFieldTypeList = [ + 'enum', + 'multiEnum', + 'array', + 'checklist', + ]; + /** @var ?AclDependencyItem[] */ private ?array $data = null; private bool $useCache; @@ -44,7 +53,8 @@ class AclDependencyProvider public function __construct( private DataCache $dataCache, private Metadata $metadata, - Config $config + Config $config, + private Defs $ormDefs ) { $this->useCache = $config->get('useCache'); } @@ -98,6 +108,41 @@ class AclDependencyProvider ]; } + foreach ($this->ormDefs->getEntityList() as $entityDefs) { + if (!$this->metadata->get(['scopes', $entityDefs->getName(), 'object'])) { + continue; + } + + foreach ($entityDefs->getFieldList() as $fieldDefs) { + if (!in_array($fieldDefs->getType(), $this->enumFieldTypeList)) { + continue; + } + + $optionsPath = $fieldDefs->getParam('optionsPath'); + $optionsReference = $fieldDefs->getParam('optionsReference'); + + if ( + !$optionsPath && + $optionsReference && + str_contains($optionsReference, '.') + ) { + [$refEntityType, $refField] = explode('.', $optionsReference); + + $optionsPath = "entityDefs.{$refEntityType}.fields.{$refField}.options"; + } + + if (!$optionsPath) { + continue; + } + + $data[] = [ + 'target' => $optionsPath, + 'scope' => $entityDefs->getName(), + 'field' => $fieldDefs->getName(), + ]; + } + } + if ($this->useCache) { $this->dataCache->store(self::CACHE_KEY, $data); } diff --git a/client/src/views/admin/field-manager/fields/options-reference.js b/client/src/views/admin/field-manager/fields/options-reference.js new file mode 100644 index 0000000000..85fa35c261 --- /dev/null +++ b/client/src/views/admin/field-manager/fields/options-reference.js @@ -0,0 +1,89 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko + * Website: https://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. + ************************************************************************/ + +define('views/admin/field-manager/fields/options-reference', ['views/fields/enum'], function (Dep) { + + return Dep.extend({ + + enumFieldTypeList: [ + 'enum', + 'multiEnum', + 'array', + 'checklist', + 'varchar', + ], + + setupOptions: function () { + this.params.options = ['']; + + let entityTypeList = Object.keys(this.getMetadata().get(['entityDefs'])) + .filter(item => this.getMetadata().get(['scopes', item, 'object'])) + .sort((s1, s2) => { + return this.getLanguage().translate(s1, 'scopesName') + .localeCompare(this.getLanguage().translate(s2, 'scopesName')); + }); + + this.translatedOptions = {}; + + entityTypeList.forEach(entityType => { + let fieldList = + Object.keys(this.getMetadata().get(['entityDefs', entityType, 'fields']) || []) + .filter(item => entityType !== this.model.scope || item !== this.model.get('name')) + .sort((s1, s2) => { + return this.getLanguage().translate(s1, 'fields', entityType) + .localeCompare(this.getLanguage().translate(s2, 'fields', entityType)); + }); + + fieldList.forEach(field => { + let {type, options, optionsPath, optionsReference} = + this.getMetadata().get(['entityDefs', entityType, 'fields', field]) || {}; + + if (!this.enumFieldTypeList.includes(type)) { + return; + } + + if (optionsPath || optionsReference) { + return; + } + + if (!options) { + return; + } + + let value = entityType + '.' + field; + + this.params.options.push(value); + + this.translatedOptions[value] = + this.translate(entityType, 'scopeName') + ' ยท ' + + this.translate(field, 'fields', entityType); + }); + }); + }, + }); +}); diff --git a/client/src/views/fields/array.js b/client/src/views/fields/array.js index 680105ec89..d23f6e422a 100644 --- a/client/src/views/fields/array.js +++ b/client/src/views/fields/array.js @@ -138,10 +138,18 @@ function (Dep, RegExpPattern, /** module:ui/multi-select*/MultiSelect) { this.selected = []; } - if (this.params.optionsPath) { - this.params.options = Espo.Utils.clone( - this.getMetadata().get(this.params.optionsPath) || [] - ); + let optionsPath = this.params.optionsPath; + /** @type {?string} */ + let optionsReference = this.params.optionsReference; + + if (!optionsPath && optionsReference) { + let [refEntityType, refField] = optionsReference.split('.'); + + optionsPath = `entityDefs.${refEntityType}.fields.${refField}.options`; + } + + if (optionsPath) { + this.params.options = Espo.Utils.clone(this.getMetadata().get(optionsPath)) || []; } this.styleMap = this.params.style || {}; @@ -214,8 +222,18 @@ function (Dep, RegExpPattern, /** module:ui/multi-select*/MultiSelect) { setupTranslation: function () { let t = {}; - if (this.params.translation) { - let arr = this.params.translation.split('.'); + let translation = this.params.translation; + /** @type {?string} */ + let optionsReference = this.params.optionsReference; + + if (!translation && optionsReference) { + let [refEntityType, refField] = optionsReference.split('.'); + + translation = `${refEntityType}.options.${refField}`; + } + + if (translation) { + let arr = translation.split('.'); let pointer = this.getLanguage().data; diff --git a/client/src/views/fields/enum.js b/client/src/views/fields/enum.js index 5351f6691b..3ae89a4ddd 100644 --- a/client/src/views/fields/enum.js +++ b/client/src/views/fields/enum.js @@ -109,10 +109,18 @@ function (Dep, /** module:ui/multi-select*/MultiSelect, /** module:ui/select*/Se } } - if (this.params.optionsPath) { - this.params.options = Espo.Utils.clone( - this.getMetadata().get(this.params.optionsPath) || [] - ); + let optionsPath = this.params.optionsPath; + /** @type {?string} */ + let optionsReference = this.params.optionsReference; + + if (!optionsPath && optionsReference) { + let [refEntityType, refField] = optionsReference.split('.'); + + optionsPath = `entityDefs.${refEntityType}.fields.${refField}.options`; + } + + if (optionsPath) { + this.params.options = Espo.Utils.clone(this.getMetadata().get(optionsPath)) || []; } this.styleMap = this.params.style || this.model.getFieldParam(this.name, 'style') || {}; @@ -153,13 +161,23 @@ function (Dep, /** module:ui/multi-select*/MultiSelect, /** module:ui/select*/Se }, setupTranslation: function () { - if (!this.params.translation) { + let translation = this.params.translation; + /** @type {?string} */ + let optionsReference = this.params.optionsReference; + + if (!translation && optionsReference) { + let [refEntityType, refField] = optionsReference.split('.'); + + translation = `${refEntityType}.options.${refField}`; + } + + if (!translation) { return; } let translationObj; - let arr = this.params.translation.split('.'); + let arr = translation.split('.'); let pointer = this.getLanguage().data; arr.forEach(key => {