diff --git a/composer.json b/composer.json index 3d3dc7c..9cb001b 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,17 @@ }, "require": { "php": "~8.1.0 || ~8.2.0", - "pimcore/pimcore": "^10.5 || ^11.0" + "doctrine/dbal": "^2.13 || ^3.9", + "pimcore/pimcore": "^10.6 || ^11.0", + "symfony/config": "^5.4 || ^6.4", + "symfony/dependency-injection": "^5.4 || ^6.4", + "symfony/http-foundation": "^5.4 || ^6.4", + "symfony/http-kernel": "^5.4 || ^6.4", + "symfony/polyfill-php84": "^1.31", + "symfony/routing": "^5.4 || ^6.4", + "symfony/translation": "^5.4 || ^6.4", + "teamneusta/converter-bundle": "^1.6", + "twig/twig": "^3.8" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.62", diff --git a/config/pimcore/config.yaml b/config/pimcore/config.yaml new file mode 100644 index 0000000..1fced03 --- /dev/null +++ b/config/pimcore/config.yaml @@ -0,0 +1,24 @@ +doctrine_migrations: + migrations_paths: + Neusta\Pimcore\AreabrickConfigBundle\Migrations: '@NeustaPimcoreAreabrickConfigBundle/migrations' + +neusta_converter: + converter: + neusta_pimcore_areabrick_config.brick.converter: + target: Neusta\Pimcore\AreabrickConfigBundle\Bricks\Model\Brick + populators: + - Neusta\Pimcore\AreabrickConfigBundle\Bricks\Populator\BrickPagePopulator + properties: + id: ~ + name: ~ + version: ~ + description: ~ + template: ~ + + neusta_pimcore_areabrick_config.page.converter: + target: Neusta\Pimcore\AreabrickConfigBundle\Bricks\Model\Page + properties: + id: ~ + type: ~ + url: fullPath + published: ~ diff --git a/config/pimcore/routing.yaml b/config/pimcore/routing.yaml new file mode 100755 index 0000000..c724bfa --- /dev/null +++ b/config/pimcore/routing.yaml @@ -0,0 +1,7 @@ +neusta_pimcore_areabrick_config: + resource: '@NeustaPimcoreAreabrickConfigBundle/src/Controller/Admin/' + type: annotation + name_prefix: neusta_areabrick_config_ + prefix: /admin/bundle/neusta-areabrick-config + options: + expose: true diff --git a/config/services.yaml b/config/services.yaml new file mode 100644 index 0000000..b5fa311 --- /dev/null +++ b/config/services.yaml @@ -0,0 +1,20 @@ +services: + _defaults: + autowire: true + autoconfigure: true + + Neusta\Pimcore\AreabrickConfigBundle\Controller\Admin\AreabrickOverviewController: + arguments: + $brickConverter: '@neusta_pimcore_areabrick_config.brick.converter' + tags: [ 'controller.service_arguments' ] + + Neusta\Pimcore\AreabrickConfigBundle\Bricks\Populator\BrickPageEditModeUrlPopulator: ~ + + Neusta\Pimcore\AreabrickConfigBundle\Bricks\Populator\BrickPagePopulator: + arguments: + $pageConverter: '@neusta_pimcore_areabrick_config.page.converter' + + Neusta\Pimcore\AreabrickConfigBundle\EventListener\PimcoreAdminListener: + tags: + - { name: kernel.event_listener, event: pimcore.bundle_manager.paths.css, method: addCSSFiles } + - { name: kernel.event_listener, event: pimcore.bundle_manager.paths.js, method: addJSFiles } diff --git a/migrations/Version20241001090000.php b/migrations/Version20241001090000.php new file mode 100644 index 0000000..d34fa05 --- /dev/null +++ b/migrations/Version20241001090000.php @@ -0,0 +1,27 @@ +addSql("INSERT IGNORE INTO users_permission_definitions (`key`, `category`) VALUES('" . self::PERMISSION_KEY_AREABRICKS . "', 'Neusta Areabrick Config Bundle');"); + } + + public function down(Schema $schema): void + { + $this->addSql("DELETE FROM users_permission_definitions WHERE `key` = '" . self::PERMISSION_KEY_AREABRICKS . "';"); + } + + protected function getBundleName(): string + { + return 'NeustaPimcoreAreabrickConfigBundle'; + } +} diff --git a/public/css/admin-style.css b/public/css/admin-style.css new file mode 100644 index 0000000..5ccc8c0 --- /dev/null +++ b/public/css/admin-style.css @@ -0,0 +1,108 @@ +#neusta_areabrick_config table { + background-color: #ffffff; + border-collapse: collapse; + border: none; + width: 100%; +} + +#neusta_areabrick_config thead { + background-color: #177fac; + position: sticky; + top: 0; + z-index: 10; + color: #ffffff; +} + +#neusta_areabrick_config tfoot { + background-color: #F7F1D4; + font-size: 80%; + border-top: 1px solid #999; +} + +#neusta_areabrick_config td, +#neusta_areabrick_config th { + text-align: match-parent; + padding: 0.5em 1em; + width: auto; +} + +#neusta_areabrick_config tbody { + display: block; + height: 100%; + overflow-y: auto; +} + +#neusta_areabrick_config thead, +#neusta_areabrick_config tbody tr { + display: table; + width: 100%; + table-layout: fixed; /* Prevents columns of different widths */ +} + +#neusta_areabrick_config tbody tr:nth-child(odd) { + background-color: #ffffff; +} + +#neusta_areabrick_config tbody tr:nth-child(even) { + background-color: #d9d9d9; +} + +#neusta_areabrick_config a { + color: #0096f3; + text-decoration: none; +} + +#neusta_areabrick_config a:hover { + text-decoration: underline; + cursor: pointer; +} + +/* Additional Properties */ +#neusta_areabrick_config ul.additional-properties li { + list-style-type: none; + margin-left: 5px; + width: 150px; + padding: 2px 4px; + border-radius: 3px; + display: inline-block; + color: #ffffff; +} + +#neusta_areabrick_config ul.additional-properties li.tag { + background-color: #2a6f9c; +} + +#neusta_areabrick_config ul.additional-properties li.group { + background-color: #194567; +} + +/* Accordion */ +#neusta_areabrick_config .accordion button { + background-color: #efefef; + color: #444; + cursor: pointer; + padding: 10px; + width: 100%; + text-align: left; + border: none; + outline: none; + transition: 0.4s; + margin-bottom: 3px; +} + +#neusta_areabrick_config .accordion button.active, +#neusta_areabrick_config .accordion button:hover { + background-color: #ccc; +} + +#neusta_areabrick_config .accordion ul { + padding: 0 18px; + display: none; + overflow-x: hidden; + overflow-y: auto; + max-height: 500px; +} + +#neusta_areabrick_config .accordion ul.active { + display: block; +} diff --git a/public/js/areabrick-overview-unpublished-toggle.js b/public/js/areabrick-overview-unpublished-toggle.js new file mode 100644 index 0000000..10cdd1d --- /dev/null +++ b/public/js/areabrick-overview-unpublished-toggle.js @@ -0,0 +1,9 @@ +document.addEventListener('DOMContentLoaded', function () { + document.addEventListener('click', event => { + const el = event.target.closest('#neusta_areabrick_config .accordion'); + + if (el) { + el.querySelectorAll('button, ul').forEach(el => el.classList.toggle('active')); + } + }); +}, { once: true }); diff --git a/public/js/areabrick-overview.js b/public/js/areabrick-overview.js new file mode 100644 index 0000000..d32207e --- /dev/null +++ b/public/js/areabrick-overview.js @@ -0,0 +1,56 @@ +pimcore.registerNS('neusta.areabrick_config.areabrick_overview'); + +neusta.areabrick_config.areabrick_overview = Class.create({ + + tabId: 'neusta-areabrick-overview-tab', + panel: null, + + initialize: function () { + this.getTabPanel(); + }, + + getTabPanel: function () { + if (!this.panel) { + this.panel = Ext.create('Ext.panel.Panel', { + id: this.tabId, + title: t('neusta_areabrick_config.areabrick_overview'), + iconCls: 'pimcore_icon_areabrick', + border: false, + layout: 'fit', + flex: 1, + width: '100%', + scrollable: true, + closable: true, + }); + + const tabPanel = Ext.getCmp('pimcore_panel_tabs'); + tabPanel.add(this.panel); + tabPanel.setActiveItem(this.tabId); + + this.panel.on('destroy', function () { + pimcore.globalmanager.remove(this.tabId); + }.bind(this)); + + Ext.Ajax.request({ + url: Routing.generate('neusta_areabrick_config_areabrick_overview'), + success: function(response) { + this.panel.add({ + html: response.responseText, + autoScroll: true, + }); + }.bind(this), + }); + + document.getElementById(this.tabId).addEventListener('click', event => { + const el = event.target.closest('#neusta_areabrick_config a[data-page-id]'); + + if (el) { + pimcore.helpers.openDocument(el.dataset.pageId, el.dataset.pageType); + } + }); + } + + return this.panel; + }, + +}); diff --git a/public/js/startup.js b/public/js/startup.js new file mode 100644 index 0000000..01bdb1e --- /dev/null +++ b/public/js/startup.js @@ -0,0 +1,49 @@ +pimcore.registerNS('neusta.areabrick_config.startup'); + +neusta.areabrick_config.startup = Class.create({ + + initialize: function () { + if (pimcore.events.preMenuBuild) { + document.addEventListener(pimcore.events.preMenuBuild, this.preMenuBuild.bind(this)); + } else { + // Todo: remove when we drop Pimcore 10 support + document.addEventListener(pimcore.events.pimcoreReady, this.preMenuBuild.bind(this)); + } + }, + + preMenuBuild: function (e) { + if (!pimcore.globalmanager.get('perspective').inToolbar('tools.areabricks')) { + return; + } + + if (!pimcore.globalmanager.get('user').isAllowed('neusta_areabrick_config.areabrick_overview')) { + return; + } + + const items = { + text: t('neusta_areabrick_config.areabrick_overview'), + iconCls: 'pimcore_nav_icon_objectbricks', + priority: 31, + itemId: 'pimcore_menu_tools_areabricks', + handler: this.openAreabrickOverview, + } + + if (e.type === pimcore.events.preMenuBuild) { + e.detail.menu.extras.items.push(items); + } else { + // Todo: remove when we drop Pimcore 10 support + pimcore.globalmanager.get('layout_toolbar').extrasMenu.insert(4, items); + } + }, + + openAreabrickOverview: function() { + try { + pimcore.globalmanager.get('neusta_areabrick_config_areabrick_overview').activate(); + } catch (e) { + pimcore.globalmanager.add('neusta_areabrick_config_areabrick_overview', new neusta.areabrick_config.areabrick_overview()); + } + }, + +}); + +new neusta.areabrick_config.startup(); diff --git a/src/Bricks/Model/Brick.php b/src/Bricks/Model/Brick.php new file mode 100644 index 0000000..7504ae9 --- /dev/null +++ b/src/Bricks/Model/Brick.php @@ -0,0 +1,18 @@ + */ + public array $pages; + + /** @var list */ + public array $additionalProperties; +} diff --git a/src/Bricks/Model/BrickProperty.php b/src/Bricks/Model/BrickProperty.php new file mode 100644 index 0000000..eb70f0c --- /dev/null +++ b/src/Bricks/Model/BrickProperty.php @@ -0,0 +1,9 @@ + + */ +final class BrickPagePopulator implements Populator +{ + /** + * @param Converter $pageConverter + */ + public function __construct( + private Connection $connection, + private Converter $pageConverter, + ) { + } + + public function populate(object $target, object $source, ?object $ctx = null): void + { + $editableUsages = $this->connection->createQueryBuilder() + ->select('documentId') + ->distinct() + ->from('documents_editables') + ->where('data LIKE :data') + ->setParameter('data', '%' . $source->getId() . '%') + ->execute(); + + // Todo: remove after upgrade to doctrine/dbal >=3.9 + \assert($editableUsages instanceof Result); + + $target->pages = []; + foreach ($editableUsages->fetchFirstColumn() as $docId) { + if ($page = Page::getById($docId)) { + $target->pages[] = $this->pageConverter->convert($page); + } + } + } +} diff --git a/src/Controller/Admin/AreabrickOverviewController.php b/src/Controller/Admin/AreabrickOverviewController.php new file mode 100644 index 0000000..6faa614 --- /dev/null +++ b/src/Controller/Admin/AreabrickOverviewController.php @@ -0,0 +1,46 @@ + $brickConverter + */ + public function __construct( + private TokenStorageUserResolver $tokenResolver, + private AreabrickManagerInterface $areabrickManager, + private Converter $brickConverter, + private Environment $twig, + ) { + } + + public function __invoke(): Response + { + if (!$this->tokenResolver->getUser()?->isAllowed('neusta_areabrick_config.areabrick_overview')) { + new AccessDeniedHttpException('Access Denied.'); + } + + $bricks = array_map($this->brickConverter->convert(...), $this->areabrickManager->getBricks()); + usort($bricks, fn ($a, $b) => strcmp($a->name, $b->name)); + + $hasAdditionalProperties = array_any($bricks, fn ($brick) => !empty($brick->additionalProperties)); + + return new Response($this->twig->render('@NeustaPimcoreAreabrickConfig/bricks/overview.html.twig', [ + 'bricks' => $bricks, + 'hasAdditionalProperties' => $hasAdditionalProperties, + ])); + } +} diff --git a/src/DependencyInjection/NeustaPimcoreAreabrickConfigExtension.php b/src/DependencyInjection/NeustaPimcoreAreabrickConfigExtension.php new file mode 100644 index 0000000..a498651 --- /dev/null +++ b/src/DependencyInjection/NeustaPimcoreAreabrickConfigExtension.php @@ -0,0 +1,17 @@ +load('services.yaml'); + } +} diff --git a/src/EventListener/PimcoreAdminListener.php b/src/EventListener/PimcoreAdminListener.php new file mode 100755 index 0000000..debc779 --- /dev/null +++ b/src/EventListener/PimcoreAdminListener.php @@ -0,0 +1,24 @@ +addPaths([ + '/bundles/neustapimcoreareabrickconfig/css/admin-style.css', + ]); + } + + public function addJSFiles(PathsEvent $event): void + { + $event->addPaths([ + '/bundles/neustapimcoreareabrickconfig/js/startup.js', + '/bundles/neustapimcoreareabrickconfig/js/areabrick-overview.js', + '/bundles/neustapimcoreareabrickconfig/js/areabrick-overview-unpublished-toggle.js', + ]); + } +} diff --git a/src/NeustaPimcoreAreabrickConfigBundle.php b/src/NeustaPimcoreAreabrickConfigBundle.php index 5a62ca1..0aa2dae 100644 --- a/src/NeustaPimcoreAreabrickConfigBundle.php +++ b/src/NeustaPimcoreAreabrickConfigBundle.php @@ -2,15 +2,23 @@ namespace Neusta\Pimcore\AreabrickConfigBundle; +use Neusta\ConverterBundle\NeustaConverterBundle; use Pimcore\Extension\Bundle\AbstractPimcoreBundle; use Pimcore\Extension\Bundle\Traits\PackageVersionTrait; +use Pimcore\HttpKernel\Bundle\DependentBundleInterface; +use Pimcore\HttpKernel\BundleCollection\BundleCollection; -final class NeustaPimcoreAreabrickConfigBundle extends AbstractPimcoreBundle +final class NeustaPimcoreAreabrickConfigBundle extends AbstractPimcoreBundle implements DependentBundleInterface { use PackageVersionTrait; public function getPath(): string { - return __DIR__; + return \dirname(__DIR__); + } + + public static function registerDependentBundles(BundleCollection $collection): void + { + $collection->addBundle(NeustaConverterBundle::class); } } diff --git a/templates/bricks/overview.html.twig b/templates/bricks/overview.html.twig new file mode 100644 index 0000000..eea8e91 --- /dev/null +++ b/templates/bricks/overview.html.twig @@ -0,0 +1,78 @@ +
+ + + + + + + + + + {% if hasAdditionalProperties %} + + {% endif %} + + + + {% for brick in bricks %} + + + + + + + + {% if hasAdditionalProperties %} + + {% endif %} + + {% endfor %} + +
{{ 'neusta_pimcore_areabrick_config.areabricks.overview.table.col_headers.name'|trans }}{{ 'neusta_pimcore_areabrick_config.areabricks.overview.table.col_headers.id'|trans }}{{ 'neusta_pimcore_areabrick_config.areabricks.overview.table.col_headers.version'|trans }}{{ 'neusta_pimcore_areabrick_config.areabricks.overview.table.col_headers.description'|trans }}{{ 'neusta_pimcore_areabrick_config.areabricks.overview.table.col_headers.template'|trans }}{{ 'neusta_pimcore_areabrick_config.areabricks.overview.table.col_headers.pages'|trans }}{{ 'neusta_pimcore_areabrick_config.areabricks.overview.table.col_headers.additional_properties'|trans }}
{{ brick.name }}{{ brick.id }}{{ brick.version }}{{ brick.description }}{{ brick.template }} + {% set published_pages = brick.pages|filter(page => page.published) %} + {% set unpublished_pages = brick.pages|filter(page => not page.published) %} + + {% if published_pages is not empty %} + +
+ + +
+ {% endif %} + + {% if unpublished_pages is not empty %} + +
+ + +
+ {% endif %} +
+
    + {% for additionalProperty in brick.additionalProperties %} +
  • + {{ additionalProperty.name }}: {{ additionalProperty.value }} +
  • + {% else %} +
  • {{ 'neusta_pimcore_areabrick_config.areabricks.overview.table.no_additional_properties'|trans }}
  • + {% endfor %} +
+
+
diff --git a/translations/admin.de.yaml b/translations/admin.de.yaml new file mode 100644 index 0000000..a6371ea --- /dev/null +++ b/translations/admin.de.yaml @@ -0,0 +1,2 @@ +neusta_areabrick_config: + areabrick_overview: Areabrick-Übersicht diff --git a/translations/admin.en.yaml b/translations/admin.en.yaml new file mode 100644 index 0000000..b9f5a76 --- /dev/null +++ b/translations/admin.en.yaml @@ -0,0 +1,2 @@ +neusta_areabrick_config: + areabrick_overview: Areabrick Overview diff --git a/translations/messages.de.yaml b/translations/messages.de.yaml new file mode 100644 index 0000000..4bacda0 --- /dev/null +++ b/translations/messages.de.yaml @@ -0,0 +1,15 @@ +neusta_pimcore_areabrick_config: + areabricks: + overview: + table: + col_headers: + id: ID + name: Name + version: Version + description: Beschreibung + template: Twig Template + pages: Seiten (mit Brick) + additional_properties: zusätzliche Eigenschaften + no_additional_properties: Keine zusätzlichen Eigenschaften + published_pages: Veröffentlichte Seiten + unpublished_pages: Unveröffentlichte Seiten diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml new file mode 100644 index 0000000..87c1702 --- /dev/null +++ b/translations/messages.en.yaml @@ -0,0 +1,15 @@ +neusta_pimcore_areabrick_config: + areabricks: + overview: + table: + col_headers: + id: id + name: name + version: version + description: description + template: twig template + pages: pages (using brick) + additional_properties: additional properties + no_additional_properties: No additional properties + published_pages: Published Pages + unpublished_pages: Unpublished Pages