mirror of
https://github.com/espocrm/espocrm.git
synced 2026-03-03 02:57:01 +00:00
Compare commits
3 Commits
33c8e202bc
...
c392fabc14
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c392fabc14 | ||
|
|
1de9046b3d | ||
|
|
b007bef366 |
@@ -32,4 +32,6 @@ namespace Espo\Core\Templates\Entities;
|
||||
use Espo\Core\Entities\CategoryTreeItem;
|
||||
|
||||
class CategoryTree extends CategoryTreeItem
|
||||
{}
|
||||
{
|
||||
public const string TEMPLATE_TYPE = 'CategoryTree';
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
"notifications": false,
|
||||
"customizable": false,
|
||||
"entityManager": {
|
||||
"fields": false,
|
||||
"formula": false,
|
||||
"relationships": false,
|
||||
"addField": false,
|
||||
"edit": false,
|
||||
"layouts": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"labels": {
|
||||
"Create {entityType}": "Create {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
[
|
||||
"Base",
|
||||
"BasePlus",
|
||||
"Event",
|
||||
"Person",
|
||||
"Company"
|
||||
]
|
||||
"Base",
|
||||
"BasePlus",
|
||||
"Event",
|
||||
"Person",
|
||||
"Company",
|
||||
"CategoryTree"
|
||||
]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,26 +29,16 @@
|
||||
|
||||
namespace Espo\Tools\EntityManager;
|
||||
|
||||
class CreateParams
|
||||
readonly class CreateParams
|
||||
{
|
||||
/**
|
||||
* @param array<string, string> $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<string, string>
|
||||
*/
|
||||
public function getReplaceData(): array
|
||||
{
|
||||
return $this->replaceData;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,11 +103,13 @@ class EntityManager
|
||||
/** @var array<string, mixed> $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.");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 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';
|
||||
}
|
||||
}
|
||||
189
application/Espo/Tools/LayoutManager/LayoutCustomizer.php
Normal file
189
application/Espo/Tools/LayoutManager/LayoutCustomizer.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 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<int, mixed> $panels
|
||||
*/
|
||||
private function hasInDetail(array $panels, string $field): bool
|
||||
{
|
||||
return $this->getCellFromDetail($panels, $field) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $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<int, mixed>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
39
application/Espo/Tools/LayoutManager/LayoutName.php
Normal file
39
application/Espo/Tools/LayoutManager/LayoutName.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 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';
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
77
package-lock.json
generated
77
package-lock.json
generated
@@ -2966,8 +2966,7 @@
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
||||
"dev": true
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||
},
|
||||
"node_modules/bare-events": {
|
||||
"version": "2.8.2",
|
||||
@@ -3133,7 +3132,6 @@
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -3523,8 +3521,7 @@
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||
"dev": true
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
@@ -5278,27 +5275,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/grunt/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/grunt/node_modules/brace-expansion": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
|
||||
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/grunt/node_modules/glob": {
|
||||
"version": "7.1.7",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
|
||||
@@ -5373,21 +5349,6 @@
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/grunt/node_modules/minimatch": {
|
||||
"version": "9.0.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz",
|
||||
"integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/gzip-size": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz",
|
||||
@@ -6517,7 +6478,6 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
|
||||
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
@@ -10537,8 +10497,7 @@
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
||||
"dev": true
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||
},
|
||||
"bare-events": {
|
||||
"version": "2.8.2",
|
||||
@@ -10651,7 +10610,6 @@
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -10897,8 +10855,7 @@
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||
"dev": true
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"content-disposition": {
|
||||
"version": "0.5.4",
|
||||
@@ -11984,23 +11941,10 @@
|
||||
"grunt-legacy-util": "~2.0.1",
|
||||
"iconv-lite": "~0.6.3",
|
||||
"js-yaml": "~3.14.0",
|
||||
"minimatch": "~9.0.6",
|
||||
"minimatch": "~3.1.3",
|
||||
"nopt": "~3.0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
|
||||
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
|
||||
"requires": {
|
||||
"balanced-match": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.7",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
|
||||
@@ -12009,7 +11953,7 @@
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "~9.0.6",
|
||||
"minimatch": "~3.1.3",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
@@ -12053,14 +11997,6 @@
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "9.0.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz",
|
||||
"integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==",
|
||||
"requires": {
|
||||
"brace-expansion": "^5.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -13019,7 +12955,6 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
|
||||
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
|
||||
@@ -104,8 +104,8 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"overrides": {
|
||||
"grunt": {
|
||||
"minimatch": "~9.0.6"
|
||||
}
|
||||
"grunt": {
|
||||
"minimatch": "~3.1.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user