diff --git a/Makefile b/Makefile index 02cdc07..10bfbdd 100644 --- a/Makefile +++ b/Makefile @@ -132,7 +132,7 @@ test.container: ## Lint the symfony container ${CONSOLE} lint:container test.yaml: ## Lint the symfony Yaml files - ${CONSOLE} lint:yaml ../../recipes ../../src/Resources/config + ${CONSOLE} lint:yaml ../../recipes ../../src/Resources/config --parse-tags test.schema: ## Validate MySQL Schema ${CONSOLE} doctrine:schema:validate diff --git a/README.md b/README.md index 6d64fc0..651cf98 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,9 @@ Then create the config file in `config/packages/monsieurbiz_sylius_menu_plugin.y ```yaml imports: - { resource: "@MonsieurBizSyliusMenuPlugin/Resources/config/config.yaml" } + +twig: + form_themes: ['@MonsieurBizSyliusMenuPlugin/Admin/Browser/Form/_theme.html.twig'] ``` Finally import the routes in `config/routes/monsieurbiz_sylius_menu_plugin.yaml`: @@ -49,6 +52,15 @@ bin/console doctrine:migrations:migrate If you want to customize your menu, like adding an image, do so by overriding the MenuItem entity (more info about [overriding entities in the Sylius documentation](https://docs.sylius.com/en/1.9/customization/model.html)). +## Add URL Provider + +The URLs selector allows you to select a URL from a list of URLs. +It provides URLs for : +- Taxons +- Products + +You can add your customer Provider by creating a class which implements the `MonsieurBiz\SyliusMenuPlugin\Provider\UrlProviderInterface` .interface. + ## Menu example ### Admin form index diff --git a/composer.json b/composer.json index 7f3ba47..405faa9 100644 --- a/composer.json +++ b/composer.json @@ -65,7 +65,7 @@ "require": "^4.4" }, "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "1.3-dev" } }, "config": { diff --git a/dist/config/packages/monsieurbiz_sylius_menu_plugin.yaml b/dist/config/packages/monsieurbiz_sylius_menu_plugin.yaml index 5b3d08b..36dbbe7 100644 --- a/dist/config/packages/monsieurbiz_sylius_menu_plugin.yaml +++ b/dist/config/packages/monsieurbiz_sylius_menu_plugin.yaml @@ -1,2 +1,5 @@ imports: - { resource: "@MonsieurBizSyliusMenuPlugin/Resources/config/config.yaml" } + +twig: + form_themes: ['@MonsieurBizSyliusMenuPlugin/Admin/Browser/Form/_theme.html.twig'] diff --git a/recipes/.gitignore b/recipes/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/recipes/1.3-dev/config/packages/monsieurbiz_sylius_menu_plugin.yaml b/recipes/1.3-dev/config/packages/monsieurbiz_sylius_menu_plugin.yaml new file mode 100644 index 0000000..36dbbe7 --- /dev/null +++ b/recipes/1.3-dev/config/packages/monsieurbiz_sylius_menu_plugin.yaml @@ -0,0 +1,5 @@ +imports: + - { resource: "@MonsieurBizSyliusMenuPlugin/Resources/config/config.yaml" } + +twig: + form_themes: ['@MonsieurBizSyliusMenuPlugin/Admin/Browser/Form/_theme.html.twig'] diff --git a/recipes/1.3-dev/config/routes/monsieurbiz_sylius_menu_plugin.yaml b/recipes/1.3-dev/config/routes/monsieurbiz_sylius_menu_plugin.yaml new file mode 100644 index 0000000..0c3393c --- /dev/null +++ b/recipes/1.3-dev/config/routes/monsieurbiz_sylius_menu_plugin.yaml @@ -0,0 +1,3 @@ +monsieurbiz_menu_admin_menu: + resource: "@MonsieurBizSyliusMenuPlugin/Resources/config/routes/admin.yaml" + prefix: /%sylius_admin.path_name% diff --git a/recipes/1.3-dev/manifest.json b/recipes/1.3-dev/manifest.json new file mode 100644 index 0000000..cf56ec8 --- /dev/null +++ b/recipes/1.3-dev/manifest.json @@ -0,0 +1,10 @@ +{ + "bundles": { + "MonsieurBiz\\SyliusMenuPlugin\\MonsieurBizSyliusMenuPlugin": [ + "all" + ] + }, + "copy-from-recipe": { + "config/": "%CONFIG_DIR%/" + } +} diff --git a/src/Controller/BrowserController.php b/src/Controller/BrowserController.php new file mode 100644 index 0000000..d8f5138 --- /dev/null +++ b/src/Controller/BrowserController.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusMenuPlugin\Controller; + +use MonsieurBiz\SyliusMenuPlugin\Provider\BrowsableObjectProviderInterface; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +final class BrowserController extends AbstractController +{ + public function __construct( + private BrowsableObjectProviderInterface $browsableObjectProvider + ) { + } + + public function listAction( + Request $request, + ): ?Response { + $inputName = (string) $request->query->get('inputName', ''); + $inputValue = (string) $request->query->get('inputValue', ''); + $locale = (string) $request->query->get('locale', ''); + + return $this->render('@MonsieurBizSyliusMenuPlugin/Admin/Browser/_modal.html.twig', [ + 'urlProviders' => $this->browsableObjectProvider->getUrlProviders(), + 'inputName' => $inputName, + 'inputValue' => $inputValue, + 'locale' => $locale, + ]); + } + + public function listItemsAction( + Request $request, + ): ?Response { + $providerCode = (string) $request->query->get('providerCode', ''); + $inputName = (string) $request->query->get('inputName', ''); + $inputValue = (string) $request->query->get('inputValue', ''); + $locale = (string) $request->query->get('locale', ''); + $search = (string) $request->query->get('search', ''); + + $urlProvider = $this->browsableObjectProvider->findProviderByCode($providerCode); + if (null === $urlProvider) { + return new JsonResponse(['error' => 'URL Provider not found'], 404); + } + + return $this->render('@MonsieurBizSyliusMenuPlugin/Admin/Browser/_modal.html.twig', [ + 'urlProvider' => $urlProvider, + 'inputName' => $inputName, + 'inputValue' => $inputValue, + 'locale' => $locale, + 'search' => $search, + ]); + } +} diff --git a/src/DependencyInjection/MonsieurBizSyliusMenuExtension.php b/src/DependencyInjection/MonsieurBizSyliusMenuExtension.php index 36e969c..445e33c 100644 --- a/src/DependencyInjection/MonsieurBizSyliusMenuExtension.php +++ b/src/DependencyInjection/MonsieurBizSyliusMenuExtension.php @@ -13,6 +13,7 @@ namespace MonsieurBiz\SyliusMenuPlugin\DependencyInjection; +use MonsieurBiz\SyliusMenuPlugin\Provider\UrlProviderInterface; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\Extension; @@ -28,6 +29,7 @@ public function load(array $config, ContainerBuilder $container): void { $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); $loader->load('services.yaml'); + $container->registerForAutoconfiguration(UrlProviderInterface::class)->addTag('monsieurbiz_menu.url_provider'); } /** diff --git a/src/Form/Type/MenuItemTranslationType.php b/src/Form/Type/MenuItemTranslationType.php index ff7adfc..b4bbb9b 100644 --- a/src/Form/Type/MenuItemTranslationType.php +++ b/src/Form/Type/MenuItemTranslationType.php @@ -30,7 +30,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ->add('label', TextType::class, [ 'label' => 'monsieurbiz_menu.ui.label', ]) - ->add('url', TextType::class, [ + ->add('url', UrlType::class, [ 'required' => false, 'label' => 'monsieurbiz_menu.ui.url', ]) diff --git a/src/Form/Type/UrlType.php b/src/Form/Type/UrlType.php new file mode 100644 index 0000000..51ef814 --- /dev/null +++ b/src/Form/Type/UrlType.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusMenuPlugin\Form\Type; + +use Symfony\Component\Form\Extension\Core\Type\TextType; + +final class UrlType extends TextType +{ + public function getBlockPrefix(): string + { + return 'monsieurbiz_sylius_menu_url'; + } +} diff --git a/src/Provider/AbstractUrlProvider.php b/src/Provider/AbstractUrlProvider.php new file mode 100644 index 0000000..567056f --- /dev/null +++ b/src/Provider/AbstractUrlProvider.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusMenuPlugin\Provider; + +use Symfony\Component\Routing\RouterInterface; + +abstract class AbstractUrlProvider implements UrlProviderInterface +{ + protected int $maxResults = 1000; + + protected string $code; + + protected string $icon = 'angle right'; + + protected int $priority = 0; + + protected array $items = []; + + public function __construct( + protected RouterInterface $router + ) { + } + + public function getIcon(): string + { + return $this->icon; + } + + public function getCode(): string + { + return $this->code; + } + + public function getPriority(): int + { + return $this->priority; + } + + public function getMaxResults(): int + { + return $this->maxResults; + } + + protected function addItem(string $name, string $path): void + { + $this->items[] = [ + 'name' => $name, + 'value' => $path, + ]; + } + + protected function sortItems(): void + { + usort($this->items, fn ($itemA, $itemB) => $itemA['name'] <=> $itemB['name']); + } + + abstract protected function getResults(string $locale, string $search = ''): iterable; + + abstract protected function addItemFromResult(object $result, string $locale): void; + + public function getItems(string $locale, string $search = ''): array + { + $this->items = []; + $results = $this->getResults($locale, $search); + + foreach ($results as $result) { + $this->addItemFromResult($result, $locale); + } + + $this->sortItems(); + + return $this->items; + } +} diff --git a/src/Provider/BrowsableObjectProvider.php b/src/Provider/BrowsableObjectProvider.php new file mode 100644 index 0000000..aab6d57 --- /dev/null +++ b/src/Provider/BrowsableObjectProvider.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusMenuPlugin\Provider; + +class BrowsableObjectProvider implements BrowsableObjectProviderInterface +{ + public function __construct(private iterable $urlProviders) + { + } + + public function getUrlProviders(): array + { + $urlProviders = []; + foreach ($this->urlProviders as $urlProvider) { + $urlProviders[$urlProvider->getCode()] = $urlProvider; + } + + uasort($urlProviders, fn ($urlProviderA, $urlProviderB) => $urlProviderB->getPriority() <=> $urlProviderA->getPriority()); + + return $urlProviders; + } + + public function findProviderByCode(string $code): ?UrlProviderInterface + { + /** @var UrlProviderInterface $urlProvider */ + foreach ($this->getUrlProviders() as $urlProvider) { + if ($urlProvider->getCode() === $code) { + return $urlProvider; + } + } + + return null; + } +} diff --git a/src/Provider/BrowsableObjectProviderInterface.php b/src/Provider/BrowsableObjectProviderInterface.php new file mode 100644 index 0000000..ee5d78c --- /dev/null +++ b/src/Provider/BrowsableObjectProviderInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusMenuPlugin\Provider; + +interface BrowsableObjectProviderInterface +{ + public function getUrlProviders(): array; + + public function findProviderByCode(string $code): ?UrlProviderInterface; +} diff --git a/src/Provider/ProductUrlProvider.php b/src/Provider/ProductUrlProvider.php new file mode 100644 index 0000000..b56777c --- /dev/null +++ b/src/Provider/ProductUrlProvider.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusMenuPlugin\Provider; + +use Sylius\Component\Core\Model\ProductInterface; +use Sylius\Component\Core\Repository\ProductRepositoryInterface; +use Symfony\Component\Routing\RouterInterface; +use Webmozart\Assert\Assert; + +class ProductUrlProvider extends AbstractUrlProvider +{ + public const PROVIDER_CODE = 'product'; + + protected string $code = self::PROVIDER_CODE; + + protected string $icon = 'cube'; + + protected int $priority = 50; + + public function __construct( + RouterInterface $router, + private ProductRepositoryInterface $productRepository, + ) { + parent::__construct($router); + } + + protected function getResults(string $locale, string $search = ''): iterable + { + $queryBuilder = $this->productRepository->createListQueryBuilder($locale) + ->andWhere('o.enabled = :enabled') + ->setParameter('enabled', true) + ; + + if (!empty($search)) { + $queryBuilder + ->andWhere('translation.name LIKE :search OR o.code LIKE :search OR translation.slug LIKE :search') + ->setParameter('search', '%' . $search . '%') + ; + } + + $queryBuilder->setMaxResults($this->getMaxResults()); + + return $queryBuilder->getQuery()->getResult(); + } + + protected function addItemFromResult(object $result, string $locale): void + { + Assert::isInstanceOf($result, ProductInterface::class); + /** @var ProductInterface $result */ + $result->setCurrentLocale($locale); + $this->addItem( + (string) $result->getName(), + $this->router->generate('sylius_shop_product_show', ['slug' => $result->getSlug(), '_locale' => $locale]) + ); + } +} diff --git a/src/Provider/TaxonUrlProvider.php b/src/Provider/TaxonUrlProvider.php new file mode 100644 index 0000000..4f91664 --- /dev/null +++ b/src/Provider/TaxonUrlProvider.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusMenuPlugin\Provider; + +use Sylius\Component\Core\Model\TaxonInterface; +use Sylius\Component\Taxonomy\Repository\TaxonRepositoryInterface; +use Symfony\Component\Routing\RouterInterface; +use Webmozart\Assert\Assert; + +class TaxonUrlProvider extends AbstractUrlProvider +{ + public const PROVIDER_CODE = 'taxon'; + + protected string $code = self::PROVIDER_CODE; + + protected string $icon = 'folder'; + + protected int $priority = 100; + + public function __construct( + RouterInterface $router, + private TaxonRepositoryInterface $taxonRepository, + ) { + parent::__construct($router); + } + + protected function getResults(string $locale, string $search = ''): iterable + { + $queryBuilder = $this->taxonRepository->createListQueryBuilder() + ->andWhere('translation.locale = :locale') + ->andWhere('o.enabled = :enabled') + ->andWhere('o.parent IS NOT NULL') // Avoid root taxons + ->setParameter('locale', $locale) + ->setParameter('enabled', true) + ; + + if (!empty($search)) { + $queryBuilder + ->andWhere('translation.name LIKE :search OR o.code LIKE :search OR translation.slug LIKE :search') + ->setParameter('search', '%' . $search . '%') + ; + } + + $queryBuilder->setMaxResults($this->getMaxResults()); + + return $queryBuilder->getQuery()->getResult(); + } + + protected function addItemFromResult(object $result, string $locale): void + { + Assert::isInstanceOf($result, TaxonInterface::class); + /** @var TaxonInterface $result */ + $result->setCurrentLocale($locale); + $this->addItem( + (string) $result->getFullname(' > '), + $this->router->generate('sylius_shop_product_index', ['slug' => $result->getSlug(), '_locale' => $locale]) + ); + } +} diff --git a/src/Provider/UrlProviderInterface.php b/src/Provider/UrlProviderInterface.php new file mode 100644 index 0000000..a104c6a --- /dev/null +++ b/src/Provider/UrlProviderInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusMenuPlugin\Provider; + +interface UrlProviderInterface +{ + public function getIcon(): string; + + public function getCode(): string; + + public function getPriority(): int; + + public function getItems(string $locale, string $search = ''): array; +} diff --git a/src/Resources/config/routes/admin.yaml b/src/Resources/config/routes/admin.yaml index 4be12fd..5f2ab9c 100644 --- a/src/Resources/config/routes/admin.yaml +++ b/src/Resources/config/routes/admin.yaml @@ -111,3 +111,18 @@ monsieurbiz_menu_admin_menu_item_move_down: defaults: _controller: monsieurbiz_menu.controller.menu_item::moveDownAction _sylius: *move_sylius + +monsieurbiz_menu_admin_browser_list: + path: /ajax/menus-browser/list + methods: [GET] + defaults: + _controller: 'MonsieurBiz\SyliusMenuPlugin\Controller\BrowserController::listAction' + condition: 'request.headers.get("X-Requested-With") == "XMLHttpRequest"' + + +monsieurbiz_menu_admin_browser_list_items: + path: /ajax/menus-browser/list-items + methods: [GET] + defaults: + _controller: 'MonsieurBiz\SyliusMenuPlugin\Controller\BrowserController::listItemsAction' + condition: 'request.headers.get("X-Requested-With") == "XMLHttpRequest"' diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index 2f67d21..6bf6aa6 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -57,3 +57,7 @@ services: arguments: - '%monsieurbiz_menu.model.menu_item_translation.class%' - '%monsieurbiz_menu.form.type.menu_item_translation.validation_groups%' + + MonsieurBiz\SyliusMenuPlugin\Provider\BrowsableObjectProvider: + arguments: + - !tagged_iterator { tag: 'monsieurbiz_menu.url_provider' } diff --git a/src/Resources/config/sylius/ui.yaml b/src/Resources/config/sylius/ui.yaml index d0cbd59..565a577 100644 --- a/src/Resources/config/sylius/ui.yaml +++ b/src/Resources/config/sylius/ui.yaml @@ -34,3 +34,7 @@ sylius_ui: template: '@MonsieurBizSyliusMenuPlugin/Layout/Footer/Grid/_your_store.html.twig' customer_care: template: '@MonsieurBizSyliusMenuPlugin/Layout/Footer/Grid/_customer_care.html.twig' + sylius.admin.layout.javascripts: + blocks: + monsieurbiz_sylius_menu_init_app: + template: '@MonsieurBizSyliusMenuPlugin/Admin/Browser/app.html.twig' diff --git a/src/Resources/translations/messages.en.yaml b/src/Resources/translations/messages.en.yaml index 17434b7..28ac7c3 100644 --- a/src/Resources/translations/messages.en.yaml +++ b/src/Resources/translations/messages.en.yaml @@ -21,6 +21,14 @@ monsieurbiz_menu: help_noopener: 'Add "noopener" to the rel attribute' nofollow: 'nofollow' help_nofollow: 'Add "nofollow" to the rel attribute' + choose_target: 'Choose a target' + cancel: 'Cancel' + go_back: 'Go back' + search: 'Search' + too_many_results: 'This view is limited to %maxResults% results. Use search to filter on items.' + provider: + product: 'Product' + taxon: 'Taxon' sylius_plus: rbac: parent: diff --git a/src/Resources/translations/messages.fr.yaml b/src/Resources/translations/messages.fr.yaml index cda2074..1ea05c6 100644 --- a/src/Resources/translations/messages.fr.yaml +++ b/src/Resources/translations/messages.fr.yaml @@ -21,6 +21,14 @@ monsieurbiz_menu: help_noopener: 'Ajoute "noopener" à l''attribut rel' nofollow: 'nofollow' help_nofollow: 'Ajoute "nofollow" à l''attribut rel' + choose_target: 'Choisir une cible' + cancel: 'Annuler' + go_back: 'Retour en arrière' + search: 'Rechercher' + too_many_results: 'La vue est limitée à %maxResults% résultats. Veuillez affiner votre recherche.' + provider: + product: 'Produit' + taxon: 'Taxon' sylius_plus: rbac: parent: diff --git a/src/Resources/views/Admin/Browser/Form/_theme.html.twig b/src/Resources/views/Admin/Browser/Form/_theme.html.twig new file mode 100644 index 0000000..431015f --- /dev/null +++ b/src/Resources/views/Admin/Browser/Form/_theme.html.twig @@ -0,0 +1,11 @@ +{% block monsieurbiz_sylius_menu_url_row %} + {% set locale = get_locale_from_form(form) %} +
+{% endblock %} diff --git a/src/Resources/views/Admin/Browser/Modal/Content/Item/_showLink.html.twig b/src/Resources/views/Admin/Browser/Modal/Content/Item/_showLink.html.twig new file mode 100644 index 0000000..675b955 --- /dev/null +++ b/src/Resources/views/Admin/Browser/Modal/Content/Item/_showLink.html.twig @@ -0,0 +1,3 @@ + + + diff --git a/src/Resources/views/Admin/Browser/Modal/Content/_back.html.twig b/src/Resources/views/Admin/Browser/Modal/Content/_back.html.twig new file mode 100644 index 0000000..69fc55a --- /dev/null +++ b/src/Resources/views/Admin/Browser/Modal/Content/_back.html.twig @@ -0,0 +1,10 @@ +