diff --git a/application/Espo/Controllers/Layout.php b/application/Espo/Controllers/Layout.php index 891c68ebb5..2e9987a64a 100644 --- a/application/Espo/Controllers/Layout.php +++ b/application/Espo/Controllers/Layout.php @@ -30,62 +30,49 @@ namespace Espo\Controllers; use Espo\Core\Utils as Utils; -use \Espo\Core\Exceptions\NotFound; -use \Espo\Core\Exceptions\Error; -use \Espo\Core\Exceptions\Forbidden; -use \Espo\Core\Exceptions\BadRequest; +use Espo\Core\Exceptions\NotFound; +use Espo\Core\Exceptions\Error; +use Espo\Core\Exceptions\Forbidden; +use Espo\Core\Exceptions\BadRequest; class Layout extends \Espo\Core\Controllers\Base { - public function actionRead($params, $data) + public function getActionRead($params, $data) { - return $this->getServiceFactory()->create('Layout')->getForFrontend($params['scope'], $params['name']); + $scope = $params['scope'] ?? null; + $name = $params['name'] ?? null; + + return $this->getServiceFactory()->create('Layout')->getForFrontend($scope, $name); } - public function actionUpdate($params, $data, $request) + public function putActionUpdate($params, $data, $request) { - if (is_object($data)) { - $data = get_object_vars($data); - } + if (is_object($data)) $data = get_object_vars($data); - if (!$this->getUser()->isAdmin()) { - throw new Forbidden(); - } + if (!$this->getUser()->isAdmin()) throw new Forbidden(); - if (!$request->isPut() && !$request->isPatch()) { - throw new BadRequest(); - } + $scope = $params['scope'] ?? null; + $name = $params['name'] ?? null; + $setId = $params['setId'] ?? null; - $layoutManager = $this->getContainer()->get('layout'); - $layoutManager->set($data, $params['scope'], $params['name']); - $result = $layoutManager->save(); - - if ($result === false) { - throw new Error("Error while saving layout."); - } - - $this->getContainer()->get('dataManager')->updateCacheTimestamp(); - - return $layoutManager->get($params['scope'], $params['name']); - } - - public function actionPatch($params, $data, $request) - { - return $this->actionUpdate($params, $data, $request); + return $this->getServiceFactory()->create('Layout')->update($scope, $name, $setId, $data); } public function postActionResetToDefault($params, $data, $request) { - if (!$this->getUser()->isAdmin()) { - throw new Forbidden(); - } + if (!$this->getUser()->isAdmin()) throw new Forbidden(); - if (empty($data->scope) || empty($data->name)) { - throw new BadRequest(); - } + if (empty($data->scope) || empty($data->name)) throw new BadRequest(); - $this->getContainer()->get('dataManager')->updateCacheTimestamp(); + return $this->getServiceFactory()->create('Layout')->resetToDefault($data->scope, $data->name, $data->setId ?? null); + } - return $this->getContainer()->get('layout')->resetToDefault($data->scope, $data->name); + public function getActionGetOriginal($params, $data, $request) + { + if (!$this->getUser()->isAdmin()) throw new Forbidden(); + + return $this->getServiceFactory()->create('Layout')->getOriginal( + $request->get('scope'), $request->get('name'), $request->get('setId') + ); } } diff --git a/application/Espo/Controllers/LayoutSet.php b/application/Espo/Controllers/LayoutSet.php new file mode 100644 index 0000000000..b81bcfa309 --- /dev/null +++ b/application/Espo/Controllers/LayoutSet.php @@ -0,0 +1,42 @@ +getUser()->isAdmin()) { + throw new Forbidden(); + } + } +} diff --git a/application/Espo/Core/SelectManagers/Base.php b/application/Espo/Core/SelectManagers/Base.php index 62a51e7947..ab107de181 100644 --- a/application/Espo/Core/SelectManagers/Base.php +++ b/application/Espo/Core/SelectManagers/Base.php @@ -1863,6 +1863,7 @@ class Base $method = 'filter' . ucfirst($filter); if (method_exists($this, $method)) { $this->$method($result); + return; } else { $className = $this->getMetadata()->get(['entityDefs', $this->entityType, 'collection', 'filters', $filter, 'className']); if ($className) { @@ -1877,7 +1878,10 @@ class Base } $impl->applyFilter($this->entityType, $filter, $result, $this); } + return; } + + $result['whereClause'][] = ['id' => null]; } public function applyFilter(string $filter, array &$result) diff --git a/application/Espo/Core/Utils/Database/Orm/Fields/Currency.php b/application/Espo/Core/Utils/Database/Orm/Fields/Currency.php index 15c18777a7..758ed08faf 100644 --- a/application/Espo/Core/Utils/Database/Orm/Fields/Currency.php +++ b/application/Espo/Core/Utils/Database/Orm/Fields/Currency.php @@ -99,6 +99,7 @@ class Currency extends Base 'orderBy' => [ 'sql' => $converedFieldName . " {direction}", 'leftJoins' => $leftJoins, + 'additionalSelect' => ["{$alias}.rate"], ], 'attributeRole' => 'valueConverted', 'fieldType' => 'currency', @@ -107,6 +108,7 @@ class Currency extends Base $defs[$entityType]['fields'][$fieldName]['orderBy'] = [ 'sql' => $part . " * {$alias}.rate {direction}", 'leftJoins' => $leftJoins, + 'additionalSelect' => ["{$alias}.rate"], ]; } diff --git a/application/Espo/Entities/LayoutRecord.php b/application/Espo/Entities/LayoutRecord.php new file mode 100644 index 0000000000..07193e0cd1 --- /dev/null +++ b/application/Espo/Entities/LayoutRecord.php @@ -0,0 +1,34 @@ +colorList) / 128); - return $this->colorList[$index]; + $colorList = $this->getMetadata()->get(['app', 'avatars', 'colorList']) ?? $this->colorList; + + $index = intval($x * count($colorList) / 128); + return $colorList[$index]; } public function run() @@ -111,7 +113,7 @@ class Avatar extends Image $hash = $userId; $color = $this->getColor($userId); if ($hash === 'system') { - $color = $this->systemColor; + $color = $this->getMetadata()->get(['app', 'avatars', 'systemColor']) ?? $this->systemColor; } $imgContent = $identicon->getImageData($hash, $width, $color); @@ -120,6 +122,4 @@ class Avatar extends Image } } } - } - diff --git a/application/Espo/ORM/DB/Query/Base.php b/application/Espo/ORM/DB/Query/Base.php index 85f0d7e0ae..581e4fe547 100644 --- a/application/Espo/ORM/DB/Query/Base.php +++ b/application/Espo/ORM/DB/Query/Base.php @@ -308,6 +308,7 @@ abstract class Base if (empty($params['customJoin'])) { $params['customJoin'] = ''; } + $params['additionalSelect'] = $params['additionalSelect'] ?? []; $wherePart = $this->getWhere($entity, $whereClause, 'AND', $params); @@ -318,9 +319,29 @@ abstract class Base } if (empty($params['aggregation'])) { - $selectPart = $this->getSelect($entity, $params['select'], $params['distinct'], $params['skipTextColumns'], $params['maxTextColumnsLength'], $params); + $selectPart = $this->getSelect( + $entity, $params['select'], $params['distinct'], $params['skipTextColumns'], $params['maxTextColumnsLength'], $params + ); + $orderPart = $this->getOrder($entity, $params['orderBy'], $params['order'], $params); + if (!empty($params['extraAdditionalSelect'])) { + $extraSelect = []; + foreach ($params['extraAdditionalSelect'] as $item) { + if (!in_array($item, $params['select']) && !in_array($item, $params['additionalSelect'])) { + $extraSelect[] = $item; + } + } + if (count($extraSelect)) { + $extraSelectPart = $this->getSelect( + $entity, $extraSelect, false + ); + if ($extraSelectPart) { + $selectPart .= ', ' . $extraSelectPart; + } + } + } + if (!empty($params['additionalColumns']) && is_array($params['additionalColumns']) && !empty($params['relationName'])) { foreach ($params['additionalColumns'] as $column => $field) { $itemAlias = $this->sanitizeSelectAlias($field); @@ -955,6 +976,16 @@ abstract class Base $params['joins'][] = $j; } } + + if (!empty($fieldDefs[$type]['additionalSelect'])) { + $params['extraAdditionalSelect'] = $params['extraAdditionalSelect'] ?? []; + foreach ($fieldDefs[$type]['additionalSelect'] as $value) { + $value = str_replace('{alias}', $alias, $value); + if (!in_array($value, $params['extraAdditionalSelect'])) { + $params['extraAdditionalSelect'][] = $value; + } + } + } } return $part; diff --git a/application/Espo/Repositories/LayoutSet.php b/application/Espo/Repositories/LayoutSet.php new file mode 100644 index 0000000000..889356798e --- /dev/null +++ b/application/Espo/Repositories/LayoutSet.php @@ -0,0 +1,68 @@ +isNew() && $entity->has('layoutList')) { + $listBefore = $entity->getFetched('layoutList') ?? []; + $listNow = $entity->get('layoutList') ?? []; + + foreach ($listBefore as $name) { + if (!in_array($name, $listNow)) { + $layout = $this->getEntityManager()->getRepository('LayoutRecord')->where([ + 'layoutSetId' => $entity->id, + 'name' => $name, + ])->findOne(); + if ($layout) { + $this->getEntityManager()->removeEntity($layout); + } + } + } + } + } + + protected function afterRemove(Entity $entity, array $options = []) + { + $layoutList = $this->getEntityManager()->getRepository('LayoutRecord')->where([ + 'layoutSetId' => $entity->id, + ])->find(); + + foreach ($layoutList as $layout) { + $this->getEntityManager()->removeEntity($layout); + } + } +} diff --git a/application/Espo/Resources/i18n/en_US/Admin.json b/application/Espo/Resources/i18n/en_US/Admin.json index 2da479c703..1d171c4829 100644 --- a/application/Espo/Resources/i18n/en_US/Admin.json +++ b/application/Espo/Resources/i18n/en_US/Admin.json @@ -71,6 +71,7 @@ "Permissions": "Permissions", "Email Addresses": "Email Addresses", "Phone Numbers": "Phone Numbers", + "Layout Sets": "Layout Sets", "Success": "Success", "Fail": "Fail", "is recommended": "is recommended", @@ -262,6 +263,7 @@ "emailAddresses": "All emailes addresses stored in the system.", "phoneNumbers": "All phone numbers stored in the system.", "dashboardTemplates": "Deploy dashboards to users.", + "layoutSets": "Collections of layouts that can be assigned to teams & portals.", "pdfTemplates": "Templates for printing to PDF." }, "options": { diff --git a/application/Espo/Resources/i18n/en_US/Global.json b/application/Espo/Resources/i18n/en_US/Global.json index 8c7960eba1..f3c6883a5a 100644 --- a/application/Espo/Resources/i18n/en_US/Global.json +++ b/application/Espo/Resources/i18n/en_US/Global.json @@ -51,6 +51,7 @@ "ArrayValue": "Array Value", "DashboardTemplate": "Dashboard Template", "Currency": "Currency", + "LayoutSet": "Layout Set", "Webhook": "Webhook" }, "scopeNamesPlural": { @@ -94,6 +95,7 @@ "ArrayValue": "Array Values", "DashboardTemplate": "Dashboard Templates", "Currency": "Currency", + "LayoutSet": "Layout Sets", "Webhook": "Webhooks" }, "labels": { diff --git a/application/Espo/Resources/i18n/en_US/LayoutSet.json b/application/Espo/Resources/i18n/en_US/LayoutSet.json new file mode 100644 index 0000000000..e3349e4bdb --- /dev/null +++ b/application/Espo/Resources/i18n/en_US/LayoutSet.json @@ -0,0 +1,11 @@ +{ + "fields": { + "layoutList": "Layouts" + }, + "labels": { + "Create LayoutSet": "Create Layout Set", + "Edit Layouts": "Edit Layouts" + }, + "tooltips": { + } +} diff --git a/application/Espo/Resources/i18n/en_US/Portal.json b/application/Espo/Resources/i18n/en_US/Portal.json index b4c0f36fc0..69568d4a83 100644 --- a/application/Espo/Resources/i18n/en_US/Portal.json +++ b/application/Espo/Resources/i18n/en_US/Portal.json @@ -17,12 +17,14 @@ "timeZone": "Time Zone", "weekStart": "First Day of Week", "defaultCurrency": "Default Currency", + "layoutSet": "Layout Set", "customUrl": "Custom URL", "customId": "Custom ID" }, "links": { "users": "Users", "portalRoles": "Roles", + "layoutSet": "Layout Set", "notes": "Notes" }, "tooltips": { diff --git a/application/Espo/Resources/i18n/en_US/Team.json b/application/Espo/Resources/i18n/en_US/Team.json index f737aa2f5e..792e232c22 100644 --- a/application/Espo/Resources/i18n/en_US/Team.json +++ b/application/Espo/Resources/i18n/en_US/Team.json @@ -2,12 +2,14 @@ "fields": { "name": "Name", "roles": "Roles", + "layoutSet": "Layout Set", "positionList": "Position List" }, "links": { "users": "Users", "notes": "Notes", "roles": "Roles", + "layoutSet": "Layout Set", "inboundEmails": "Group Email Accounts" }, "tooltips": { diff --git a/application/Espo/Resources/layouts/LayoutSet/detail.json b/application/Espo/Resources/layouts/LayoutSet/detail.json new file mode 100644 index 0000000000..9f83b8fe73 --- /dev/null +++ b/application/Espo/Resources/layouts/LayoutSet/detail.json @@ -0,0 +1,17 @@ +[ + { + "label": "", + "rows": [ + [{"name": "name"}, false], + [ + {"name": "layoutList"}, + { + "name": "edit", + "customLabel": "", + "view": "views/layout-set/fields/edit", + "inlineEditDisabled": true + } + ] + ] + } +] diff --git a/application/Espo/Resources/layouts/LayoutSet/detailSmall.json b/application/Espo/Resources/layouts/LayoutSet/detailSmall.json new file mode 100644 index 0000000000..b119d9e328 --- /dev/null +++ b/application/Espo/Resources/layouts/LayoutSet/detailSmall.json @@ -0,0 +1,8 @@ +[ + { + "label": "", + "rows": [ + [{"name": "name"}] + ] + } +] diff --git a/application/Espo/Resources/layouts/LayoutSet/list.json b/application/Espo/Resources/layouts/LayoutSet/list.json new file mode 100644 index 0000000000..2ae42ee989 --- /dev/null +++ b/application/Espo/Resources/layouts/LayoutSet/list.json @@ -0,0 +1,3 @@ +[ + {"name":"name", "link": true} +] diff --git a/application/Espo/Resources/layouts/LayoutSet/relationships.json b/application/Espo/Resources/layouts/LayoutSet/relationships.json new file mode 100644 index 0000000000..9b3453caf9 --- /dev/null +++ b/application/Espo/Resources/layouts/LayoutSet/relationships.json @@ -0,0 +1,3 @@ +[ + "teams" +] diff --git a/application/Espo/Resources/layouts/Portal/detail.json b/application/Espo/Resources/layouts/Portal/detail.json index 690516b898..cb0fb341c7 100644 --- a/application/Espo/Resources/layouts/Portal/detail.json +++ b/application/Espo/Resources/layouts/Portal/detail.json @@ -20,6 +20,7 @@ "label": "User Interface", "rows": [ [{"name": "companyLogo"}, {"name": "theme"}], + [{"name": "layoutSet"}, false], [{"name": "tabList"}, {"name": "quickCreateList"}], [{"name": "dashboardLayout", "fullWidth": true}] ] diff --git a/application/Espo/Resources/layouts/Team/detail.json b/application/Espo/Resources/layouts/Team/detail.json index 63675851e7..f8888defaf 100644 --- a/application/Espo/Resources/layouts/Team/detail.json +++ b/application/Espo/Resources/layouts/Team/detail.json @@ -2,11 +2,16 @@ { "rows": [ [ - {"name": "name"} + {"name": "name"}, + false ], [ {"name": "roles"}, {"name": "positionList"} + ], + [ + {"name": "layoutSet"}, + false ] ] } diff --git a/application/Espo/Resources/metadata/app/adminPanel.json b/application/Espo/Resources/metadata/app/adminPanel.json index 63dfa905e6..acda6c07c6 100644 --- a/application/Espo/Resources/metadata/app/adminPanel.json +++ b/application/Espo/Resources/metadata/app/adminPanel.json @@ -257,6 +257,13 @@ "iconClass": "fas fa-th-large", "description": "dashboardTemplates" }, + { + "url": "#LayoutSet", + "label": "Layout Sets", + "iconClass": "fas fa-table", + "description": "layoutSets" + + }, { "url": "#Attachment", "label": "Attachments", diff --git a/application/Espo/Resources/metadata/clientDefs/LayoutSet.json b/application/Espo/Resources/metadata/clientDefs/LayoutSet.json new file mode 100644 index 0000000000..37e746e4be --- /dev/null +++ b/application/Espo/Resources/metadata/clientDefs/LayoutSet.json @@ -0,0 +1,12 @@ +{ + "controller": "controllers/layout-set", + "searchPanelDisabled": true, + "duplicateDisabled": true, + "relationshipPanels": { + "teams": { + "createDisabled": true, + "viewDisabled": true, + "rowActionsView": "views/record/row-actions/relationship-unlink-only" + } + } +} diff --git a/application/Espo/Resources/metadata/entityDefs/LayoutRecord.json b/application/Espo/Resources/metadata/entityDefs/LayoutRecord.json new file mode 100644 index 0000000000..845a741b7a --- /dev/null +++ b/application/Espo/Resources/metadata/entityDefs/LayoutRecord.json @@ -0,0 +1,25 @@ +{ + "fields": { + "name": { + "type": "varchar" + }, + "layoutSet": { + "type": "link" + }, + "data": { + "type": "jsonObject" + } + }, + "links": { + "layoutSet": { + "type": "belengsTo", + "entity": "LayoutSet", + "foreign": "layoutRecords" + } + }, + "indexes": { + "nameLayoutSetId": { + "columns": ["name", "layoutSetId"] + } + } +} diff --git a/application/Espo/Resources/metadata/entityDefs/LayoutSet.json b/application/Espo/Resources/metadata/entityDefs/LayoutSet.json new file mode 100644 index 0000000000..c7440379c4 --- /dev/null +++ b/application/Espo/Resources/metadata/entityDefs/LayoutSet.json @@ -0,0 +1,44 @@ +{ + "fields": { + "name": { + "type": "varchar", + "required": true, + "maxLength": 100, + "trim": true + }, + "layoutList": { + "type": "multiEnum", + "displayAsList": true, + "view": "views/layout-set/fields/layout-list" + }, + "createdAt": { + "type": "datetime", + "readOnly": true + }, + "modifiedAt": { + "type": "datetime", + "readOnly": true + } + }, + "links": { + "layoutRecords": { + "type": "hasMany", + "entity": "LayoutRecord", + "foreign": "layoutSet" + }, + "teams": { + "type": "hasMany", + "entity": "Team", + "foreign": "layoutSet" + }, + "portals": { + "type": "hasMany", + "entity": "Portal", + "foreign": "layoutSet" + } + }, + "collection": { + "orderBy": "name", + "order": "asc" + } +} diff --git a/application/Espo/Resources/metadata/entityDefs/Portal.json b/application/Espo/Resources/metadata/entityDefs/Portal.json index 02632096d2..c82c8d4feb 100644 --- a/application/Espo/Resources/metadata/entityDefs/Portal.json +++ b/application/Espo/Resources/metadata/entityDefs/Portal.json @@ -94,6 +94,9 @@ "customUrl": { "type": "url" }, + "layoutSet": { + "type": "link" + }, "modifiedAt": { "type": "datetime", "readOnly": true @@ -136,6 +139,11 @@ "type": "hasMany", "entity": "Note", "foreign": "portals" + }, + "layoutSet": { + "type": "belongsTo", + "entity": "LayoutSet", + "foreign": "portals" } }, "collection": { diff --git a/application/Espo/Resources/metadata/entityDefs/Team.json b/application/Espo/Resources/metadata/entityDefs/Team.json index 78e47398cd..4289babb34 100644 --- a/application/Espo/Resources/metadata/entityDefs/Team.json +++ b/application/Espo/Resources/metadata/entityDefs/Team.json @@ -18,6 +18,9 @@ "notStorable": true, "disabled": true }, + "layoutSet": { + "type": "link" + }, "createdAt": { "type": "datetime", "readOnly": true @@ -47,6 +50,11 @@ "type": "hasMany", "entity": "InboundEmail", "foreign": "teams" + }, + "layoutSet": { + "type": "belongsTo", + "entity": "LayoutSet", + "foreign": "teams" } }, "collection": { diff --git a/application/Espo/Resources/metadata/scopes/LayoutRecord.json b/application/Espo/Resources/metadata/scopes/LayoutRecord.json new file mode 100644 index 0000000000..39c96e7ac2 --- /dev/null +++ b/application/Espo/Resources/metadata/scopes/LayoutRecord.json @@ -0,0 +1,3 @@ +{ + "entity": true +} diff --git a/application/Espo/Resources/metadata/scopes/LayoutSet.json b/application/Espo/Resources/metadata/scopes/LayoutSet.json new file mode 100644 index 0000000000..39c96e7ac2 --- /dev/null +++ b/application/Espo/Resources/metadata/scopes/LayoutSet.json @@ -0,0 +1,3 @@ +{ + "entity": true +} diff --git a/application/Espo/Resources/routes.json b/application/Espo/Resources/routes.json index 33cca66201..c664ab2a38 100644 --- a/application/Espo/Resources/routes.json +++ b/application/Espo/Resources/routes.json @@ -157,8 +157,8 @@ } }, { - "route": "/:controller/layout/:name", - "method": "patch", + "route": "/:controller/layout/:name/:setId", + "method": "put", "params": { "controller": "Layout", "scope": ":controller" diff --git a/application/Espo/Services/Layout.php b/application/Espo/Services/Layout.php index 4c96e26bde..bd9ea6f097 100644 --- a/application/Espo/Services/Layout.php +++ b/application/Espo/Services/Layout.php @@ -30,6 +30,7 @@ namespace Espo\Services; use Espo\Core\Exceptions\NotFound; +use Espo\Core\Exceptions\Error; class Layout extends \Espo\Core\Services\Base { @@ -37,7 +38,10 @@ class Layout extends \Espo\Core\Services\Base { $this->addDependency('acl'); $this->addDependency('layout'); + $this->addDependency('entityManager'); $this->addDependency('metadata'); + $this->addDependency('dataManager'); + $this->addDependency('user'); } protected function getAcl() @@ -52,10 +56,53 @@ class Layout extends \Espo\Core\Services\Base public function getForFrontend(string $scope, string $name) { - $dataString = $this->getInjection('layout')->get($scope, $name); + $layoutSetId = null; + $data = null; + + $em = $this->getInjection('entityManager'); + $user = $this->getInjection('user'); + + if ($user->isPortal()) { + $portalId = $user->get('portalId'); + if ($portalId) { + $portal = $em->getRepository('Portal')->select(['layoutSetId'])->where(['id' => $portalId])->findOne(); + if ($portal) { + $layoutSetId = $portal->get('layoutSetId'); + } + } + } else { + $teamId = $user->get('defaultTeamId'); + if ($teamId) { + $team = $em->getRepository('Team')->select(['layoutSetId'])->where(['id' => $teamId])->findOne(); + if ($team) { + $layoutSetId = $team->get('layoutSetId'); + } + } + } + + if ($layoutSetId) { + $nameReal = $name; + + if ($user->isPortal()) { + if (substr($name, -6) === 'Portal') { + $nameReal = substr($name, 0, -6); + } + } + + $layout = $this->getRecordFromSet($scope, $nameReal, $layoutSetId, true); + if ($layout) { + $data = $layout->get('data'); + } + } + + if (!$data) { + $dataString = $this->getInjection('layout')->get($scope, $name); + } else { + $dataString = json_encode($data); + } if (!$dataString) { - throw new NotFound("Layout {$scope}:{$scope} is not found."); + throw new NotFound("Layout {$scope}:{$name} is not found."); } if (!$this->getUser()->isAdmin()) { @@ -80,4 +127,90 @@ class Layout extends \Espo\Core\Services\Base return $dataString; } + + protected function getRecordFromSet(string $scope, string $name, string $setId, bool $skipCheck = false) + { + $em = $this->getInjection('entityManager'); + $layoutSet = $em->getEntity('LayoutSet', $setId); + if (!$layoutSet) throw new NotFound(); + + $layoutList = $layoutSet->get('layoutList') ?? []; + + $fullName = $scope . '.' . $name; + + if (!in_array($fullName, $layoutList)) { + if ($skipCheck) return null; + throw new NotFound("Layout {$fullName} is no allowed in set."); + } + + $layout = $em->getRepository('LayoutRecord')->where([ + 'layoutSetId' => $setId, + 'name' => $fullName, + ])->findOne(); + + return $layout; + } + + public function update(string $scope, string $name, ?string $setId, $data) + { + if ($setId) { + $layout = $this->getRecordFromSet($scope, $name, $setId); + + $em = $this->getInjection('entityManager'); + + if (!$layout) { + $layout = $em->getEntity('LayoutRecord'); + $layout->set([ + 'layoutSetId' => $setId, + 'name' => $scope . '.' . $name, + ]); + } + + $layout->set('data', $data); + + $em->saveEntity($layout); + + return $layout->get('data'); + } + + $layoutManager = $this->getInjection('layout'); + + $layoutManager->set($data, $scope, $name); + $result = $layoutManager->save(); + + if ($result === false) throw new Error("Error while saving layout."); + + $this->getInjection('dataManager')->updateCacheTimestamp(); + + return $layoutManager->get($scope, $name); + } + + public function resetToDefault(string $scope, string $name, ?string $setId = null) + { + $this->getInjection('dataManager')->updateCacheTimestamp(); + + if ($setId) { + $layout = $this->getRecordFromSet($scope, $name, $setId); + if ($layout) { + $em = $this->getInjection('entityManager'); + $em->removeEntity($layout); + } + return $this->getInjection('layout')->get($scope, $name); + } + + return $this->getInjection('layout')->resetToDefault($scope, $name); + } + + public function getOriginal(string $scope, string $name, ?string $setId = null) + { + $this->getInjection('dataManager')->updateCacheTimestamp(); + + if ($setId) { + $layout = $this->getRecordFromSet($scope, $name, $setId, true); + if ($layout) { + return $layout->get('data'); + } + } + return $this->getInjection('layout')->get($scope, $name); + } } diff --git a/client/res/templates/admin/layouts/index.tpl b/client/res/templates/admin/layouts/index.tpl index b326a164d6..7231af5950 100644 --- a/client/res/templates/admin/layouts/index.tpl +++ b/client/res/templates/admin/layouts/index.tpl @@ -1,6 +1,4 @@ - +
diff --git a/client/res/templates/fields/multi-enum/edit.tpl b/client/res/templates/fields/multi-enum/edit.tpl index 3f0831145a..eae74d875a 100644 --- a/client/res/templates/fields/multi-enum/edit.tpl +++ b/client/res/templates/fields/multi-enum/edit.tpl @@ -1,2 +1,2 @@ - + diff --git a/client/res/templates/lead-capture/opt-in-confirmation-expired.tpl b/client/res/templates/lead-capture/opt-in-confirmation-expired.tpl index eab4ae6368..3ffd1f6dee 100644 --- a/client/res/templates/lead-capture/opt-in-confirmation-expired.tpl +++ b/client/res/templates/lead-capture/opt-in-confirmation-expired.tpl @@ -1,5 +1,5 @@
-
+

diff --git a/client/res/templates/lead-capture/opt-in-confirmation-success.tpl b/client/res/templates/lead-capture/opt-in-confirmation-success.tpl index ea86885f14..222249b66b 100644 --- a/client/res/templates/lead-capture/opt-in-confirmation-success.tpl +++ b/client/res/templates/lead-capture/opt-in-confirmation-success.tpl @@ -1,5 +1,5 @@

-
+
{{#if messageField}} diff --git a/client/src/controllers/layout-set.js b/client/src/controllers/layout-set.js new file mode 100644 index 0000000000..287e060cdc --- /dev/null +++ b/client/src/controllers/layout-set.js @@ -0,0 +1,45 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014-2020 Yuri Kuznetsov, Taras Machyshyn, Oleksiy 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('controllers/layout-set', 'controllers/record', function (Dep) { + + return Dep.extend({ + + actionEditLayouts: function (options) { + var id = options.id; + if (!id) throw new Error("ID not passed."); + + this.main('views/layout-set/layouts', { + layoutSetId: id, + scope: options.scope, + type: options.type, + }); + }, + + }); +}); diff --git a/client/src/layout-manager.js b/client/src/layout-manager.js index 371a0b25ab..afb9e82d84 100644 --- a/client/src/layout-manager.js +++ b/client/src/layout-manager.js @@ -50,8 +50,12 @@ define('layout-manager', [], function () { return this.applicationId + '-' + scope + '-' + type; }, - getUrl: function (scope, type) { - return scope + '/layout/' + type; + getUrl: function (scope, type, setId) { + var url = scope + '/layout/' + type; + if (setId) { + url += '/' + setId; + } + return url; }, get: function (scope, type, callback, cache) { @@ -81,11 +85,8 @@ define('layout-manager', [], function () { } } - this.ajax({ - url: this.getUrl(scope, type), - type: 'GET', - dataType: 'json', - success: function (layout) { + Espo.Ajax.getRequest(this.getUrl(scope, type)).then( + function (layout) { if (typeof callback === 'function') { callback(layout); } @@ -94,54 +95,61 @@ define('layout-manager', [], function () { this.cache.set('app-layout', key, layout); } }.bind(this) - }); + ); }, - set: function (scope, type, layout, callback) { - var key = this.getKey(scope, type); + getOriginal: function (scope, type, setId, callback) { + var url = 'Layout/action/getOriginal?scope='+scope+'&name='+type; + if (setId) url += '&setId='+setId; - this.ajax({ - url: this.getUrl(scope, type), - type: 'PUT', - data: JSON.stringify(layout), - success: function () { - if (this.cache && key) { - this.cache.set('app-layout', key, layout); + Espo.Ajax.getRequest(url).then( + function (layout) { + if (typeof callback === 'function') { + callback(layout); } - this.data[key] = layout; + }.bind(this) + ); + }, + + set: function (scope, type, layout, callback, setId) { + Espo.Ajax.putRequest(this.getUrl(scope, type, setId), layout).then( + function () { + var key = this.getKey(scope, type); + if (this.cache && key) { + this.cache.clear('app-layout', key); + } + delete this.data[key]; this.trigger('sync'); + if (typeof callback === 'function') { callback(); } }.bind(this) - }); + ); }, - resetToDefault: function (scope, type, callback) { - var key = this.getKey(scope, type); - - this.ajax({ - url: 'Layout/action/resetToDefault', - type: 'POST', - data: JSON.stringify({ - scope: scope, - name: type - }), - success: function (layout) { + resetToDefault: function (scope, type, callback, setId) { + Espo.Ajax.postRequest('Layout/action/resetToDefault', { + scope: scope, + name: type, + setId: setId, + }).then( + function (layout) { + var key = this.getKey(scope, type); if (this.cache) { this.cache.clear('app-layout', key); } - this.data[key] = layout; + delete this.data[key]; this.trigger('sync'); + if (typeof callback === 'function') { callback(); } }.bind(this) - }); - } + ); + }, }, Backbone.Events); return LayoutManager; - }); diff --git a/client/src/views/admin/layouts/base.js b/client/src/views/admin/layouts/base.js index 897d9a4691..3c937fb472 100644 --- a/client/src/views/admin/layouts/base.js +++ b/client/src/views/admin/layouts/base.js @@ -26,7 +26,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -Espo.define('views/admin/layouts/base', 'view', function (Dep) { +define('views/admin/layouts/base', 'view', function (Dep) { return Dep.extend({ @@ -92,13 +92,13 @@ Espo.define('views/admin/layouts/base', 'view', function (Dep) { if (typeof callback == 'function') { callback(); } - }.bind(this)); + }.bind(this), this.setId); }, resetToDefault: function () { this.getHelper().layoutManager.resetToDefault(this.scope, this.type, function () { this.cancel(); - }.bind(this)); + }.bind(this), this.options.setId); }, reset: function () { @@ -112,6 +112,7 @@ Espo.define('views/admin/layouts/base', 'view', function (Dep) { this.events = _.clone(this.events); this.scope = this.options.scope; this.type = this.options.type; + this.setId = this.options.setId; this.dataAttributeList = this.getMetadata().get(['clientDefs', this.scope, 'additionalLayouts', this.type, 'dataAttributeList']) diff --git a/client/src/views/admin/layouts/default-side-panel.js b/client/src/views/admin/layouts/default-side-panel.js index dade2048a0..2e437c8c86 100644 --- a/client/src/views/admin/layouts/default-side-panel.js +++ b/client/src/views/admin/layouts/default-side-panel.js @@ -66,12 +66,12 @@ define('views/admin/layouts/default-side-panel', 'views/admin/layouts/rows', fun loadLayout: function (callback) { this.getModelFactory().create(Espo.Utils.hyphenToUpperCamelCase(this.scope), function (model) { - this.getHelper().layoutManager.get(this.scope, this.type, function (layout) { + this.getHelper().layoutManager.getOriginal(this.scope, this.type, this.setId, function (layout) { this.readDataFromLayout(model, layout); if (callback) { callback(); } - }.bind(this), false); + }.bind(this)); }.bind(this)); }, diff --git a/client/src/views/admin/layouts/detail-convert.js b/client/src/views/admin/layouts/detail-convert.js index f614eac985..d9ed508e98 100644 --- a/client/src/views/admin/layouts/detail-convert.js +++ b/client/src/views/admin/layouts/detail-convert.js @@ -26,10 +26,9 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -Espo.define('views/admin/layouts/detail-convert', 'views/admin/layouts/detail', function (Dep) { +define('views/admin/layouts/detail-convert', 'views/admin/layouts/detail', function (Dep) { return Dep.extend({ }); }); - diff --git a/client/src/views/admin/layouts/detail-small.js b/client/src/views/admin/layouts/detail-small.js index ae8b718edc..f142ee7089 100644 --- a/client/src/views/admin/layouts/detail-small.js +++ b/client/src/views/admin/layouts/detail-small.js @@ -26,7 +26,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -Espo.define('views/admin/layouts/detail-small', 'views/admin/layouts/detail', function (Dep) { +define('views/admin/layouts/detail-small', 'views/admin/layouts/detail', function (Dep) { return Dep.extend({ @@ -34,4 +34,3 @@ Espo.define('views/admin/layouts/detail-small', 'views/admin/layouts/detail', fu }); }); - diff --git a/client/src/views/admin/layouts/detail.js b/client/src/views/admin/layouts/detail.js index ee5ef2d647..06726e285e 100644 --- a/client/src/views/admin/layouts/detail.js +++ b/client/src/views/admin/layouts/detail.js @@ -26,7 +26,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -Espo.define('views/admin/layouts/detail', 'views/admin/layouts/grid', function (Dep) { +define('views/admin/layouts/detail', 'views/admin/layouts/grid', function (Dep) { return Dep.extend({ @@ -91,7 +91,7 @@ Espo.define('views/admin/layouts/detail', 'views/admin/layouts/grid', function ( promiseList.push( new Promise(function (resolve) { this.getModelFactory().create(this.scope, function (m) { - this.getHelper().layoutManager.get(this.scope, this.type, function (layoutLoaded) { + this.getHelper().layoutManager.getOriginal(this.scope, this.type, this.setId, function (layoutLoaded) { layout = layoutLoaded; model = m; resolve(); @@ -103,10 +103,14 @@ Espo.define('views/admin/layouts/detail', 'views/admin/layouts/grid', function ( if (~['detail', 'detailSmall'].indexOf(this.type)) { promiseList.push( new Promise(function (resolve) { - this.getHelper().layoutManager.get(this.scope, 'sidePanels' + Espo.Utils.upperCaseFirst(this.type), function (layoutLoaded) { - this.sidePanelsLayout = layoutLoaded; - resolve(); - }.bind(this)); + this.getHelper().layoutManager.getOriginal( + this.scope, 'sidePanels' + Espo.Utils.upperCaseFirst(this.type), + this.setId, + function (layoutLoaded) { + this.sidePanelsLayout = layoutLoaded; + resolve(); + }.bind(this) + ); }.bind(this)) ); } @@ -116,24 +120,26 @@ Espo.define('views/admin/layouts/detail', 'views/admin/layouts/grid', function ( function (resolve) { if (this.getMetadata().get(['clientDefs', scope, 'layoutDefaultSidePanelDisabled'])) resolve(); - this.getHelper().layoutManager.get(this.scope, 'defaultSidePanel', function (layoutLoaded) { - this.defaultSidePanelLayout = layoutLoaded; + this.getHelper().layoutManager.getOriginal(this.scope, 'defaultSidePanel', this.setId, + function (layoutLoaded) { + this.defaultSidePanelLayout = layoutLoaded; - this.defaultPanelFieldList = Espo.Utils.clone(this.defaultPanelFieldList); + this.defaultPanelFieldList = Espo.Utils.clone(this.defaultPanelFieldList); - layoutLoaded.forEach(function (item) { - var field = item.name; - if (!field) return; - if (field === ':assignedUser') { - field = 'assignedUser'; - } - if (!~this.defaultPanelFieldList.indexOf(field)) { - this.defaultPanelFieldList.push(field); - } - }, this); + layoutLoaded.forEach(function (item) { + var field = item.name; + if (!field) return; + if (field === ':assignedUser') { + field = 'assignedUser'; + } + if (!~this.defaultPanelFieldList.indexOf(field)) { + this.defaultPanelFieldList.push(field); + } + }, this); - resolve(); - }.bind(this)); + resolve(); + }.bind(this) + ); }.bind(this) ) ); diff --git a/client/src/views/admin/layouts/filters.js b/client/src/views/admin/layouts/filters.js index ce09dc48a5..2230e474d4 100644 --- a/client/src/views/admin/layouts/filters.js +++ b/client/src/views/admin/layouts/filters.js @@ -26,7 +26,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -Espo.define('views/admin/layouts/filters', 'views/admin/layouts/rows', function (Dep) { +define('views/admin/layouts/filters', 'views/admin/layouts/rows', function (Dep) { return Dep.extend({ @@ -47,7 +47,7 @@ Espo.define('views/admin/layouts/filters', 'views/admin/layouts/rows', function loadLayout: function (callback) { this.getModelFactory().create(this.scope, function (model) { - this.getHelper().layoutManager.get(this.scope, this.type, function (layout) { + this.getHelper().layoutManager.getOriginal(this.scope, this.type, this.setId, function (layout) { var allFields = []; for (var field in model.defs.fields) { @@ -86,7 +86,7 @@ Espo.define('views/admin/layouts/filters', 'views/admin/layouts/rows', function } callback(); - }.bind(this), false); + }.bind(this)); }.bind(this)); }, @@ -111,8 +111,7 @@ Espo.define('views/admin/layouts/filters', 'views/admin/layouts/rows', function return false; } return !model.getFieldParam(name, 'disabled') && !model.getFieldParam(name, 'layoutFiltersDisabled'); - } + }, }); }); - diff --git a/client/src/views/admin/layouts/index.js b/client/src/views/admin/layouts/index.js index 4a6af195c4..e2570d5140 100644 --- a/client/src/views/admin/layouts/index.js +++ b/client/src/views/admin/layouts/index.js @@ -58,6 +58,7 @@ define('views/admin/layouts/index', 'view', function (Dep) { typeList: this.typeList, scope: this.scope, layoutScopeDataList: this.getLayoutScopeDataList(), + headerHtml: this.getHeaderHtml(), }; }, @@ -125,7 +126,8 @@ define('views/admin/layouts/index', 'view', function (Dep) { }, this); this.on('after:render', function () { - $("#layouts-menu button[data-scope='" + this.options.scope + "'][data-type='" + this.options.type + "']").addClass('disabled'); + $("#layouts-menu button[data-scope='" + this.options.scope + "'][data-type='" + this.options.type + "']") + .addClass('disabled'); this.renderLayoutHeader(); if (!this.options.scope) { this.renderDefaultPage(); @@ -143,16 +145,17 @@ define('views/admin/layouts/index', 'view', function (Dep) { this.scope = scope; this.type = type; - this.getRouter().navigate('#Admin/layouts/scope=' + scope + '&type=' + type, {trigger: false}); + this.navigate(scope, type); this.notify('Loading...'); var typeReal = this.getMetadata().get('clientDefs.' + scope + '.additionalLayouts.' + type + '.type') || type; - this.createView('content', 'Admin.Layouts.' + Espo.Utils.upperCaseFirst(typeReal), { + this.createView('content', 'views/admin/layouts/' + Espo.Utils.camelCaseToHyphen(typeReal), { el: '#layout-content', scope: scope, type: type, + setId: this.setId, }, function (view) { this.renderLayoutHeader(); view.render(); @@ -161,6 +164,10 @@ define('views/admin/layouts/index', 'view', function (Dep) { }.bind(this)); }, + navigate: function (scope, type) { + this.getRouter().navigate('#Admin/layouts/scope=' + scope + '&type=' + type, {trigger: false}); + }, + renderDefaultPage: function () { $("#layout-header").html('').hide(); $("#layout-content").html(this.translate('selectLayout', 'messages', 'Admin')); @@ -179,7 +186,14 @@ define('views/admin/layouts/index', 'view', function (Dep) { updatePageTitle: function () { this.setPageTitle(this.getLanguage().translate('Layout Manager', 'labels', 'Admin')); }, + + getHeaderHtml: function () { + var separatorHtml = ''; + + var html = ""+this.translate('Administration')+" " + separatorHtml + ' ' + + this.translate('Layout Manager', 'labels', 'Admin'); + + return html; + }, }); }); - - diff --git a/client/src/views/admin/layouts/kanban.js b/client/src/views/admin/layouts/kanban.js index ea6f5337e5..168cfd8c1d 100644 --- a/client/src/views/admin/layouts/kanban.js +++ b/client/src/views/admin/layouts/kanban.js @@ -26,7 +26,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -Espo.define('views/admin/layouts/kanban', 'views/admin/layouts/list', function (Dep) { +define('views/admin/layouts/kanban', 'views/admin/layouts/list', function (Dep) { return Dep.extend({ @@ -54,7 +54,7 @@ Espo.define('views/admin/layouts/kanban', 'views/admin/layouts/list', function ( ignoreList: [], - ignoreTypeList: [] + ignoreTypeList: [], }); }); diff --git a/client/src/views/admin/layouts/list-small.js b/client/src/views/admin/layouts/list-small.js index 3c1fde3e08..e8c1dd2a8f 100644 --- a/client/src/views/admin/layouts/list-small.js +++ b/client/src/views/admin/layouts/list-small.js @@ -26,10 +26,9 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -Espo.define('views/admin/layouts/list-small', 'views/admin/layouts/list', function (Dep) { +define('views/admin/layouts/list-small', 'views/admin/layouts/list', function (Dep) { return Dep.extend({ + }); }); - - diff --git a/client/src/views/admin/layouts/list.js b/client/src/views/admin/layouts/list.js index 2af38de085..f5a513e4fb 100644 --- a/client/src/views/admin/layouts/list.js +++ b/client/src/views/admin/layouts/list.js @@ -26,7 +26,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -Espo.define('views/admin/layouts/list', 'views/admin/layouts/rows', function (Dep) { +define('views/admin/layouts/list', 'views/admin/layouts/rows', function (Dep) { return Dep.extend({ @@ -84,12 +84,12 @@ Espo.define('views/admin/layouts/list', 'views/admin/layouts/rows', function (De loadLayout: function (callback) { this.getModelFactory().create(Espo.Utils.hyphenToUpperCamelCase(this.scope), function (model) { - this.getHelper().layoutManager.get(this.scope, this.type, function (layout) { + this.getHelper().layoutManager.getOriginal(this.scope, this.type, this.setId, function (layout) { this.readDataFromLayout(model, layout); if (callback) { callback(); } - }.bind(this), false); + }.bind(this)); }.bind(this)); }, diff --git a/client/src/views/admin/layouts/mass-update.js b/client/src/views/admin/layouts/mass-update.js index 6b8b9b514b..f53cf8a924 100644 --- a/client/src/views/admin/layouts/mass-update.js +++ b/client/src/views/admin/layouts/mass-update.js @@ -26,7 +26,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -Espo.define('views/admin/layouts/mass-update', 'views/admin/layouts/rows', function (Dep) { +define('views/admin/layouts/mass-update', 'views/admin/layouts/rows', function (Dep) { return Dep.extend({ @@ -55,7 +55,7 @@ Espo.define('views/admin/layouts/mass-update', 'views/admin/layouts/rows', funct loadLayout: function (callback) { this.getModelFactory().create(this.scope, function (model) { - this.getHelper().layoutManager.get(this.scope, this.type, function (layout) { + this.getHelper().layoutManager.getOriginal(this.scope, this.type, this.setId, function (layout) { var allFields = []; for (var field in model.defs.fields) { @@ -97,7 +97,7 @@ Espo.define('views/admin/layouts/mass-update', 'views/admin/layouts/rows', funct } callback(); - }.bind(this), false); + }.bind(this)); }.bind(this)); }, @@ -125,7 +125,7 @@ Espo.define('views/admin/layouts/mass-update', 'views/admin/layouts/rows', funct if (layoutList && !~layoutList.indexOf(this.type)) return; return !model.getFieldParam(name, 'disabled') && !model.getFieldParam(name, 'layoutMassUpdateDisabled') && !model.getFieldParam(name, 'readOnly'); - } + }, }); }); diff --git a/client/src/views/admin/layouts/relationships.js b/client/src/views/admin/layouts/relationships.js index f071b463ac..230b243f18 100644 --- a/client/src/views/admin/layouts/relationships.js +++ b/client/src/views/admin/layouts/relationships.js @@ -26,7 +26,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -Espo.define('views/admin/layouts/relationships', 'views/admin/layouts/rows', function (Dep) { +define('views/admin/layouts/relationships', 'views/admin/layouts/rows', function (Dep) { return Dep.extend({ @@ -65,7 +65,7 @@ Espo.define('views/admin/layouts/relationships', 'views/admin/layouts/rows', fun loadLayout: function (callback) { this.getModelFactory().create(this.scope, function (model) { - this.getHelper().layoutManager.get(this.scope, this.type, function (layout) { + this.getHelper().layoutManager.getOriginal(this.scope, this.type, this.setId, function (layout) { var allFields = []; for (var field in model.defs.links) { @@ -125,7 +125,7 @@ Espo.define('views/admin/layouts/relationships', 'views/admin/layouts/rows', fun } callback(); - }.bind(this), false); + }.bind(this)); }.bind(this)); }, @@ -138,4 +138,3 @@ Espo.define('views/admin/layouts/relationships', 'views/admin/layouts/rows', fun } }); }); - diff --git a/client/src/views/admin/layouts/side-panels-detail-small.js b/client/src/views/admin/layouts/side-panels-detail-small.js index 9322377f5b..3cf6f6e191 100644 --- a/client/src/views/admin/layouts/side-panels-detail-small.js +++ b/client/src/views/admin/layouts/side-panels-detail-small.js @@ -26,13 +26,11 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -Espo.define('views/admin/layouts/side-panels-detail-small', 'views/admin/layouts/side-panels-detail', function (Dep) { +define('views/admin/layouts/side-panels-detail-small', 'views/admin/layouts/side-panels-detail', function (Dep) { return Dep.extend({ - viewType: 'detailSmall' + viewType: 'detailSmall', }); }); - - diff --git a/client/src/views/admin/layouts/side-panels-detail.js b/client/src/views/admin/layouts/side-panels-detail.js index f1ae3e6765..5e1cd64a1c 100644 --- a/client/src/views/admin/layouts/side-panels-detail.js +++ b/client/src/views/admin/layouts/side-panels-detail.js @@ -26,7 +26,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -Espo.define('views/admin/layouts/side-panels-detail', 'views/admin/layouts/rows', function (Dep) { +define('views/admin/layouts/side-panels-detail', 'views/admin/layouts/rows', function (Dep) { return Dep.extend({ @@ -71,12 +71,12 @@ Espo.define('views/admin/layouts/side-panels-detail', 'views/admin/layouts/rows' }, loadLayout: function (callback) { - this.getHelper().layoutManager.get(this.scope, this.type, function (layout) { + this.getHelper().layoutManager.getOriginal(this.scope, this.type, this.setId, function (layout) { this.readDataFromLayout(layout); if (callback) { callback(); } - }.bind(this), false); + }.bind(this)); }, readDataFromLayout: function (layout) { @@ -188,5 +188,3 @@ Espo.define('views/admin/layouts/side-panels-detail', 'views/admin/layouts/rows' }); }); - - diff --git a/client/src/views/admin/layouts/side-panels-edit-small.js b/client/src/views/admin/layouts/side-panels-edit-small.js index 58feb712eb..bf58be5a5c 100644 --- a/client/src/views/admin/layouts/side-panels-edit-small.js +++ b/client/src/views/admin/layouts/side-panels-edit-small.js @@ -26,13 +26,11 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -Espo.define('views/admin/layouts/side-panels-edit-small', 'views/admin/layouts/side-panels-detail', function (Dep) { +define('views/admin/layouts/side-panels-edit-small', 'views/admin/layouts/side-panels-detail', function (Dep) { return Dep.extend({ - viewType: 'editSmall' + viewType: 'editSmall', }); }); - - diff --git a/client/src/views/admin/layouts/side-panels-edit.js b/client/src/views/admin/layouts/side-panels-edit.js index 5dae652b31..d7a0fdb586 100644 --- a/client/src/views/admin/layouts/side-panels-edit.js +++ b/client/src/views/admin/layouts/side-panels-edit.js @@ -26,13 +26,11 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -Espo.define('views/admin/layouts/side-panels-edit', 'views/admin/layouts/side-panels-detail', function (Dep) { +define('views/admin/layouts/side-panels-edit', 'views/admin/layouts/side-panels-detail', function (Dep) { return Dep.extend({ - viewType: 'edit' + viewType: 'edit', }); }); - - diff --git a/client/src/views/email/fields/email-address-varchar.js b/client/src/views/email/fields/email-address-varchar.js index 966a85c692..669e306527 100644 --- a/client/src/views/email/fields/email-address-varchar.js +++ b/client/src/views/email/fields/email-address-varchar.js @@ -318,7 +318,7 @@ define( var entityType = this.typeHash[address] || null; var id = this.idHash[address] || null; - var addressHtml = '' + address + ''; + var addressHtml = this.getHelper().escapeString(address); if (name) { name = this.getHelper().escapeString(name); @@ -329,14 +329,15 @@ define( lineHtml = '
' + '' + name + ' » ' + addressHtml + '
'; } else { if (name) { - lineHtml = '' + name + ' » ' + addressHtml + ''; + lineHtml = ''; } else { - lineHtml = addressHtml; + lineHtml = ''; } } if (!id) { if (this.getAcl().check('Contact', 'edit')) { - lineHtml += From.prototype.getCreateHtml.call(this, address); + lineHtml = From.prototype.getCreateHtml.call(this, address) + lineHtml; } } lineHtml = '
' + lineHtml + '
'; @@ -344,5 +345,4 @@ define( }, }); - }); diff --git a/client/src/views/email/fields/from-address-varchar.js b/client/src/views/email/fields/from-address-varchar.js index ca5e7afe72..6321d18583 100644 --- a/client/src/views/email/fields/from-address-varchar.js +++ b/client/src/views/email/fields/from-address-varchar.js @@ -131,19 +131,21 @@ define( var entityType = this.typeHash[address] || null; var id = this.idHash[address] || null; - var addressHtml = '' + address + ''; + var addressHtml = this.getHelper().escapeString(address); var lineHtml = ''; if (id) { - lineHtml = '
' + '' + name + ' » ' + addressHtml + '
'; + lineHtml = '
' + '' + name + + ' » ' + addressHtml + '
'; } else { if (this.getAcl().check('Contact', 'create') || this.getAcl().check('Lead', 'create')) { lineHtml += this.getCreateHtml(address); } if (name) { - lineHtml += '' + name + ' » ' + addressHtml + ''; + lineHtml += ''; } else { - lineHtml += addressHtml; + lineHtml += ''; } } lineHtml = '
' + lineHtml + '
'; diff --git a/client/src/views/fields/array-int.js b/client/src/views/fields/array-int.js index f79dbc78a4..8358ed8fec 100644 --- a/client/src/views/fields/array-int.js +++ b/client/src/views/fields/array-int.js @@ -26,7 +26,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -Espo.define('views/fields/array-int', 'views/fields/array', function (Dep) { +define('views/fields/array-int', 'views/fields/array', function (Dep) { return Dep.extend({ @@ -57,9 +57,15 @@ Espo.define('views/fields/array-int', 'views/fields/array', function (Dep) { if (isNaN(value)) { return; } - Dep.prototype.removeValue.call(this, value); - } + + var valueInternal = value.toString().replace(/"/g, '\\"'); + + this.$list.children('[data-value="' + valueInternal + '"]').remove(); + + var index = this.selected.indexOf(value); + this.selected.splice(index, 1); + this.trigger('change'); + }, }); }); - diff --git a/client/src/views/fields/formula.js b/client/src/views/fields/formula.js index dad1f81b8e..1e8fbbe146 100644 --- a/client/src/views/fields/formula.js +++ b/client/src/views/fields/formula.js @@ -102,8 +102,11 @@ define('views/fields/formula', 'views/fields/text', function (Dep) { if (this.$editor.length && (this.mode === 'edit' || this.mode == 'detail' || this.mode == 'list')) { this.$editor - .css('minHeight', this.height + 'px') .css('fontSize', '14px'); + + if (this.mode === 'edit') { + this.$editor.css('minHeight', this.height + 'px'); + } var editor = this.editor = ace.edit(this.containerId); editor.setOptions({ diff --git a/client/src/views/fields/multi-enum.js b/client/src/views/fields/multi-enum.js index d7d6983181..14091ff610 100644 --- a/client/src/views/fields/multi-enum.js +++ b/client/src/views/fields/multi-enum.js @@ -159,6 +159,7 @@ define('views/fields/multi-enum', ['views/fields/array', 'lib!Selectize'], funct highlight: false, searchField: ['label'], plugins: pluginList, + copyClassesToDropdown: true, score: function (search) { var score = this.getScoreFunction(search); search = search.toLowerCase(); diff --git a/client/src/views/layout-set/fields/edit.js b/client/src/views/layout-set/fields/edit.js new file mode 100644 index 0000000000..fd80cdaec4 --- /dev/null +++ b/client/src/views/layout-set/fields/edit.js @@ -0,0 +1,40 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014-2020 Yuri Kuznetsov, Taras Machyshyn, Oleksiy 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/layout-set/fields/edit', ['views/fields/base'], function (Dep) { + + return Dep.extend({ + + detailTemplateContent: + "" + + "{{translate 'Edit Layouts' scope='LayoutSet'}}", + + editTemplateContent: '', + + }); +}); diff --git a/client/src/views/layout-set/fields/layout-list.js b/client/src/views/layout-set/fields/layout-list.js new file mode 100644 index 0000000000..bb58518f49 --- /dev/null +++ b/client/src/views/layout-set/fields/layout-list.js @@ -0,0 +1,73 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014-2020 Yuri Kuznetsov, Taras Machyshyn, Oleksiy 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/layout-set/fields/layout-list', [ + 'views/fields/multi-enum', 'views/admin/layouts/index'], function (Dep, LaouytsIndex) { + + return Dep.extend({ + + typeList: [ + 'list', + 'detail', + 'listSmall', + 'detailSmall', + 'filters', + 'massUpdate', + 'relationships', + 'sidePanelsDetail', + 'sidePanelsEdit', + 'sidePanelsDetailSmall', + 'sidePanelsEditSmall', + ], + + setupOptions: function () { + this.params.options = []; + this.translatedOptions = {}; + + this.scopeList = Object.keys(this.getMetadata().get('scopes')).filter(function (item) { + return this.getMetadata().get(['scopes', item, 'layouts']); + }, this).sort(function (v1, v2) { + return this.translate(v1, 'scopeNames').localeCompare(this.translate(v2, 'scopeNames')); + }.bind(this)); + + var dataList = LaouytsIndex.prototype.getLayoutScopeDataList.call(this); + + dataList.forEach(function (item1) { + item1.typeList.forEach(function (type) { + var item = item1.scope + '.' + type; + if (type.substr(-6) === 'Portal') return; + this.params.options.push(item); + + this.translatedOptions[item] = this.translate(item1.scope, 'scopeNames') + '.' + + this.translate(type, 'layouts', 'Admin'); + }, this); + }, this); + }, + + }); +}); diff --git a/client/src/views/layout-set/layouts.js b/client/src/views/layout-set/layouts.js new file mode 100644 index 0000000000..3d041b5f72 --- /dev/null +++ b/client/src/views/layout-set/layouts.js @@ -0,0 +1,97 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014-2020 Yuri Kuznetsov, Taras Machyshyn, Oleksiy 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/layout-set/layouts', 'views/admin/layouts/index', function (Dep) { + + return Dep.extend({ + + setup: function () { + Dep.prototype.setup.call(this); + + var setId = this.setId = this.options.layoutSetId; + + this.wait( + this.getModelFactory().create('LayoutSet') + .then( + function (m) { + this.sModel = m; + m.id = setId; + return m.fetch(); + }.bind(this) + ) + ); + }, + + getLayoutScopeDataList: function () { + var dataList = []; + var list = this.sModel.get('layoutList') || []; + + var scopeList = []; + + list.forEach(function (item) { + var arr = item.split('.'); + var scope = arr[0]; + if (~scopeList.indexOf(scope)) return; + scopeList.push(scope); + }); + + scopeList.forEach(function (scope) { + var o = {}; + o.scope = scope; + o.typeList = []; + + list.forEach(function (item) { + var arr = item.split('.'); + var scope = arr[0]; + var type = arr[1]; + if (scope !== o.scope) return; + o.typeList.push(type); + }); + + dataList.push(o); + }); + + return dataList; + }, + + getHeaderHtml: function () { + var m = this.sModel; + var separatorHtml = ''; + + var html = ""+this.translate('LayoutSet', 'scopeNamesPlural')+" " + separatorHtml + ' ' + + ""+Handlebars.Utils.escapeExpression(m.get('name'))+" " + + separatorHtml + ' ' + this.translate('Edit Layouts', 'labels', 'LayoutSet'); + + return html; + }, + + navigate: function (scope, type) { + this.getRouter().navigate('#LayoutSet/editLayouts/id='+this.setId+'&scope='+scope + '&type='+type, {trigger: false}); + }, + }); +}); diff --git a/frontend/less/espo/custom.less b/frontend/less/espo/custom.less index a706775470..6741a88e17 100644 --- a/frontend/less/espo/custom.less +++ b/frontend/less/espo/custom.less @@ -2790,7 +2790,7 @@ table.table td.cell .complex-text { .email-address-create-dropdown .btn { position: relative; - top: -1px; + top: -2px; padding: 0 4px; margin: 0; margin-bottom: 0; @@ -2798,6 +2798,11 @@ table.table td.cell .complex-text { line-height: 1.3; } +.email-address-create-dropdown + .email-address-line { + display: block; + width: ~"calc(100% - 17px)"; +} + #main > .calendar-container { margin-top: 10px; @@ -3624,3 +3629,8 @@ a.link-gray { } } } + + +.selectize-control.as-list .item { + display: block; +} \ No newline at end of file