diff --git a/application/Espo/Classes/FieldDuplicators/DashboardTemplate/Layout.php b/application/Espo/Classes/FieldDuplicators/DashboardTemplate/Layout.php new file mode 100644 index 0000000000..09d022ace0 --- /dev/null +++ b/application/Espo/Classes/FieldDuplicators/DashboardTemplate/Layout.php @@ -0,0 +1,115 @@ +. + * + * 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\Classes\FieldDuplicators\DashboardTemplate; + +use Espo\Core\Record\Duplicator\FieldDuplicator; +use Espo\Core\Utils\ObjectUtil; +use Espo\Core\Utils\Util; +use Espo\Entities\DashboardTemplate; +use Espo\ORM\Entity; +use UnexpectedValueException; +use LogicException; +use RuntimeException; +use stdClass; + +class Layout implements FieldDuplicator +{ + public function duplicate(Entity $entity, string $field): stdClass + { + if (!$entity instanceof DashboardTemplate) { + throw new UnexpectedValueException(); + } + + $layout = $entity->getLayoutRaw(); + $options = $entity->getDashletsOptionsRaw(); + + if (!$layout) { + return (object) []; + } + + $copyLayout = []; + $idMap = []; + + foreach ($layout as $tab) { + $copyLayout[] = $this->copyTab($tab, $idMap); + } + + $copyOptions = (object) []; + + foreach (get_object_vars($options) as $id => $item) { + $copyId = $idMap[$id] ?? null; + + if (!is_string($copyId)) { + throw new LogicException(); + } + + if (!$item instanceof stdClass) { + throw new RuntimeException("Bad dashboard options."); + } + + $copyOptions->$copyId = ObjectUtil::clone($item); + } + + return (object) [ + DashboardTemplate::FIELD_LAYOUT => $copyLayout, + DashboardTemplate::FIELD_DASHLETS_OPTIONS => $copyOptions, + ]; + } + + /** + * @param array $idMap + */ + private function copyTab(stdClass $tab, array &$idMap): stdClass + { + $copy = ObjectUtil::clone($tab); + + $copy->id = Util::generateId(); + + $layout = $copy->layout ?? []; + + foreach ($layout as $item) { + if (!$item instanceof stdClass) { + throw new RuntimeException("Bad layout dashlet definition."); + } + + $id = $item->id ?? null; + + if (!is_string($id)) { + throw new RuntimeException("No string ID in layout dashlet definition."); + } + + $item->id = Util::generateId(); + + $idMap[$id] = $item->id; + } + + return $copy; + } +} diff --git a/application/Espo/Entities/DashboardTemplate.php b/application/Espo/Entities/DashboardTemplate.php index 3e98e65ed9..ab63ea5cd6 100644 --- a/application/Espo/Entities/DashboardTemplate.php +++ b/application/Espo/Entities/DashboardTemplate.php @@ -29,7 +29,31 @@ namespace Espo\Entities; -class DashboardTemplate extends \Espo\Core\ORM\Entity +use Espo\Core\ORM\Entity; +use stdClass; + +class DashboardTemplate extends Entity { - public const ENTITY_TYPE = 'DashboardTemplate'; + public const string ENTITY_TYPE = 'DashboardTemplate'; + + public const string FIELD_LAYOUT = 'layout'; + public const string FIELD_DASHLETS_OPTIONS = 'dashletsOptions'; + + /** + * @return array + * @since 10.0.0 + */ + public function getLayoutRaw(): array + { + /** @var array */ + return $this->get(self::FIELD_LAYOUT) ?? []; + } + + /** + * @since 10.0.0 + */ + public function getDashletsOptionsRaw(): stdClass + { + return $this->get(self::FIELD_DASHLETS_OPTIONS) ?? (object) []; + } } diff --git a/application/Espo/Resources/metadata/entityDefs/DashboardTemplate.json b/application/Espo/Resources/metadata/entityDefs/DashboardTemplate.json index 9590319613..0d7c825c74 100644 --- a/application/Espo/Resources/metadata/entityDefs/DashboardTemplate.json +++ b/application/Espo/Resources/metadata/entityDefs/DashboardTemplate.json @@ -9,11 +9,13 @@ "type": "jsonArray", "view": "views/settings/fields/dashboard-layout", "inlineEditDisabled": true, - "required": true + "required": true, + "duplicatorClassName": "Espo\\Classes\\FieldDuplicators\\DashboardTemplate\\Layout" }, "dashletsOptions": { "type": "jsonObject", - "utility": true + "utility": true, + "duplicateIgnore": true }, "createdAt": { "type": "datetime", diff --git a/tests/unit/Espo/Tools/DashboardTemplate/DuplicateTest.php b/tests/unit/Espo/Tools/DashboardTemplate/DuplicateTest.php new file mode 100644 index 0000000000..13c8134466 --- /dev/null +++ b/tests/unit/Espo/Tools/DashboardTemplate/DuplicateTest.php @@ -0,0 +1,78 @@ +. + * + * 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 tests\unit\Espo\Tools\DashboardTemplate; + +use Espo\Classes\FieldDuplicators\DashboardTemplate\Layout as LayoutFieldDuplicator; +use Espo\Entities\DashboardTemplate; +use PHPUnit\Framework\TestCase; + +class DuplicateTest extends TestCase +{ + public function testDuplicate(): void + { + $entity = $this->createMock(DashboardTemplate::class); + + $original = [ + (object) [ + 'id' => 'tab-01', + 'layout' => [ + (object) [ + 'id' => 'd-01', + ] + ], + ], + ]; + + $originalOptions = (object) [ + 'd-01' => (object) ['k' => 'v'], + ]; + + $entity->method('getLayoutRaw') + ->willReturn($original); + + $entity->method('getDashletsOptionsRaw') + ->willReturn($originalOptions); + + $duplicator = new LayoutFieldDuplicator(); + + $values = $duplicator->duplicate($entity, DashboardTemplate::FIELD_LAYOUT); + + $copy = $values->{DashboardTemplate::FIELD_LAYOUT} ?? null; + $copyOptions = $values->{DashboardTemplate::FIELD_DASHLETS_OPTIONS} ?? null; + + $this->assertIsArray($copy); + $this->assertIsString($copy[0]->id); + $this->assertNotEquals($original[0]->id, $copy[0]->id); + $this->assertNotEquals($original[0]->layout[0]->id, $copy[0]->layout[0]->id); + $this->assertIsString($copy[0]->layout[0]->id); + $this->assertFalse(property_exists($copyOptions, 'd-01')); + $this->assertCount(1, get_object_vars($copyOptions)); + } +}