From c167e55819418dfa223715122aac1eb80352e6dc Mon Sep 17 00:00:00 2001 From: Robin Morel Date: Thu, 10 Sep 2026 15:38:14 +0200 Subject: [PATCH 01/19] chore: require Thelia 3.0 and bump to 2.0.0 --- Config/module.xml | 4 ++-- composer.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Config/module.xml b/Config/module.xml index ddf184f..68dbe5c 100644 --- a/Config/module.xml +++ b/Config/module.xml @@ -20,7 +20,7 @@ en_US fr_FR - 1.2.0 + 2.0.0 Delage Mathis @@ -44,7 +44,7 @@ HookSearch --> - 2.4.0 + 3.0.0 other 0 0 diff --git a/composer.json b/composer.json index 4f089f9..9cd3aa0 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "license": "LGPL-3.0-or-later", "type": "thelia-module", "require": { - "thelia/installer": "~1.1" + "thelia/installer": "^1.6" }, "extra": { "installer-name": "CustomFrontMenu" From f44882ec6326b3d1e3aa6a81c4e70b1288899649 Mon Sep 17 00:00:00 2001 From: Robin Morel Date: Thu, 10 Sep 2026 15:44:48 +0200 Subject: [PATCH 02/19] feat(front): render the menu through a Twig function instead of a Smarty plugin --- README.md | 29 ++++--- Service/CustomFrontMenuLoadService.php | 39 +++++++--- Smarty/Plugins/CustomFrontMenuPlugin.php | 53 ------------- Twig/CustomFrontMenuExtension.php | 76 +++++++++++++++++++ templates/front/menu.html.twig | 27 +++++++ .../assets/css/customFrontMenu.css.html | 40 ---------- .../frontOffice/default/customFrontMenu.html | 28 ------- 7 files changed, 151 insertions(+), 141 deletions(-) delete mode 100644 Smarty/Plugins/CustomFrontMenuPlugin.php create mode 100644 Twig/CustomFrontMenuExtension.php create mode 100644 templates/front/menu.html.twig delete mode 100644 templates/frontOffice/default/assets/css/customFrontMenu.css.html delete mode 100644 templates/frontOffice/default/customFrontMenu.html diff --git a/README.md b/README.md index 128e0f7..1e9fc2f 100644 --- a/README.md +++ b/README.md @@ -33,14 +33,19 @@ Menu items can be added, deleted, renamed or moved. Translations can be made dir Each menu item is linked to a URL. This can be entered directly or associated with a `brand`, `category`, `content`, `folder` or `product`. -In front-office, each menu should be called by a smarty plugin manually. - -To override the css file, you can replace or modify : `templates/frontOffice/default/assets/css/customFrontMenu.css.html`. +In front-office, each menu is rendered by calling the `custom_front_menu()` Twig function +in your theme. The module ships markup only: style the `.cfm-menu` classes from your theme. ## Example -```smarty -{CustomFrontMenuPlugin menu_id=388} +```twig +{{ custom_front_menu(388) }} +``` + +The visitor's locale is used by default. Pass a second argument to force one: + +```twig +{{ custom_front_menu(388, 'fr_FR') }} ``` _________________ @@ -80,12 +85,18 @@ Les éléments du menu peuvent être ajoutés, supprimés, renommés ou déplac Chaque élément du menu est lié à une URL. Celle-ci peut être saisie directement ou associée à un `brand`, `category`, `content`, `folder` ou `product`. -Dans le front-office, chaque menu doit être appelé manuellement par un plugin smarty. +Dans le front-office, chaque menu est rendu en appelant la fonction Twig `custom_front_menu()` dans votre thème. -Pour remplacer le fichier css, vous pouvez remplacer ou modifier : `templates/frontOffice/default/assets/css/customFrontMenu.css.html`. +Le module ne fournit que le markup : les classes `.cfm-menu` sont à styler depuis votre thème. ## Exemple -```smarty -{CustomFrontMenuPlugin menu_id=388} +```twig +{{ custom_front_menu(388) }} +``` + +La locale du visiteur est utilisée par défaut. Un second argument permet de la forcer : + +```twig +{{ custom_front_menu(388, 'fr_FR') }} ``` diff --git a/Service/CustomFrontMenuLoadService.php b/Service/CustomFrontMenuLoadService.php index 394792e..67e1942 100644 --- a/Service/CustomFrontMenuLoadService.php +++ b/Service/CustomFrontMenuLoadService.php @@ -1,5 +1,7 @@ strtolower($type), strtolower($type).'_id' => $id]; - if($lang) { - $parameters['lang'] = $lang; + $queryClass = match (strtolower($view)) { + 'brand' => BrandQuery::class, + 'category' => CategoryQuery::class, + 'content' => ContentQuery::class, + 'folder' => FolderQuery::class, + 'product' => ProductQuery::class, + default => null, + }; + + if (null === $queryClass) { + return null; } - return URL::getInstance()->absoluteUrl('', $parameters); + + $target = $queryClass::create()->findPk($viewId); + + // Target deleted since the menu was composed: no link rather than a broken one. + return $target?->getUrl($locale); } /** @@ -198,7 +215,7 @@ public function loadTableBrowserLang(CustomFrontMenuItem $parent, string $lang) $view = $descendant->getView(); $viewId = $descendant->getViewId(); if ($view && $viewId) { - $newArray['url'] = $this->generateUrl($view, $viewId, $lang); + $newArray['url'] = $this->resolvePublicUrl($view, (int) $viewId, $lang) ?? ''; } } diff --git a/Smarty/Plugins/CustomFrontMenuPlugin.php b/Smarty/Plugins/CustomFrontMenuPlugin.php deleted file mode 100644 index 1b1f50e..0000000 --- a/Smarty/Plugins/CustomFrontMenuPlugin.php +++ /dev/null @@ -1,53 +0,0 @@ -requestStack->getCurrentRequest()->getSession()->getLang()->getLocale(); - - if (!$params['menu_id']) { - throw new \InvalidArgumentException('The menu_id parameter is required', 1); - } - - $menu = $this->customFrontMenuService->getMenu($params['menu_id']); - if (!$menu) { - throw new \InvalidArgumentException('The menu does not exist', 2); - } - - $menuItems = $this->CustomFrontMenuLoadService->loadTableBrowserLang($menu, $lang); - $smarty->assign('menuItems', $menuItems); - - $cssPath = $smarty->getTemplateDir("CustomFrontMenu"). "assets/css/customFrontMenu.css.html"; - $smarty->display($cssPath); - $templatePath = $smarty->getTemplateDir("CustomFrontMenu"). "customFrontMenu.html"; - $smarty->display($templatePath); - } -} \ No newline at end of file diff --git a/Twig/CustomFrontMenuExtension.php b/Twig/CustomFrontMenuExtension.php new file mode 100644 index 0000000..a812a0d --- /dev/null +++ b/Twig/CustomFrontMenuExtension.php @@ -0,0 +1,76 @@ +render(...), + ['needs_environment' => true, 'is_safe' => ['html']], + ), + ]; + } + + public function render(Environment $twig, int $menuId, ?string $locale = null): string + { + $menu = $this->menuService->getMenu($menuId); + + // A theme asking for a menu that no longer exists gets nothing, not an + // exception: a deleted menu must not take the whole page down. + if (null === $menu) { + return ''; + } + + return $twig->render(self::TEMPLATE, [ + 'menuItems' => $this->loadService->loadTableBrowserLang($menu, $locale ?? $this->locale()), + ]); + } + + private function locale(): string + { + /** @var Session|null $session */ + $session = $this->requestStack->getCurrentRequest()?->getSession(); + + return $session?->getLang()?->getLocale() ?? 'en_US'; + } +} diff --git a/templates/front/menu.html.twig b/templates/front/menu.html.twig new file mode 100644 index 0000000..bf002b4 --- /dev/null +++ b/templates/front/menu.html.twig @@ -0,0 +1,27 @@ +{# + Markup only: the module ships no stylesheet, the theme styles .cfm-menu. + Rendered by the custom_front_menu() Twig function. +#} +{% macro items(menuItems) %} + {% for item in menuItems %} +
  • + {% if item.url is defined and item.url is not empty %} + {{ item.title }} + {% else %} + {{ item.title }} + {% endif %} + + {% if item.children is defined and item.children is not empty %} +
      + {{ _self.items(item.children) }} +
    + {% endif %} +
  • + {% endfor %} +{% endmacro %} + + diff --git a/templates/frontOffice/default/assets/css/customFrontMenu.css.html b/templates/frontOffice/default/assets/css/customFrontMenu.css.html deleted file mode 100644 index 1476135..0000000 --- a/templates/frontOffice/default/assets/css/customFrontMenu.css.html +++ /dev/null @@ -1,40 +0,0 @@ - \ No newline at end of file diff --git a/templates/frontOffice/default/customFrontMenu.html b/templates/frontOffice/default/customFrontMenu.html deleted file mode 100644 index 802b909..0000000 --- a/templates/frontOffice/default/customFrontMenu.html +++ /dev/null @@ -1,28 +0,0 @@ -{function name=printMenu} - {foreach $menuItems as $menuItem} -
  • - {if isset($menuItem.url) && $menuItem.url != ""} - {$menuItem.title} - {else} - {$menuItem.title} - {/if} - - -
      - {if isset($menuItem.children) && $menuItem.children|count > 0} - {call name=printMenu menuItems=$menuItem.children} - {/if} -
    -
  • - {/foreach} -{/function} - - From b8666c162c2c208ddd154ef666b7d0d0e17d0c12 Mon Sep 17 00:00:00 2001 From: Robin Morel Date: Thu, 10 Sep 2026 16:09:56 +0200 Subject: [PATCH 03/19] feat(back-office): port the composition screen to the default-twig theme --- Controller/MenuController.php | 251 +++++---- CustomFrontMenu.php | 17 +- Hook/ConfigHook.php | 96 ++-- .../{default => default-twig}/en_US.php | 0 .../{default => default-twig}/es_ES.php | 0 .../{default => default-twig}/fr_FR.php | 0 .../{default => default-twig}/it_IT.php | 0 Service/BackOffice/MenuTargetCatalog.php | 81 +++ .../assets/css/styles.css | 0 .../assets/js/main.js | 83 +-- .../default-twig/module-config.html.twig | 325 ++++++++++++ .../backOffice/default/module-config.html | 484 ------------------ 12 files changed, 657 insertions(+), 680 deletions(-) rename I18n/backOffice/{default => default-twig}/en_US.php (100%) rename I18n/backOffice/{default => default-twig}/es_ES.php (100%) rename I18n/backOffice/{default => default-twig}/fr_FR.php (100%) rename I18n/backOffice/{default => default-twig}/it_IT.php (100%) create mode 100644 Service/BackOffice/MenuTargetCatalog.php rename templates/backOffice/{default => default-twig}/assets/css/styles.css (100%) rename templates/backOffice/{default => default-twig}/assets/js/main.js (94%) create mode 100644 templates/backOffice/default-twig/module-config.html.twig delete mode 100644 templates/backOffice/default/module-config.html diff --git a/Controller/MenuController.php b/Controller/MenuController.php index a504e8d..def7d4a 100644 --- a/Controller/MenuController.php +++ b/Controller/MenuController.php @@ -1,75 +1,119 @@ requestStack->getCurrentRequest()->getSession(); + if (null !== $response = $this->checkAuth(AdminResources::MODULE, 'CustomFrontMenu', AccessManager::UPDATE)) { + return $response; + } + + $this->getTokenProvider()->checkToken((string) $request->request->get('_token', '')); + + return null; + } + + private function backToScreen(): RedirectResponse + { + return new RedirectResponse(URL::getInstance()->absoluteUrl(self::COOKIE_PATH)); + } + + private function rememberMenu(int $menuId): void + { + setcookie(self::COOKIE_NAME, (string) $menuId, [ + 'path' => self::COOKIE_PATH, + 'httponly' => true, + 'samesite' => 'Lax', + ]); } /** * Load the menu selected by the user. - * @param Request $request The user request with the desired menu id - * @return RedirectResponse */ - #[Route("/admin/module/CustomFrontMenu/selectMenu", name: "admin.customfrontmenu.select.menu", methods: ["POST"])] - public function selectOtherMenu(Request $request) : RedirectResponse + #[Route('/admin/module/CustomFrontMenu/selectMenu', name: 'admin.customfrontmenu.select.menu', methods: ['POST'])] + public function selectOtherMenu(Request $request): Response { - $menuId = intval(str_replace("menu-selected-", "", $request->get('menuId'))); + if (null !== $denied = $this->denyUnlessAllowed($request)) { + return $denied; + } - setcookie('menuId', $menuId); + $this->rememberMenu((int) str_replace('menu-selected-', '', (string) $request->get('menuId'))); - return new RedirectResponse(URL::getInstance()->absoluteUrl('/admin/module/CustomFrontMenu')); + return $this->backToScreen(); } /** * Save the selected menu items in database. - * @param Request $request The user request with the menu items and the selected menu id - * @param CustomFrontMenuSaveService $customFrontMenuSave The saving service - * @param CustomFrontMenuService $customFrontMenuService + * * @throws PropelException - * @throws Exception */ - #[Route("/admin/module/CustomFrontMenu/save", name:"admin.customfrontmenu.save", methods:["POST"])] - public function saveMenuItems(Request $request, CustomFrontMenuSaveService $customFrontMenuSave, CustomFrontMenuService $customFrontMenuService) : RedirectResponse - { + #[Route('/admin/module/CustomFrontMenu/save', name: 'admin.customfrontmenu.save', methods: ['POST'])] + public function saveMenuItems( + Request $request, + CustomFrontMenuSaveService $customFrontMenuSave, + CustomFrontMenuService $customFrontMenuService, + ): Response { + if (null !== $denied = $this->denyUnlessAllowed($request)) { + return $denied; + } - $dataJson = $request->get('menuData'); - $newMenu = json_decode($dataJson, true); - $menuId = json_decode($request->get('menuDataId')); + $newMenu = json_decode((string) $request->get('menuData'), true); + $menuId = json_decode((string) $request->get('menuDataId')); + + if (!\is_array($newMenu)) { + throw new \InvalidArgumentException('Save failed: the menu payload is not a list of items'); + } - if (!$menuId || $menuId === 'undefined' || $menuId === 'null') { - throw new Exception('Save failed : the menu id cannot be null or empty'); + if (!\is_int($menuId) || 0 === $menuId) { + throw new \InvalidArgumentException('Save failed: the menu id cannot be null or empty'); } $menuToCheck = $customFrontMenuService->getMenu($menuId); - if (!$menuToCheck || $menuToCheck->getLevel() !== 1) { - throw new Exception('Save failed : the menu id is invalid'); + if (!$menuToCheck || 1 !== $menuToCheck->getLevel()) { + throw new \InvalidArgumentException('Save failed: the menu id is invalid'); } // Delete all the items currently in database for the menu to save @@ -78,105 +122,128 @@ public function saveMenuItems(Request $request, CustomFrontMenuSaveService $cust // Add all new items in database $customFrontMenuSave->saveTableBrowser($newMenu, $menu); - $this->getSession()->getFlashBag()->add('success', Translator::getInstance()->trans('This menu has been successfully saved !', [], CustomFrontMenu::DOMAIN_NAME)); + $this->getSession()->getFlashBag()->add( + 'success', + Translator::getInstance()->trans('This menu has been successfully saved !', [], CustomFrontMenu::DOMAIN_NAME), + ); - return new RedirectResponse(URL::getInstance()->absoluteUrl('/admin/module/CustomFrontMenu')); + return $this->backToScreen(); } /** - * Add a new menu with the name given by the user. - * The user is redirected in this new menu. - * @param Request $request The user request with the menu name - * @param CustomFrontMenuLoadService $customFrontMenuLoadService The loading service - * @param CustomFrontMenuService $customFrontMenuService The menu service - * @throws Exception + * Add a new menu with the name given by the user, and select it. + * + * @throws PropelException */ - #[Route("/admin/module/CustomFrontMenu/add", name: "admin.customfrontmenu.addmenu", methods: ["POST"])] - public function addMenu(Request $request, CustomFrontMenuLoadService $customFrontMenuLoadService, CustomFrontMenuService $customFrontMenuService) : RedirectResponse + #[Route('/admin/module/CustomFrontMenu/add', name: 'admin.customfrontmenu.addmenu', methods: ['POST'])] + public function addMenu(Request $request, CustomFrontMenuService $customFrontMenuService): Response { - $menuName = $request->get('menuName'); + if (null !== $denied = $this->denyUnlessAllowed($request)) { + return $denied; + } + $root = $customFrontMenuService->getRoot(); - $itemId = $customFrontMenuService->addMenu($root, $menuName); - $this->loadMenuItems($customFrontMenuLoadService, $customFrontMenuService, $itemId); - setcookie('menuId', $itemId); + $itemId = $customFrontMenuService->addMenu($root, (string) $request->get('menuName')); - $this->getSession()->getFlashBag()->add('success', Translator::getInstance()->trans('New menu added successfully', [], CustomFrontMenu::DOMAIN_NAME)); + $this->rememberMenu($itemId); - return new RedirectResponse(URL::getInstance()->absoluteUrl('/admin/module/CustomFrontMenu')); + $this->getSession()->getFlashBag()->add( + 'success', + Translator::getInstance()->trans('New menu added successfully', [], CustomFrontMenu::DOMAIN_NAME), + ); + + return $this->backToScreen(); } /** * Delete the current menu. - * The user is redirected in the first menu if it exists. - * @param Request $request The user request with the menu id - * @param CustomFrontMenuService $customFrontMenuService The menu service - * @throws Exception */ - #[Route("/admin/module/CustomFrontMenu/delete", name:"admin.customfrontmenu.deletemenu", methods:["POST"])] - public function deleteMenu(Request $request, CustomFrontMenuService $customFrontMenuService) : RedirectResponse + #[Route('/admin/module/CustomFrontMenu/delete', name: 'admin.customfrontmenu.deletemenu', methods: ['POST'])] + public function deleteMenu(Request $request, CustomFrontMenuService $customFrontMenuService): Response { - $firstCurrentMenuId = $request->get('menuId'); - if($firstCurrentMenuId === null || $firstCurrentMenuId === 'menu-selected-') { - throw new Exception('Delete failed : the menu id cannot be null or empty'); + if (null !== $denied = $this->denyUnlessAllowed($request)) { + return $denied; } - $currentMenuId = intval(str_replace("menu-selected-", "", $firstCurrentMenuId)); + $rawMenuId = (string) $request->get('menuId', ''); - $customFrontMenuService->deleteMenu($currentMenuId); + if ('' === $rawMenuId || 'menu-selected-' === $rawMenuId) { + throw new \InvalidArgumentException('Delete failed: the menu id cannot be null or empty'); + } - $this->getSession()->getFlashBag()->add('success', Translator::getInstance()->trans('Current menu deleted successfully', [], CustomFrontMenu::DOMAIN_NAME)); + $customFrontMenuService->deleteMenu((int) str_replace('menu-selected-', '', $rawMenuId)); - if (isset($_COOKIE['menuId'])) { - setcookie('menuId', -1); - } + $this->getSession()->getFlashBag()->add( + 'success', + Translator::getInstance()->trans('Current menu deleted successfully', [], CustomFrontMenu::DOMAIN_NAME), + ); - return new RedirectResponse(URL::getInstance()->absoluteUrl('/admin/module/CustomFrontMenu')); - } + $this->rememberMenu(-1); - /** - * Clear all flashes - */ - #[Route("/admin/module/CustomFrontMenu/clearFlashes", name:"admin.customfrontmenu.clearflashes", methods:["GET"])] - public function clearFlashes() : Response - { - $this->getSession()->getFlashBag()->clear(); - // Clear the response too to limit the data returned by http - return new Response('', ResponseAlias::HTTP_OK); + return $this->backToScreen(); } /** - * Load the menu items - * @param CustomFrontMenuLoadService $customFrontMenuLoadService The loading service - * @param CustomFrontMenuService $customFrontMenuService The menu service - * @param ?int $menuId The id of the menu to load - * @return array All the data necessary to load the page content : Menu names, menu items and the current menu id. + * Everything the composition screen needs to render. + * + * @return array + * * @throws PropelException */ - public function loadMenuItems(CustomFrontMenuLoadService $customFrontMenuLoadService, CustomFrontMenuService $customFrontMenuService, ?int $menuId = null) : array - { + public function loadMenuItems( + CustomFrontMenuLoadService $customFrontMenuLoadService, + CustomFrontMenuService $customFrontMenuService, + ?int $menuId = null, + ): array { $menuNames = $customFrontMenuLoadService->loadSelectMenu($customFrontMenuService->getRoot()); $data = []; - if (!$menuId && count($menuNames) > 0) { - $menuId = intval(str_replace("menu-selected-", "", $menuNames[0]['id'])); + if (!$menuId && \count($menuNames) > 0) { + $menuId = (int) str_replace('menu-selected-', '', $menuNames[0]['id']); } - if($menuId) { + if ($menuId) { $menu = $customFrontMenuService->getMenu($menuId); - if (!$menu || $menu->getLevel() !== 1) { - $this->getSession()->getFlashBag()->add('fail', Translator::getInstance()->trans('This menu does not exists', [], CustomFrontMenu::DOMAIN_NAME)); - $menuId = intval(str_replace("menu-selected-", "", $menuNames[0]['id'])); - setcookie('menuId', $menuId, ['path' => '/admin/module/CustomFrontMenu']); + + if (!$menu || 1 !== $menu->getLevel()) { + $this->getSession()->getFlashBag()->add( + 'fail', + Translator::getInstance()->trans('This menu does not exists', [], CustomFrontMenu::DOMAIN_NAME), + ); + + if (0 === \count($menuNames)) { + return $this->screenData($menuNames, [], 0); + } + + $menuId = (int) str_replace('menu-selected-', '', $menuNames[0]['id']); + $this->rememberMenu($menuId); $menu = $customFrontMenuService->getMenu($menuId); } - $data = $customFrontMenuLoadService->loadTableBrowser($menu); + if ($menu) { + $data = $customFrontMenuLoadService->loadTableBrowser($menu); + } } + return $this->screenData($menuNames, $data, (int) $menuId); + } + + /** + * @param array> $menuNames + * @param array $menuItems + * + * @return array + */ + private function screenData(array $menuNames, array $menuItems, int $menuId): array + { + $locale = $this->getSession()->getAdminLang()->getLocale(); + return [ 'menuNames' => json_encode($menuNames), - 'menuItems' => json_encode($data), - 'currentMenuId' => utf8_encode($menuId) + 'menuItems' => json_encode($menuItems), + 'currentMenuId' => $menuId, + 'locale' => $locale, + 'targets' => $this->targetCatalog->targets($locale), ]; } -} \ No newline at end of file +} diff --git a/CustomFrontMenu.php b/CustomFrontMenu.php index 2619046..6038fdc 100644 --- a/CustomFrontMenu.php +++ b/CustomFrontMenu.php @@ -1,4 +1,7 @@ name('*.sql') ->depth(0) ->sortByName() - ->in(__DIR__.DS.'Config'.DS.'update'); + ->in($updateDir); $database = new Database($con); @@ -69,7 +80,7 @@ public function destroy(ConnectionInterface $con = null, $deleteModuleData = fal public static function configureServices(ServicesConfigurator $servicesConfigurator): void { $servicesConfigurator->load(self::getModuleCode().'\\', __DIR__) - ->exclude([THELIA_MODULE_DIR . ucfirst(self::getModuleCode()). "/I18n/*"]) + ->exclude([__DIR__.'/I18n/*']) ->autowire(true) ->autoconfigure(true); } diff --git a/Hook/ConfigHook.php b/Hook/ConfigHook.php index af3d51d..7b31d24 100644 --- a/Hook/ConfigHook.php +++ b/Hook/ConfigHook.php @@ -1,5 +1,17 @@ [ - [ - "type" => "back", - "method" => "addMenuJs" - ] + 'module.config-js' => [ + ['type' => 'back', 'method' => 'addMenuJs'], + ], + 'main.head-css' => [ + ['type' => 'back', 'method' => 'addMenuCss'], ], - "main.head-css" => [ - [ - "type" => "back", - "method" => "addMenuCss" - ] + 'module.configuration' => [ + ['type' => 'back', 'method' => 'onModuleConfiguration'], ], - "module.configuration" => [ - [ - "type" => "back", - "method" => "onModuleConfiguration" - ] - ] ]; } - public function addMenuJs(HookRenderEvent $event):void + public function addMenuJs(HookRenderEvent $event): void { - $event->add($this->addJS("assets/js/main.js")); + $event->add($this->addJS('assets/js/main.js')); } - public function addMenuCss(HookRenderEvent $event):void + public function addMenuCss(HookRenderEvent $event): void { - $event->add($this->addCSS("assets/css/styles.css")); + $event->add($this->addCSS('assets/css/styles.css')); } /** * @throws PropelException */ - public function onModuleConfiguration(HookRenderEvent $event) : void + public function onModuleConfiguration(HookRenderEvent $event): void { - if (isset($_COOKIE['menuId']) && $_COOKIE['menuId'] != -1) { - $data = $this->menuController->loadMenuItems($this->customFrontMenuLoadService, $this->customFrontMenuService, $_COOKIE['menuId']); - } else { - $data = $this->menuController->loadMenuItems($this->customFrontMenuLoadService, $this->customFrontMenuService); + $event->add($this->render('module-config.html.twig', $this->menuController->loadMenuItems( + $this->customFrontMenuLoadService, + $this->customFrontMenuService, + $this->selectedMenuId(), + ))); + } + + /** + * The screen remembers the menu being composed in a cookie, so it survives a redirect + * after save. The value is client-controlled: anything but a positive integer means + * "no selection" and lets the screen fall back to the first menu. + */ + private function selectedMenuId(): ?int + { + $raw = $this->requestStack->getCurrentRequest()?->cookies->get('menuId'); + + if (!\is_string($raw) || 1 !== preg_match('/^\d+$/', $raw)) { + return null; } - $event->add($this->render("module-config.html", $data)); + $menuId = (int) $raw; + + return $menuId > 0 ? $menuId : null; } } diff --git a/I18n/backOffice/default/en_US.php b/I18n/backOffice/default-twig/en_US.php similarity index 100% rename from I18n/backOffice/default/en_US.php rename to I18n/backOffice/default-twig/en_US.php diff --git a/I18n/backOffice/default/es_ES.php b/I18n/backOffice/default-twig/es_ES.php similarity index 100% rename from I18n/backOffice/default/es_ES.php rename to I18n/backOffice/default-twig/es_ES.php diff --git a/I18n/backOffice/default/fr_FR.php b/I18n/backOffice/default-twig/fr_FR.php similarity index 100% rename from I18n/backOffice/default/fr_FR.php rename to I18n/backOffice/default-twig/fr_FR.php diff --git a/I18n/backOffice/default/it_IT.php b/I18n/backOffice/default-twig/it_IT.php similarity index 100% rename from I18n/backOffice/default/it_IT.php rename to I18n/backOffice/default-twig/it_IT.php diff --git a/Service/BackOffice/MenuTargetCatalog.php b/Service/BackOffice/MenuTargetCatalog.php new file mode 100644 index 0000000..d19dddd --- /dev/null +++ b/Service/BackOffice/MenuTargetCatalog.php @@ -0,0 +1,81 @@ + tag per row, so a shop with + * ten thousand products shipped ten thousand script tags. This runs the same five queries + * once and hands the screen a single payload to put in a data- attribute. + */ +final readonly class MenuTargetCatalog +{ + private const FALLBACK_LOCALE = 'en_US'; + + /** + * @return array>> + */ + public function targets(?string $locale = null): array + { + $locale ??= self::FALLBACK_LOCALE; + + return [ + 'brand' => $this->rows(BrandQuery::create(), $locale), + 'category' => $this->rows(CategoryQuery::create(), $locale), + 'content' => $this->rows(ContentQuery::create(), $locale), + 'folder' => $this->rows(FolderQuery::create(), $locale), + 'product' => $this->rows(ProductQuery::create(), $locale, withReference: true), + ]; + } + + /** + * @return list> + */ + private function rows(ModelCriteria $query, string $locale, bool $withReference = false): array + { + // LEFT_JOIN, not the default inner join: an item with no translation in this + // locale must still be pickable, titleless rather than absent. + $results = $query + ->joinWithI18n($locale, Criteria::LEFT_JOIN) + ->orderById() + ->find(); + + $rows = []; + + foreach ($results as $result) { + $row = [ + 'id' => (int) $result->getId(), + 'title' => (string) $result->getTitle(), + ]; + + if ($withReference) { + $row['reference'] = (string) $result->getRef(); + } + + $rows[] = $row; + } + + return $rows; + } +} diff --git a/templates/backOffice/default/assets/css/styles.css b/templates/backOffice/default-twig/assets/css/styles.css similarity index 100% rename from templates/backOffice/default/assets/css/styles.css rename to templates/backOffice/default-twig/assets/css/styles.css diff --git a/templates/backOffice/default/assets/js/main.js b/templates/backOffice/default-twig/assets/js/main.js similarity index 94% rename from templates/backOffice/default/assets/js/main.js rename to templates/backOffice/default-twig/assets/js/main.js index 75e776c..78ff569 100644 --- a/templates/backOffice/default/assets/js/main.js +++ b/templates/backOffice/default-twig/assets/js/main.js @@ -1,14 +1,14 @@ +// The screen hands its payload over in data- attributes on #cfm-screen: the catalogue +// of pickable targets, the menus, the current selection, the locale and the JS strings. +const CFM_SCREEN = document.getElementById('cfm-screen') + var MENU_NAMES var MENU_LIST -var CURRENT_SELECTED_MENU_ID -var LOCALE -var loopsDictionary = { - "brand": brandLoopData, - "category": categoryLoopData, - "content": contentLoopData, - "folder": folderLoopData, - "product": productLoopData -}; +var CURRENT_SELECTED_MENU_ID = parseInt(CFM_SCREEN.dataset.cfmCurrentMenuId, 10) +var LOCALE = CFM_SCREEN.dataset.cfmLocale +var loopsDictionary = JSON.parse(CFM_SCREEN.dataset.cfmTargets) +var translations = JSON.parse(CFM_SCREEN.dataset.cfmTranslations) +var buttonState = 'hide' let CURRENT_ID = null let allowUnload = false let selectedLanguage @@ -51,8 +51,7 @@ function getValueByLocaleOf(element, locale) { function closeClosestModal(element) { let modal = element.closest('.modal'); if (modal) { - let modalId = modal.getAttribute('id'); - $(`#${modalId}`).modal('hide'); + bootstrap.Modal.getOrCreateInstance(modal).hide(); } } // End close closest modal @@ -163,7 +162,7 @@ function addMenu() { errorMessageEmpty.style.display = 'none'; errorMessageBackQuote.style.display = 'block'; } else { - $('#ConfirmAddMenu').modal('hide'); + bootstrap.Modal.getOrCreateInstance(document.getElementById('ConfirmAddMenu')).hide(); errorMessageEmpty.style.display = 'none'; errorMessageBackQuote.style.display = 'none'; document.getElementById('addMenuForm').submit(); @@ -685,19 +684,19 @@ function generateMenuRecursive(menuItem) { ` + getValueByLocaleOf(menuItem.title) + `` + arrowSpan + ` - - + +