diff --git a/spec/Renderer/ContentElementRendererStrategySpec.php b/spec/Renderer/ContentElementRendererStrategySpec.php index 568ca110..7cd6b2c7 100644 --- a/spec/Renderer/ContentElementRendererStrategySpec.php +++ b/spec/Renderer/ContentElementRendererStrategySpec.php @@ -10,23 +10,18 @@ use Sylius\CmsPlugin\Entity\ContentConfigurationInterface; use Sylius\CmsPlugin\Entity\PageInterface; use Sylius\CmsPlugin\Renderer\ContentElement\ContentElementRendererInterface; -use Sylius\CmsPlugin\Renderer\ContentElementRendererStrategy; use Sylius\CmsPlugin\Renderer\ContentElementRendererStrategyInterface; use Sylius\CmsPlugin\Twig\Parser\ContentParserInterface; +use Sylius\Component\Locale\Context\LocaleContextInterface; final class ContentElementRendererStrategySpec extends ObjectBehavior { public function let( ContentParserInterface $contentParser, - ContentElementRendererInterface $renderer1, - ContentElementRendererInterface $renderer2, + LocaleContextInterface $localeContext, + ContentElementRendererInterface $renderer ): void { - $this->beConstructedWith($contentParser, [$renderer1, $renderer2]); - } - - public function it_is_initializable(): void - { - $this->shouldHaveType(ContentElementRendererStrategy::class); + $this->beConstructedWith($contentParser, $localeContext, [$renderer]); } public function it_implements_content_element_renderer_strategy_interface(): void @@ -34,55 +29,59 @@ public function it_implements_content_element_renderer_strategy_interface(): voi $this->shouldImplement(ContentElementRendererStrategyInterface::class); } - public function it_renders_content_elements_using_registered_renderers( - ContentParserInterface $contentParser, - ContentElementRendererInterface $renderer1, - ContentElementRendererInterface $renderer2, - BlockInterface $block, - ContentConfigurationInterface $contentElement1, - ContentConfigurationInterface $contentElement2, + public function it_renders_a_page_content_element_correctly( + PageInterface $page, + ContentConfigurationInterface $contentElement, + LocaleContextInterface $localeContext, + ContentElementRendererInterface $renderer, + ContentParserInterface $contentParser ): void { - $block->getContentElements()->willReturn( - new ArrayCollection([$contentElement1->getWrappedObject(), $contentElement2->getWrappedObject()]), - ); + $page->getContentElements()->willReturn(new ArrayCollection([$contentElement->getWrappedObject()])); + $localeContext->getLocaleCode()->willReturn('en_US'); + $contentElement->getLocale()->willReturn('en_US'); - $renderer1->supports($contentElement1)->willReturn(true); - $renderer1->supports($contentElement2)->willReturn(false); - $renderer1->render($contentElement1)->willReturn('Rendered content 1'); - $renderer2->supports($contentElement2)->willReturn(true); - $renderer2->supports($contentElement1)->willReturn(false); - $renderer2->render($contentElement2)->willReturn('Rendered content 2'); + $renderer->supports($contentElement)->willReturn(true); + $renderer->render($contentElement)->willReturn('<p>Hello World</p>'); - $expectedParsedContent = 'Parsed content after rendering'; + $contentParser->parse('

Hello World

')->willReturn('

Hello World

'); - $contentParser->parse('Rendered content 1Rendered content 2')->willReturn($expectedParsedContent); - - $this->render($block)->shouldReturn($expectedParsedContent); + $this->render($page)->shouldReturn('

Hello World

'); } - public function it_renders_content_elements_using_registered_renderers_for_page( - ContentParserInterface $contentParser, - ContentElementRendererInterface $renderer1, - ContentElementRendererInterface $renderer2, - PageInterface $page, - ContentConfigurationInterface $contentElement1, - ContentConfigurationInterface $contentElement2, + public function it_skips_content_element_with_non_matching_locale( + BlockInterface $block, + ContentConfigurationInterface $contentElement, + LocaleContextInterface $localeContext, + ContentParserInterface $contentParser ): void { - $page->getContentElements()->willReturn( - new ArrayCollection([$contentElement1->getWrappedObject(), $contentElement2->getWrappedObject()]), - ); + $block->getContentElements()->willReturn(new ArrayCollection([$contentElement])); + $localeContext->getLocaleCode()->willReturn('en_US'); + $contentElement->getLocale()->willReturn('fr_FR'); - $renderer1->supports($contentElement1)->willReturn(true); - $renderer1->supports($contentElement2)->willReturn(false); - $renderer1->render($contentElement1)->willReturn('Rendered content 1'); - $renderer2->supports($contentElement2)->willReturn(true); - $renderer2->supports($contentElement1)->willReturn(false); - $renderer2->render($contentElement2)->willReturn('Rendered content 2'); + $contentParser->parse('')->willReturn(''); + + $this->render($block)->shouldReturn(''); + } + + public function it_renders_only_supported_content_elements( + BlockInterface $block, + ContentConfigurationInterface $supportedElement, + ContentConfigurationInterface $unsupportedElement, + LocaleContextInterface $localeContext, + ContentElementRendererInterface $renderer, + ContentParserInterface $contentParser + ): void { + $block->getContentElements()->willReturn(new ArrayCollection([$supportedElement->getWrappedObject(), $unsupportedElement->getWrappedObject()])); + $localeContext->getLocaleCode()->willReturn('en_US'); + $supportedElement->getLocale()->willReturn('en_US'); + $unsupportedElement->getLocale()->willReturn('en_US'); - $expectedParsedContent = 'Parsed content after rendering'; + $renderer->supports($supportedElement)->willReturn(true); + $renderer->render($supportedElement)->willReturn('<p>Supported</p>'); + $renderer->supports($unsupportedElement)->willReturn(false); - $contentParser->parse('Rendered content 1Rendered content 2')->willReturn($expectedParsedContent); + $contentParser->parse('

Supported

')->willReturn('

Supported

'); - $this->render($page)->shouldReturn($expectedParsedContent); + $this->render($block)->shouldReturn('

Supported

'); } } diff --git a/src/Entity/ContentConfiguration.php b/src/Entity/ContentConfiguration.php index 54ec60ed..1b08cb74 100644 --- a/src/Entity/ContentConfiguration.php +++ b/src/Entity/ContentConfiguration.php @@ -12,6 +12,8 @@ class ContentConfiguration implements ContentConfigurationInterface protected array $configuration = []; + protected ?string $locale = null; + protected ?BlockInterface $block = null; protected ?PageInterface $page = null; @@ -41,6 +43,16 @@ public function setConfiguration(array $configuration): void $this->configuration = $configuration; } + public function getLocale(): ?string + { + return $this->locale; + } + + public function setLocale(?string $locale): void + { + $this->locale = $locale; + } + public function getBlock(): ?BlockInterface { return $this->block; diff --git a/src/Entity/ContentConfigurationInterface.php b/src/Entity/ContentConfigurationInterface.php index d814b223..b593a3c2 100644 --- a/src/Entity/ContentConfigurationInterface.php +++ b/src/Entity/ContentConfigurationInterface.php @@ -16,6 +16,10 @@ public function getConfiguration(): array; public function setConfiguration(array $configuration): void; + public function getLocale(): ?string; + + public function setLocale(?string $locale): void; + public function getBlock(): ?BlockInterface; public function setBlock(?BlockInterface $block): void; diff --git a/src/Form/Type/BlockType.php b/src/Form/Type/BlockType.php index b32acf3b..955ffad7 100755 --- a/src/Form/Type/BlockType.php +++ b/src/Form/Type/BlockType.php @@ -9,13 +9,32 @@ use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType; use Sylius\Bundle\TaxonomyBundle\Form\Type\TaxonAutocompleteChoiceType; use Sylius\CmsPlugin\Entity\BlockInterface; +use Sylius\Component\Locale\Model\LocaleInterface; +use Sylius\Component\Resource\Repository\RepositoryInterface; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\CollectionType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; final class BlockType extends AbstractResourceType { + private array $locales = []; + + public function __construct( + private RepositoryInterface $localeRepository, + string $dataClass, + array $validationGroups = [], + ) { + parent::__construct($dataClass, $validationGroups); + + /** @var LocaleInterface[] $locales */ + $locales = $this->localeRepository->findAll(); + foreach ($locales as $locale) { + $this->locales[$locale->getName()] = $locale->getCode(); + } + } + public function buildForm(FormBuilderInterface $builder, array $options): void { /** @var BlockInterface $block */ @@ -50,6 +69,12 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'allow_delete' => true, 'by_reference' => false, 'required' => false, + 'entry_options' => [ + 'label' => false, + ], + 'attr' => [ + 'class' => 'content-elements-container', + ], ]) ->add('products', ProductAutocompleteChoiceType::class, [ 'label' => 'sylius_cms.ui.display_for_products.label', @@ -70,7 +95,17 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'label' => false, 'mapped' => false, ]) + ->add('locale', ChoiceType::class, [ + 'choices' => $this->locales, + 'mapped' => false, + 'label' => 'sylius.ui.locale', + 'attr' => [ + 'class' => 'locale-selector', + ] + ]) ; + + PageType::addContentElementLocaleListener($builder); } public function getBlockPrefix(): string diff --git a/src/Form/Type/ContentConfigurationType.php b/src/Form/Type/ContentConfigurationType.php index ece65504..f6572e43 100644 --- a/src/Form/Type/ContentConfigurationType.php +++ b/src/Form/Type/ContentConfigurationType.php @@ -7,6 +7,7 @@ use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType; use Sylius\CmsPlugin\Entity\ContentConfigurationInterface; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; @@ -38,6 +39,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $defaultActionConfigurationType = $this->actionConfigurationTypes[$defaultActionType]; $builder + ->add('locale', HiddenType::class) ->add('type', ChoiceType::class, [ 'label' => 'sylius.ui.type', 'choices' => $this->actionTypes, diff --git a/src/Form/Type/PageType.php b/src/Form/Type/PageType.php index 528a78cb..c168697f 100755 --- a/src/Form/Type/PageType.php +++ b/src/Form/Type/PageType.php @@ -8,14 +8,35 @@ use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType; use Sylius\Bundle\ResourceBundle\Form\Type\ResourceTranslationsType; use Sylius\CmsPlugin\Form\Type\Translation\PageTranslationType; +use Sylius\Component\Locale\Model\LocaleInterface; +use Sylius\Component\Resource\Repository\RepositoryInterface; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\CollectionType; use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; final class PageType extends AbstractResourceType { + private array $locales = []; + + public function __construct( + private RepositoryInterface $localeRepository, + string $dataClass, + array $validationGroups = [], + ) { + parent::__construct($dataClass, $validationGroups); + + /** @var LocaleInterface[] $locales */ + $locales = $this->localeRepository->findAll(); + foreach ($locales as $locale) { + $this->locales[$locale->getName()] = $locale->getCode(); + } + } + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder @@ -57,12 +78,46 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'allow_delete' => true, 'by_reference' => false, 'required' => false, + 'entry_options' => [ + 'label' => false, + ], + 'attr' => [ + 'class' => 'content-elements-container', + ], ]) ->add('template', TemplatePageAutocompleteChoiceType::class, [ 'label' => false, 'mapped' => false, ]) + ->add('locale', ChoiceType::class, [ + 'choices' => $this->locales, + 'mapped' => false, + 'label' => 'sylius.ui.locale', + 'attr' => [ + 'class' => 'locale-selector', + ] + ]) ; + + self::addContentElementLocaleListener($builder); + } + + public static function addContentElementLocaleListener(FormBuilderInterface $builder): void + { + $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) { + $data = $event->getData(); + $selectedLocale = $data['locale'] ?? null; + + if (isset($data['contentElements'])) { + foreach ($data['contentElements'] as &$contentElement) { + if (empty($contentElement['locale'])) { + $contentElement['locale'] = $selectedLocale; + } + } + } + + $event->setData($data); + }); } public function getBlockPrefix(): string diff --git a/src/Migrations/Version20240912094638.php b/src/Migrations/Version20240912094638.php new file mode 100644 index 00000000..80e6e51a --- /dev/null +++ b/src/Migrations/Version20240912094638.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE sylius_cms_content_configuration ADD locale VARCHAR(255) NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE sylius_cms_content_configuration DROP locale'); + } +} diff --git a/src/Renderer/ContentElementRendererStrategy.php b/src/Renderer/ContentElementRendererStrategy.php index 2e45094e..2f4fc6bc 100644 --- a/src/Renderer/ContentElementRendererStrategy.php +++ b/src/Renderer/ContentElementRendererStrategy.php @@ -8,6 +8,7 @@ use Sylius\CmsPlugin\Entity\PageInterface; use Sylius\CmsPlugin\Renderer\ContentElement\ContentElementRendererInterface; use Sylius\CmsPlugin\Twig\Parser\ContentParserInterface; +use Sylius\Component\Locale\Context\LocaleContextInterface; final class ContentElementRendererStrategy implements ContentElementRendererStrategyInterface { @@ -16,6 +17,7 @@ final class ContentElementRendererStrategy implements ContentElementRendererStra */ public function __construct( private ContentParserInterface $contentParser, + private LocaleContextInterface $localeContext, private iterable $renderers, ) { } @@ -25,6 +27,10 @@ public function render(BlockInterface|PageInterface $item): string $content = ''; foreach ($item->getContentElements() as $contentElement) { + if ($contentElement->getLocale() !== $this->localeContext->getLocaleCode()) { + continue; + } + foreach ($this->renderers as $renderer) { if ($renderer->supports($contentElement)) { $content .= html_entity_decode($renderer->render($contentElement), \ENT_QUOTES); diff --git a/src/Resources/assets/admin/js/cms/cms-content-configuration.js b/src/Resources/assets/admin/js/cms/cms-content-configuration.js index 2544ba60..1320437a 100644 --- a/src/Resources/assets/admin/js/cms/cms-content-configuration.js +++ b/src/Resources/assets/admin/js/cms/cms-content-configuration.js @@ -1,4 +1,32 @@ $(document).ready(function() { + const localeSelector = $('.locale-selector'); + const contentElementsContainer = $('.content-elements-container'); + + function updateContentElementsVisibility() { + const selectedLocale = localeSelector.val(); + + contentElementsContainer.find('.bb-collection-item').each(function() { + const $element = $(this); + const elementLocale = $element.find('input[name$="[locale]"]').val(); + + if (elementLocale === selectedLocale) { + $element.show(); + } else { + $element.hide(); + } + }); + } + + localeSelector.on('change', function() { + updateContentElementsVisibility(); + }); + + contentElementsContainer.on('contentElementAdded', function() { + updateContentElementsVisibility(); + }); + + updateContentElementsVisibility(); + $('.cms-media-autocomplete, .sylius-autocomplete').each((index, element) => { $(element).autoComplete(); }); @@ -32,15 +60,19 @@ $(document).ready(function() { if (!$(collectionHolder).length) { return; } + $(document).on('collection-form-add', () => { $('.cms-media-autocomplete, .sylius-autocomplete').each((index, element) => { if ($._data($(element).get(0), 'events') === undefined) { $(element).autoComplete(); } }); + $(`${collectionHolder} [data-form-collection="item"]`).each((index, element) => { $(document).loadContentConfiguration(element); }); + + $('.bb-collection-item:last-child').find('input[name$="[locale]"]').val(localeSelector.val()); }); $.fn.extend({ loadContentConfiguration(target) { diff --git a/src/Resources/assets/admin/js/cms/cms-template.js b/src/Resources/assets/admin/js/cms/cms-template.js index bee12f2a..87d631dc 100644 --- a/src/Resources/assets/admin/js/cms/cms-template.js +++ b/src/Resources/assets/admin/js/cms/cms-template.js @@ -5,6 +5,11 @@ export class HandleTemplate { const cmsPageTemplate = $('#sylius_cms_page_template'); const cmsBlockTemplate = $('#sylius_cms_block_template'); + let locales = []; + $('.locale-selector option').each(function() { + locales.push($(this).val()); + }); + cmsPageTemplate.on('change', function() { if ($(this).val()) { $('#load-template-confirmation-modal').modal('show'); @@ -38,15 +43,31 @@ export class HandleTemplate { .html(''); $.each(data.content, function () { - $('[data-form-collection="add"]').trigger('click'); + locales.forEach(function (locale) { + $('[data-form-collection="add"]').trigger('click'); + }); }); const elements = $('.bb-collection-item'); + let idx = 0; $.each(data.content, function (index, element) { - setTimeout(() => { - elements.eq(index).find('select:first').val(element.type); - elements.eq(index).find('select:first').change(); - }, 300); + locales.forEach(function (locale) { + elements.eq(idx).find('select:first').val(element.type); + elements.eq(idx).find('select:first').change(); + elements.eq(idx).find('input[name$="[locale]"]').val(locale); + idx++; + }); + }); + + $('.content-elements-container').find('.bb-collection-item').each(function() { + const $element = $(this); + const elementLocale = $element.find('input[name$="[locale]"]').val(); + + if (elementLocale === $('.locale-selector').val()) { + $element.show(); + } else { + $element.hide(); + } }); } else { console.error(data.message); diff --git a/src/Resources/config/doctrine/ContentConfiguration.orm.xml b/src/Resources/config/doctrine/ContentConfiguration.orm.xml index a1822195..b46c9f87 100644 --- a/src/Resources/config/doctrine/ContentConfiguration.orm.xml +++ b/src/Resources/config/doctrine/ContentConfiguration.orm.xml @@ -18,6 +18,8 @@ + + diff --git a/src/Resources/config/services/form.xml b/src/Resources/config/services/form.xml index 473585fc..1d517afc 100644 --- a/src/Resources/config/services/form.xml +++ b/src/Resources/config/services/form.xml @@ -13,6 +13,7 @@ + %sylius_cms.model.block.class% %sylius_cms.form.type.block.validation_groups% @@ -27,6 +28,7 @@ + %sylius_cms.model.page.class% %sylius_cms.form.type.page.validation_groups% diff --git a/src/Resources/config/services/renderer.xml b/src/Resources/config/services/renderer.xml index 106b8fba..7c17f5e6 100644 --- a/src/Resources/config/services/renderer.xml +++ b/src/Resources/config/services/renderer.xml @@ -6,6 +6,7 @@ + diff --git a/src/Resources/views/Block/Crud/_form.html.twig b/src/Resources/views/Block/Crud/_form.html.twig index 31310485..d7fdf6de 100755 --- a/src/Resources/views/Block/Crud/_form.html.twig +++ b/src/Resources/views/Block/Crud/_form.html.twig @@ -35,6 +35,7 @@

{{ 'sylius_cms.ui.content_elements.title'|trans }}

{% include '@SyliusCmsPlugin/Template/form.html.twig' with {ajax_url: path('sylius_cms_admin_ajax_template_content_by_id', {'id': 'REPLACE_ID'}) } %} + {% include '@SyliusCmsPlugin/Locale/form.html.twig' %}
{{ form_row(form.contentElements) }} diff --git a/src/Resources/views/Locale/form.html.twig b/src/Resources/views/Locale/form.html.twig new file mode 100644 index 00000000..a18cb691 --- /dev/null +++ b/src/Resources/views/Locale/form.html.twig @@ -0,0 +1,8 @@ +
+
{{ form_label(form.locale) }}
+
+
+ {{ form_widget(form.locale) }} +
+
+
diff --git a/src/Resources/views/Page/Crud/_form.html.twig b/src/Resources/views/Page/Crud/_form.html.twig index d46eb10f..fb9384da 100755 --- a/src/Resources/views/Page/Crud/_form.html.twig +++ b/src/Resources/views/Page/Crud/_form.html.twig @@ -43,6 +43,7 @@

{{ 'sylius_cms.ui.content_elements.title'|trans }}

{% include '@SyliusCmsPlugin/Template/form.html.twig' with {ajax_url: path('sylius_cms_admin_ajax_template_content_by_id', {'id': 'REPLACE_ID'}) } %} + {% include '@SyliusCmsPlugin/Locale/form.html.twig' %}
{{ form_row(form.contentElements) }}