diff --git a/application/Espo/Core/Templates/Entities/CategoryTree.php b/application/Espo/Core/Templates/Entities/CategoryTree.php index 1bb3a29b1d..9053a9241b 100644 --- a/application/Espo/Core/Templates/Entities/CategoryTree.php +++ b/application/Espo/Core/Templates/Entities/CategoryTree.php @@ -32,4 +32,6 @@ namespace Espo\Core\Templates\Entities; use Espo\Core\Entities\CategoryTreeItem; class CategoryTree extends CategoryTreeItem -{} +{ + public const string TEMPLATE_TYPE = 'CategoryTree'; +} diff --git a/application/Espo/Core/Templates/Metadata/CategoryTree/clientDefs.json b/application/Espo/Core/Templates/Metadata/CategoryTree/clientDefs.json index 7affbef26c..f01f13ff64 100644 --- a/application/Espo/Core/Templates/Metadata/CategoryTree/clientDefs.json +++ b/application/Espo/Core/Templates/Metadata/CategoryTree/clientDefs.json @@ -9,6 +9,13 @@ "link": "#{entityType}/list", "acl": "read", "style": "default" + }, + { + "labelTranslation": "Global.scopeNamesPlural.{subjectEntityType}", + "link": "#{subjectEntityType}", + "acl": "read", + "style": "default", + "aclScope": "{subjectEntityType}" } ] }, @@ -19,6 +26,13 @@ "link": "#{entityType}", "acl": "read", "style": "default" + }, + { + "labelTranslation": "Global.scopeNamesPlural.{subjectEntityType}", + "link": "#{subjectEntityType}", + "acl": "read", + "style": "default", + "aclScope": "{subjectEntityType}" } ] } @@ -29,4 +43,4 @@ "unlinkDisabled": true } } -} \ No newline at end of file +} diff --git a/application/Espo/Core/Templates/Metadata/CategoryTree/scopes.json b/application/Espo/Core/Templates/Metadata/CategoryTree/scopes.json index 78db1a0b32..0b6d3b26c5 100644 --- a/application/Espo/Core/Templates/Metadata/CategoryTree/scopes.json +++ b/application/Espo/Core/Templates/Metadata/CategoryTree/scopes.json @@ -1,10 +1,26 @@ { "entity": true, - "layouts": true, - "tab": true, + "tab": false, "acl": true, "aclPortal": true, - "customizable": true, + "aclLevelList": [ + "all", + "team", + "no" + ], + "aclPortalLevelList": [ + "all", + "no" + ], "importable": false, - "notifications": false -} \ No newline at end of file + "notifications": false, + "customizable": false, + "entityManager": { + "fields": false, + "formula": false, + "relationships": false, + "addField": false, + "edit": false, + "layouts": false + } +} diff --git a/application/Espo/Core/Templates/i18n/en_US/CategoryTree.json b/application/Espo/Core/Templates/i18n/en_US/CategoryTree.json new file mode 100644 index 0000000000..81eede104f --- /dev/null +++ b/application/Espo/Core/Templates/i18n/en_US/CategoryTree.json @@ -0,0 +1,5 @@ +{ + "labels": { + "Create {entityType}": "Create {entityTypeTranslated}" + } +} diff --git a/application/Espo/Resources/i18n/en_US/EntityManager.json b/application/Espo/Resources/i18n/en_US/EntityManager.json index 5b98e7fe77..79c3a50177 100644 --- a/application/Espo/Resources/i18n/en_US/EntityManager.json +++ b/application/Espo/Resources/i18n/en_US/EntityManager.json @@ -55,7 +55,8 @@ "assignedUsers": "Multiple Assigned Users", "collaborators": "Collaborators", "aclContactLink": "ACL Contact Link", - "aclAccountLink": "ACL Account Link" + "aclAccountLink": "ACL Account Link", + "categories": "Categories" }, "options": { "type": { @@ -98,6 +99,7 @@ "beforeSaveApiScript": "A script called on create and update API requests before an entity is saved. Use for custom validation and duplicate checking." }, "tooltips": { + "categories": "Enable the category tree feature. Records can be placed into categories.", "aclContactLink": "The link with Contact to use when applying access control for portal users.", "aclAccountLink": "The link with Account to use when applying access control for portal users.", "collaborators": "The ability to share records with specific users.", @@ -118,5 +120,11 @@ "countDisabled": "Total number won't be displayed on the list view. Can decrease loading time when the DB table is big.", "fullTextSearch": "Running rebuild is required.", "linkParamReadOnly": "A read-only link cannot be edited via the *link* and *unlink* API requests. It won't be possible to relate and unrelate records via the relationship panel. It still possible to edit read-only links via link and link-multiple fields." + }, + "entityNameParts": { + "Category": "Category" + }, + "entityNamePartsPlural": { + "Category": "Categories" } } diff --git a/application/Espo/Resources/i18n/en_US/Global.json b/application/Espo/Resources/i18n/en_US/Global.json index 39d2ca83df..7e2798532c 100644 --- a/application/Espo/Resources/i18n/en_US/Global.json +++ b/application/Espo/Resources/i18n/en_US/Global.json @@ -521,7 +521,8 @@ "names": "Names", "types": "Types", "targetListIsOptedOut": "Is Opted Out (Target List)", - "childList": "Child List" + "childList": "Child List", + "category": "Category" }, "links": { "assignedUser": "Assigned User", diff --git a/application/Espo/Resources/metadata/app/entityManager.json b/application/Espo/Resources/metadata/app/entityManager.json index 212cacc3c4..b47316b6b2 100644 --- a/application/Espo/Resources/metadata/app/entityManager.json +++ b/application/Espo/Resources/metadata/app/entityManager.json @@ -6,11 +6,13 @@ "deleteHookClassNameList": [ "Espo\\Tools\\EntityManager\\Hook\\Hooks\\PlusDeleteHook", "Espo\\Tools\\EntityManager\\Hook\\Hooks\\EventDeleteHook", - "Espo\\Tools\\EntityManager\\Hook\\Hooks\\DeleteHasChildrenLinks" + "Espo\\Tools\\EntityManager\\Hook\\Hooks\\DeleteHasChildrenLinks", + "Espo\\Tools\\EntityManager\\Hook\\Hooks\\CategoriesDeleteHook" ], "updateHookClassNameList": [ "Espo\\Tools\\EntityManager\\Hook\\Hooks\\AssignedUsersUpdateHook", "Espo\\Tools\\EntityManager\\Hook\\Hooks\\CollaboratorsUpdateHook", - "Espo\\Tools\\EntityManager\\Hook\\Hooks\\StreamUpdateHook" + "Espo\\Tools\\EntityManager\\Hook\\Hooks\\StreamUpdateHook", + "Espo\\Tools\\EntityManager\\Hook\\Hooks\\CategoriesUpdateHook" ] } diff --git a/application/Espo/Resources/metadata/app/entityManagerParams.json b/application/Espo/Resources/metadata/app/entityManagerParams.json index 0dda3bcc38..486f114f56 100644 --- a/application/Espo/Resources/metadata/app/entityManagerParams.json +++ b/application/Espo/Resources/metadata/app/entityManagerParams.json @@ -167,6 +167,13 @@ "tooltip": true, "view": "views/admin/entity-manager/fields/acl-account-link" } + }, + "categories": { + "location": "scopes", + "fieldDefs": { + "type": "bool", + "tooltip": true + } } }, "@BasePlus": { @@ -216,6 +223,13 @@ "tooltip": true, "view": "views/admin/entity-manager/fields/acl-account-link" } + }, + "categories": { + "location": "scopes", + "fieldDefs": { + "type": "bool", + "tooltip": true + } } } } diff --git a/application/Espo/Resources/metadata/app/entityTemplateList.json b/application/Espo/Resources/metadata/app/entityTemplateList.json index 44a776bcac..0f01a3da0c 100644 --- a/application/Espo/Resources/metadata/app/entityTemplateList.json +++ b/application/Espo/Resources/metadata/app/entityTemplateList.json @@ -1,7 +1,8 @@ [ - "Base", - "BasePlus", - "Event", - "Person", - "Company" -] \ No newline at end of file + "Base", + "BasePlus", + "Event", + "Person", + "Company", + "CategoryTree" +] diff --git a/application/Espo/Resources/metadata/app/entityTemplates.json b/application/Espo/Resources/metadata/app/entityTemplates.json index a7990d4e3b..17a14b44d0 100644 --- a/application/Espo/Resources/metadata/app/entityTemplates.json +++ b/application/Espo/Resources/metadata/app/entityTemplates.json @@ -18,5 +18,10 @@ "Person": { "entityClassName": "Espo\\Core\\Templates\\Entities\\Person", "repositoryClassName": "Espo\\Core\\Templates\\Repositories\\Person" + }, + "CategoryTree": { + "entityClassName": "Espo\\Core\\Templates\\Entities\\CategoryTree", + "repositoryClassName": "Espo\\Core\\Templates\\Repositories\\CategoryTree", + "isNotCreatable": true } } diff --git a/application/Espo/Tools/EntityManager/CreateParams.php b/application/Espo/Tools/EntityManager/CreateParams.php index 9d5a8e0fd4..5796e15dc6 100644 --- a/application/Espo/Tools/EntityManager/CreateParams.php +++ b/application/Espo/Tools/EntityManager/CreateParams.php @@ -29,26 +29,16 @@ namespace Espo\Tools\EntityManager; -class CreateParams +readonly class CreateParams { /** * @param array $replaceData */ public function __construct( - private bool $forceCreate = false, - private array $replaceData = [] + public bool $forceCreate = false, + public array $replaceData = [], + public bool $skipCustomPrefix = false, + public bool $isNotRemovable = false, + public bool $addTab = true, ) {} - - public function forceCreate(): bool - { - return $this->forceCreate; - } - - /** - * @return array - */ - public function getReplaceData(): array - { - return $this->replaceData; - } } diff --git a/application/Espo/Tools/EntityManager/EntityManager.php b/application/Espo/Tools/EntityManager/EntityManager.php index 61b3be0362..14801755ad 100644 --- a/application/Espo/Tools/EntityManager/EntityManager.php +++ b/application/Espo/Tools/EntityManager/EntityManager.php @@ -103,11 +103,13 @@ class EntityManager /** @var array $templateDefs */ $templateDefs = $this->metadata->get(['app', 'entityTemplates', $type], []); - if (!empty($templateDefs['isNotCreatable']) && !$createParams->forceCreate()) { + if (!empty($templateDefs['isNotCreatable']) && !$createParams->forceCreate) { throw new Error("Type '$type' is not creatable."); } - $name = $this->nameUtil->addCustomPrefix($name, true); + if (!$createParams->skipCustomPrefix) { + $name = $this->nameUtil->addCustomPrefix($name, true); + } if ($this->nameUtil->nameIsBad($name)) { throw new Error("Entity name should contain only letters and numbers, " . @@ -206,7 +208,7 @@ class EntityManager } $languageContents = $this->fileManager->getContents($filePath); - $languageContents = $this->replace($languageContents, $name, $createParams->getReplaceData()); + $languageContents = $this->replace($languageContents, $name, $createParams->replaceData); $languageContents = str_replace('{entityTypeTranslated}', $labelSingular, $languageContents); $destinationFilePath = 'custom/Espo/Custom/Resources/i18n/' . $language . '/' . $name . '.json'; @@ -217,7 +219,7 @@ class EntityManager $filePath = $templatePath . "/Metadata/$type/scopes.json"; $scopesDataContents = $this->fileManager->getContents($filePath); - $scopesDataContents = $this->replace($scopesDataContents, $name, $createParams->getReplaceData()); + $scopesDataContents = $this->replace($scopesDataContents, $name, $createParams->replaceData); $scopesData = Json::decode($scopesDataContents, true); @@ -228,7 +230,7 @@ class EntityManager $scopesData['object'] = true; $scopesData['isCustom'] = true; - if (!empty($templateDefs['isNotRemovable']) || !empty($params['isNotRemovable'])) { + if ($createParams->isNotRemovable) { $scopesData['isNotRemovable'] = true; } @@ -241,7 +243,7 @@ class EntityManager $filePath = $templatePath . "/Metadata/$type/entityDefs.json"; $entityDefsDataContents = $this->fileManager->getContents($filePath); - $entityDefsDataContents = $this->replace($entityDefsDataContents, $name, $createParams->getReplaceData()); + $entityDefsDataContents = $this->replace($entityDefsDataContents, $name, $createParams->replaceData); $entityDefsData = Json::decode($entityDefsDataContents, true); @@ -250,7 +252,7 @@ class EntityManager $filePath = $templatePath . "/Metadata/$type/clientDefs.json"; $clientDefsContents = $this->fileManager->getContents($filePath); - $clientDefsContents = $this->replace($clientDefsContents, $name, $createParams->getReplaceData()); + $clientDefsContents = $this->replace($clientDefsContents, $name, $createParams->replaceData); $clientDefsData = Json::decode($clientDefsContents, true); @@ -287,13 +289,15 @@ class EntityManager $this->processCreateHook($entityTypeParams); - $tabList = $this->config->get('tabList', []); + if ($createParams->addTab) { + $tabList = $this->config->get('tabList', []); - if (!in_array($name, $tabList)) { - $tabList[] = $name; + if (!in_array($name, $tabList)) { + $tabList[] = $name; - $this->configWriter->set('tabList', $tabList); - $this->configWriter->save(); + $this->configWriter->set('tabList', $tabList); + $this->configWriter->save(); + } } $this->dataManager->rebuild(); @@ -522,7 +526,7 @@ class EntityManager throw new Forbidden; } - if (!$this->isScopeCustomizable($name)) { + if (!$this->isScopeCustomizable($name) && !$deleteParams->forceRemove()) { throw new Error("Entity type $name is not customizable."); } diff --git a/application/Espo/Tools/EntityManager/Hook/Hooks/CategoriesDeleteHook.php b/application/Espo/Tools/EntityManager/Hook/Hooks/CategoriesDeleteHook.php new file mode 100644 index 0000000000..c8dd2e7191 --- /dev/null +++ b/application/Espo/Tools/EntityManager/Hook/Hooks/CategoriesDeleteHook.php @@ -0,0 +1,66 @@ +. + * + * 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\Exceptions\Error; +use Espo\Core\Exceptions\Forbidden; +use Espo\Core\Templates\Entities\Base; +use Espo\Core\Templates\Entities\BasePlus; +use Espo\Tools\EntityManager\Hook\DeleteHook; +use Espo\Tools\EntityManager\Params; + +/** + * @noinspection PhpUnused + */ +class CategoriesDeleteHook implements DeleteHook +{ + private const string PARAM = 'categories'; + + public function __construct( + private CategoriesUpdateHook $categoriesUpdateHook, + ) {} + + /** + * @throws Forbidden + * @throws Error + */ + public function process(Params $params): void + { + if (!in_array($params->getType(), [BasePlus::TEMPLATE_TYPE, Base::TEMPLATE_TYPE])) { + return; + } + + if (!$params->get(self::PARAM)) { + return; + } + + $this->categoriesUpdateHook->remove($params->getName()); + } +} diff --git a/application/Espo/Tools/EntityManager/Hook/Hooks/CategoriesUpdateHook.php b/application/Espo/Tools/EntityManager/Hook/Hooks/CategoriesUpdateHook.php new file mode 100644 index 0000000000..f6702466be --- /dev/null +++ b/application/Espo/Tools/EntityManager/Hook/Hooks/CategoriesUpdateHook.php @@ -0,0 +1,203 @@ +. + * + * 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\DataManager; +use Espo\Core\Exceptions\BadRequest; +use Espo\Core\Exceptions\Conflict; +use Espo\Core\Exceptions\Error; +use Espo\Core\Exceptions\Forbidden; +use Espo\Core\InjectableFactory; +use Espo\Core\ORM\Type\FieldType; +use Espo\Core\Templates\Entities\Base; +use Espo\Core\Templates\Entities\BasePlus; +use Espo\Core\Templates\Entities\CategoryTree; +use Espo\Core\Utils\Language; +use Espo\Core\Utils\Log; +use Espo\Core\Utils\Metadata; +use Espo\ORM\Defs\Params\FieldParam; +use Espo\ORM\Defs\Params\RelationParam; +use Espo\ORM\Type\RelationType; +use Espo\Tools\EntityManager\CreateParams; +use Espo\Tools\EntityManager\DeleteParams; +use Espo\Tools\EntityManager\EntityManager; +use Espo\Tools\EntityManager\Hook\UpdateHook; +use Espo\Tools\EntityManager\Params; +use Espo\Tools\LayoutManager\LayoutCustomizer; +use Espo\Tools\LayoutManager\LayoutName; + +/** + * @noinspection PhpUnused + */ +class CategoriesUpdateHook implements UpdateHook +{ + private const string PARAM = 'categories'; + private const string FIELD = 'category'; + + public function __construct( + private InjectableFactory $injectableFactory, + private Language $defaultLanguage, + private Metadata $metadata, + private DataManager $dataManager, + private LayoutCustomizer $layoutCustomizer, + private Log $log, + ) {} + + /** + * @throws BadRequest + * @throws Forbidden + * @throws Error + * @throws Conflict + */ + public function process(Params $params, Params $previousParams): void + { + if (!in_array($params->getType(), [BasePlus::TEMPLATE_TYPE, Base::TEMPLATE_TYPE])) { + return; + } + + 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()); + } + } + + /** + * @throws BadRequest + * @throws Conflict + * @throws Error + */ + private function add(string $name): void + { + $entityType = $this->composeEntityType($name); + + if ($this->metadata->get("scopes.$entityType")) { + $message = "Could not create category entity type $entityType as the same entity type exists"; + + $this->log->warning($message); + + return; + } + + $createParams = new CreateParams( + forceCreate: true, + replaceData: [ + 'subjectEntityType' => $name, + ], + skipCustomPrefix: true, + isNotRemovable: true, + addTab: false, + ); + + $this->getEntityManagerTool()->create( + name: $entityType, + type: CategoryTree::TEMPLATE_TYPE, + params: [ + 'labelSingular' => $this->defaultLanguage->translateLabel($name, 'scopeNames') . ' ' . + $this->defaultLanguage->translateLabel('Category', 'entityNameParts', 'EntityManager'), + 'labelPlural' => $this->defaultLanguage->translateLabel($name, 'scopeNames') . ' ' . + $this->defaultLanguage->translateLabel('Category', 'entityNamePartsPlural', 'EntityManager') + ], + createParams: $createParams, + ); + + $this->metadata->set('entityDefs', $name, [ + 'fields' => [ + self::FIELD => [ + FieldParam::TYPE => FieldType::LINK, + 'audited' => true, + 'view' => 'views/fields/link-category-tree' + ] + ], + 'links' => [ + self::FIELD => [ + RelationParam::TYPE => RelationType::BELONGS_TO, + RelationParam::ENTITY => $entityType, + ] + ], + ]); + + $this->metadata->set('clientDefs', $name, [ + 'views' => [ + 'list' => 'views/list-with-categories', + ], + 'modalViews' => [ + 'select' => 'views/modals/select-records-with-categories', + ], + ]); + + $this->metadata->save(); + $this->dataManager->rebuild(); + + $this->layoutCustomizer->addDetailField($name, self::FIELD, LayoutName::DETAIL); + $this->layoutCustomizer->addDetailField($name, self::FIELD, LayoutName::DETAIL_SMALL); + } + + /** + * @throws Forbidden + * @throws Error + */ + public function remove(string $name): void + { + $entityType = $this->composeEntityType($name); + + $deleteParams = new DeleteParams( + forceRemove: true, + ); + + $this->getEntityManagerTool()->delete($entityType, $deleteParams); + + $this->metadata->delete('entityDefs', $name, [ + 'fields.' . self::FIELD, + 'links.' . self::FIELD, + ]); + + $this->metadata->delete('clientDefs', $name, [ + 'views.list', + 'modalViews.select', + ]); + + $this->metadata->save(); + $this->dataManager->rebuild(); + + $this->layoutCustomizer->removeInDetail($name, self::FIELD, LayoutName::DETAIL); + $this->layoutCustomizer->removeInDetail($name, self::FIELD, LayoutName::DETAIL_SMALL); + } + + private function getEntityManagerTool(): EntityManager + { + return $this->injectableFactory->create(EntityManager::class); + } + + private function composeEntityType(string $name): string + { + return $name . 'Category'; + } +} diff --git a/application/Espo/Tools/LayoutManager/LayoutCustomizer.php b/application/Espo/Tools/LayoutManager/LayoutCustomizer.php new file mode 100644 index 0000000000..4dd098eb19 --- /dev/null +++ b/application/Espo/Tools/LayoutManager/LayoutCustomizer.php @@ -0,0 +1,189 @@ +. + * + * 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\LayoutManager; + +use Espo\Core\Exceptions\Error; +use Espo\Core\Utils\Json; +use Espo\Tools\Layout\LayoutProvider; +use RuntimeException; +use stdClass; + +/** + * @since 9.4.0 + */ +class LayoutCustomizer +{ + public function __construct( + private LayoutProvider $layoutProvider, + private LayoutManager $layoutManager, + ) {} + + /** + * @throws Error + */ + public function addDetailField(string $entityType, string $field, string $layoutName): void + { + $layoutData = $this->getDetailLayout($entityType, $layoutName); + + if ($this->hasInDetail($layoutData, $field)) { + return; + } + + $lastPanel = $layoutData[count($layoutData) - 1]; + + if (!$lastPanel instanceof stdClass) { + throw new RuntimeException("Bad layout panel definition in $entityType.$layoutName."); + } + + if (isset($lastPanel->cols) && is_array($lastPanel->cols)) { + $cols = $lastPanel->cols; + + $cols[] = [[(object) ['name' => $field]]]; + + $lastPanel->cols = $cols; + } else { + $rows = $lastPanel->rows ?? []; + + $rows[] = [(object) ['name' => $field], false]; + + $lastPanel->rows = $rows; + } + + $this->layoutManager->set($layoutData, $entityType, $layoutName); + $this->layoutManager->save(); + } + + /** + * @throws Error + */ + public function removeInDetail(string $entityType, string $field, string $layoutName): void + { + $panels = $this->getDetailLayout($entityType, $layoutName); + + $cell = $this->getCellFromDetail($panels, $field); + + if (!$cell) { + return; + } + + foreach ($panels as $panelItem) { + if (isset($panelItem->cols)) { + $rowItems =& $panelItem->cols; + } else if (isset($panelItem->rows)) { + $rowItems =& $panelItem->rows; + } else { + continue; + } + + if (!is_array($rowItems)) { + continue; + } + + foreach ($rowItems as &$rowItem) { + if (!is_array($rowItem)) { + continue; + } + + foreach ($rowItem as $i => $cellItem) { + if (!$cellItem instanceof stdClass) { + continue; + } + + if (($cellItem->name ?? null) === $field) { + $rowItem[$i] = false; + } + } + } + } + + $this->layoutManager->set($panels, $entityType, $layoutName); + $this->layoutManager->save(); + } + + /** + * @param array $panels + */ + private function hasInDetail(array $panels, string $field): bool + { + return $this->getCellFromDetail($panels, $field) !== null; + } + + /** + * @param array $panels + */ + private function getCellFromDetail(array $panels, string $field): ?stdClass + { + foreach ($panels as $panelItem) { + $rowItems = $panelItem->cols ?? $panelItem->rows ?? null; + + if (!is_array($rowItems)) { + continue; + } + + foreach ($rowItems as $rowItem) { + if (!is_array($rowItem)) { + continue; + } + + foreach ($rowItem as $cellItem) { + if (!$cellItem instanceof stdClass) { + continue; + } + + if (($cellItem->name ?? null) === $field) { + return $cellItem; + } + } + } + } + + return null; + } + + /** + * @return array + */ + private function getDetailLayout(string $entityType, string $layoutName): array + { + $layoutString = $this->layoutProvider->get($entityType, $layoutName); + + if (!$layoutString) { + $layoutString = '[]'; + } + + $layoutData = Json::decode($layoutString); + + if (!is_array($layoutData)) { + throw new RuntimeException("Bad layout $entityType.$layoutName."); + } + + return $layoutData; + } +} diff --git a/application/Espo/Tools/LayoutManager/LayoutName.php b/application/Espo/Tools/LayoutManager/LayoutName.php new file mode 100644 index 0000000000..d425d48610 --- /dev/null +++ b/application/Espo/Tools/LayoutManager/LayoutName.php @@ -0,0 +1,39 @@ +. + * + * 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\LayoutManager; + +/** + * @since 9.4.0 + */ +class LayoutName +{ + const string DETAIL = 'detail'; + const string DETAIL_SMALL = 'detailSmall'; +} diff --git a/client/src/views/admin/entity-manager/edit.js b/client/src/views/admin/entity-manager/edit.js index d2f99515f5..1bcb11a004 100644 --- a/client/src/views/admin/entity-manager/edit.js +++ b/client/src/views/admin/entity-manager/edit.js @@ -381,12 +381,15 @@ class EntityManagerEditView extends View { setupDefs() { const scope = this.scope; + const typeList = (this.getMetadata().get('app.entityTemplateList') || ['Base']) + .filter(it => !this.getMetadata().get(`app.entityTemplates.${it}.isNotCreatable`)); + const defs = { fields: { type: { type: 'enum', required: true, - options: this.getMetadata().get('app.entityTemplateList') || ['Base'], + options: typeList, readOnly: scope !== false, tooltip: true, },