diff --git a/src/Form/Type/ArticleSelectionElementType.php b/src/Form/Type/ArticleSelectionElementType.php new file mode 100644 index 0000000..b129cc5 --- /dev/null +++ b/src/Form/Type/ArticleSelectionElementType.php @@ -0,0 +1,71 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Form\Type; + +use MonsieurBiz\SyliusBlogPlugin\Entity\Article; +use MonsieurBiz\SyliusBlogPlugin\Entity\ArticleInterface; +use MonsieurBiz\SyliusBlogPlugin\Repository\ArticleRepositoryInterface; +use Sylius\Bundle\ResourceBundle\Form\DataTransformer\ResourceToIdentifierTransformer; +use Sylius\Component\Channel\Context\ChannelContextInterface; +use Sylius\Component\Locale\Context\LocaleContextInterface; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\IntegerType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\ReversedTransformer; +use Symfony\Component\Validator\Constraints as Assert; + +final class ArticleSelectionElementType extends AbstractType +{ + public function __construct( + private readonly ArticleRepositoryInterface $articleRepository, + private readonly ChannelContextInterface $channelContext, + private readonly LocaleContextInterface $localeContext, + ) { + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('article', EntityType::class, [ + 'class' => Article::class, + 'label' => 'monsieurbiz_blog.ui_element.articles_selection_ui_element.fields.article', + 'choice_label' => fn (Article $article) => $article->getTitle(), + 'choice_value' => fn (?Article $article) => $article?->getId(), + 'required' => true, + 'query_builder' => function (ArticleRepositoryInterface $articleRepository) { + return $articleRepository->createShopListQueryBuilderByType( + $this->localeContext->getLocaleCode(), + ArticleInterface::BLOG_TYPE, + $this->channelContext->getChannel(), + null + )->orderBy('translation.title'); + }, + ]) + ->add('position', IntegerType::class, [ + 'label' => 'monsieurbiz_blog.ui_element.articles_selection_ui_element.fields.position', + 'required' => true, + 'constraints' => [ + new Assert\NotBlank(), + new Assert\GreaterThan(0), + ], + ]) + ; + + $builder->get('article')->addModelTransformer( + new ReversedTransformer(new ResourceToIdentifierTransformer($this->articleRepository, 'id')), + ); + } +} diff --git a/src/Form/Type/ArticlesDisplayType.php b/src/Form/Type/ArticlesDisplayType.php new file mode 100644 index 0000000..abfd012 --- /dev/null +++ b/src/Form/Type/ArticlesDisplayType.php @@ -0,0 +1,43 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Form\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\FormBuilderInterface; + +final class ArticlesDisplayType extends AbstractType +{ + public const MULTIPLE_WITH_IMAGE = 'multiple_with_image'; + + public const MULTIPLE_WITHOUT_IMAGE = 'multiple_without_image'; + + public const SINGLE = 'single'; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameters) + */ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('display', ChoiceType::class, [ + 'label' => 'monsieurbiz_blog.articles.display.label', + 'required' => true, + 'choices' => [ + 'monsieurbiz_blog.articles.display.choices.multiple_with_image' => self::MULTIPLE_WITH_IMAGE, + 'monsieurbiz_blog.articles.display.choices.multiple_without_image' => self::MULTIPLE_WITHOUT_IMAGE, + 'monsieurbiz_blog.articles.display.choices.single' => self::SINGLE, + ], + ]) + ; + } +} diff --git a/src/Form/Type/CaseStudyElementType.php b/src/Form/Type/CaseStudyElementType.php index d884057..2a355ad 100644 --- a/src/Form/Type/CaseStudyElementType.php +++ b/src/Form/Type/CaseStudyElementType.php @@ -38,23 +38,21 @@ public function __construct( */ public function buildForm(FormBuilderInterface $builder, array $options): void { - $caseStudies = $this->articleRepository->createShopListQueryBuilderByType( - $this->localeContext->getLocaleCode(), - ArticleInterface::CASE_STUDY_TYPE, - $this->channelContext->getChannel(), - null - ); - - $caseStudies = $caseStudies->orderBy('translation.title')->getQuery()->getResult(); - $builder ->add('case_study', EntityType::class, [ 'class' => Article::class, 'label' => 'monsieurbiz_blog.ui_element.case_studies_ui_element.fields.case_study', 'choice_label' => fn (Article $caseStudy) => $caseStudy->getTitle(), - 'choice_value' => fn (?Article $caseStudy) => $caseStudy ? $caseStudy->getId() : null, + 'choice_value' => fn (?Article $caseStudy) => $caseStudy?->getId(), 'required' => true, - 'choices' => $caseStudies, + 'query_builder' => function (ArticleRepositoryInterface $articleRepository) { + return $articleRepository->createShopListQueryBuilderByType( + $this->localeContext->getLocaleCode(), + ArticleInterface::CASE_STUDY_TYPE, + $this->channelContext->getChannel(), + null + )->orderBy('translation.title'); + }, ]) ->add('position', IntegerType::class, [ 'label' => 'monsieurbiz_blog.ui_element.case_studies_ui_element.fields.position', diff --git a/src/Form/Type/UiElement/ArticlesByTagsUiElementType.php b/src/Form/Type/UiElement/ArticlesByTagsUiElementType.php new file mode 100644 index 0000000..06af902 --- /dev/null +++ b/src/Form/Type/UiElement/ArticlesByTagsUiElementType.php @@ -0,0 +1,111 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Form\Type\UiElement; + +use MonsieurBiz\SyliusBlogPlugin\Entity\Tag; +use MonsieurBiz\SyliusBlogPlugin\Form\Type\ArticlesDisplayType; +use MonsieurBiz\SyliusBlogPlugin\Repository\TagRepositoryInterface; +use MonsieurBiz\SyliusRichEditorPlugin\Attribute\AsUiElement; +use MonsieurBiz\SyliusRichEditorPlugin\Attribute\TemplatesUiElement; +use MonsieurBiz\SyliusRichEditorPlugin\Form\Type\LinkType; +use Sylius\Component\Locale\Context\LocaleContextInterface; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\CallbackTransformer; +use Symfony\Component\Form\Extension\Core\Type\IntegerType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Validator\Constraints as Assert; + +#[AsUiElement( + code: 'monsieurbiz_blog.articles_by_tags_ui_element', + icon: 'tags', + title: 'monsieurbiz_blog.ui_element.articles_by_tags_ui_element.title', + description: 'monsieurbiz_blog.ui_element.articles_by_tags_ui_element.description', + uiElement: 'MonsieurBiz\SyliusBlogPlugin\UiElement\ArticlesByTagsUiElement', + templates: new TemplatesUiElement( + adminRender: '@MonsieurBizSyliusBlogPlugin/Admin/UiElement/articles_by_tags.html.twig', + frontRender: '@MonsieurBizSyliusBlogPlugin/Shop/UiElement/articles_by_tags.html.twig', + ), + wireframe: 'articles-by-tags', + tags: [], +)] +class ArticlesByTagsUiElementType extends AbstractType +{ + public function __construct( + private readonly TagRepositoryInterface $tagRepository, + private readonly LocaleContextInterface $localeContext, + ) { + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('title', TextType::class, [ + 'label' => 'monsieurbiz_blog.ui_element.articles_by_tags_ui_element.fields.title', + 'required' => false, + ]) + ->add('display', ArticlesDisplayType::class, [ + 'label' => false, // already defined in the ArticlesDisplayType + ]) + ->add('tags', EntityType::class, [ + 'label' => 'monsieurbiz_blog.ui_element.articles_by_tags_ui_element.fields.tags', + 'required' => false, + 'class' => Tag::class, + 'choice_label' => fn (Tag $tag) => $tag->getName(), + 'choice_value' => fn (?Tag $tag) => $tag?->getId(), + 'query_builder' => function (TagRepositoryInterface $tagRepository) { + return $tagRepository->createListQueryBuilder( + $this->localeContext->getLocaleCode(), + )->orderBy('translation.name'); + }, + 'multiple' => true, + ]) + ->add('limit', IntegerType::class, [ + 'label' => 'monsieurbiz_blog.ui_element.articles_by_tags_ui_element.fields.limit', + 'help' => 'monsieurbiz_blog.ui_element.articles_by_tags_ui_element.help.limit', + 'required' => true, + 'constraints' => [ + new Assert\NotBlank(), + new Assert\GreaterThan(0), + ], + ]) + ->add('buttonLabel', TextType::class, [ + 'label' => 'monsieurbiz_blog.ui_element.articles_by_tags_ui_element.fields.button_label', + 'required' => false, + ]) + ->add('buttonUrl', LinkType::class, [ + 'label' => 'monsieurbiz_blog.ui_element.articles_by_tags_ui_element.fields.button_url', + 'required' => false, + ]) + ; + + $builder->get('tags')->addModelTransformer( + new CallbackTransformer( + function ($tagsAsArray) { + return $this->tagRepository->findBy(['id' => $tagsAsArray ?? []]); + }, + function ($tagsAsString) { + $tags = []; + foreach ($tagsAsString as $tag) { + $tags[] = $tag->getId(); + } + + return $tags; + } + ), + ); + } +} diff --git a/src/Form/Type/UiElement/ArticlesSelectionUiElementType.php b/src/Form/Type/UiElement/ArticlesSelectionUiElementType.php new file mode 100644 index 0000000..904f678 --- /dev/null +++ b/src/Form/Type/UiElement/ArticlesSelectionUiElementType.php @@ -0,0 +1,94 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Form\Type\UiElement; + +use MonsieurBiz\SyliusBlogPlugin\Form\Type\ArticlesDisplayType; +use MonsieurBiz\SyliusBlogPlugin\Form\Type\ArticleSelectionElementType; +use MonsieurBiz\SyliusRichEditorPlugin\Attribute\AsUiElement; +use MonsieurBiz\SyliusRichEditorPlugin\Attribute\TemplatesUiElement; +use MonsieurBiz\SyliusRichEditorPlugin\Form\Type\LinkType; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\CollectionType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\Validator\Constraints as Assert; + +#[AsUiElement( + code: 'monsieurbiz_blog.articles_selection_ui_element', + icon: 'newspaper', + title: 'monsieurbiz_blog.ui_element.articles_selection_ui_element.title', + description: 'monsieurbiz_blog.ui_element.articles_selection_ui_element.description', + uiElement: 'MonsieurBiz\SyliusBlogPlugin\UiElement\ArticlesSelectionUiElement', + templates: new TemplatesUiElement( + adminRender: '@MonsieurBizSyliusBlogPlugin/Admin/UiElement/articles_selection.html.twig', + frontRender: '@MonsieurBizSyliusBlogPlugin/Shop/UiElement/articles_selection.html.twig', + ), + wireframe: 'articles-selection', + tags: [], +)] +class ArticlesSelectionUiElementType extends AbstractType +{ + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('title', TextType::class, [ + 'label' => 'monsieurbiz_blog.ui_element.articles_selection_ui_element.fields.title', + 'required' => false, + ]) + ->add('display', ArticlesDisplayType::class, [ + 'label' => false, // already defined in the ArticlesDisplayType + ]) + ->add('articles', CollectionType::class, [ + 'label' => 'monsieurbiz_blog.ui_element.articles_selection_ui_element.fields.articles', + 'entry_type' => ArticleSelectionElementType::class, + 'prototype_name' => '__article_selection__', + 'allow_add' => true, + 'allow_delete' => true, + 'by_reference' => false, + 'delete_empty' => true, + 'attr' => [ + 'class' => 'ui segment secondary collection--flex', + ], + 'constraints' => [ + new Assert\Count(['min' => 1]), + ], + ]) + ->add('buttonLabel', TextType::class, [ + 'label' => 'monsieurbiz_blog.ui_element.articles_selection_ui_element.fields.button_label', + 'required' => false, + ]) + ->add('buttonUrl', LinkType::class, [ + 'label' => 'monsieurbiz_blog.ui_element.articles_selection_ui_element.fields.button_url', + 'required' => false, + ]) + ; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function finishView(FormView $view, FormInterface $form, array $options): void + { + usort($view['articles']->children, function (FormView $articleA, FormView $articleB) { + return match (true) { + !$articleA->offsetExists('position') => -1, + !$articleB->offsetExists('position') => 1, + default => $articleA['position']->vars['data'] <=> $articleB['position']->vars['data'] + }; + }); + } +} diff --git a/src/Form/Type/UiElement/CaseStudiesUiElementType.php b/src/Form/Type/UiElement/CaseStudiesUiElementType.php index 1d81755..30117eb 100644 --- a/src/Form/Type/UiElement/CaseStudiesUiElementType.php +++ b/src/Form/Type/UiElement/CaseStudiesUiElementType.php @@ -23,15 +23,15 @@ #[AsUiElement( code: 'monsieurbiz_blog.case_studies_ui_element', icon: 'crosshairs', - uiElement: 'MonsieurBiz\SyliusBlogPlugin\UiElement\CaseStudiesUiElement', title: 'monsieurbiz_blog.ui_element.case_studies_ui_element.title', description: 'monsieurbiz_blog.ui_element.case_studies_ui_element.description', + uiElement: 'MonsieurBiz\SyliusBlogPlugin\UiElement\CaseStudiesUiElement', templates: new TemplatesUiElement( adminRender: '@MonsieurBizSyliusBlogPlugin/Admin/UiElement/case_studies.html.twig', frontRender: '@MonsieurBizSyliusBlogPlugin/Shop/UiElement/case_studies.html.twig', ), - tags: [], wireframe: 'case-studies', + tags: [], )] class CaseStudiesUiElementType extends AbstractType { diff --git a/src/Repository/ArticleRepository.php b/src/Repository/ArticleRepository.php index 56fc559..23aea3b 100644 --- a/src/Repository/ArticleRepository.php +++ b/src/Repository/ArticleRepository.php @@ -60,6 +60,31 @@ public function findAllEnabledAndPublishedByTag(string $localeCode, string $type ; } + public function findAllEnabledAndPublishedByTags(string $localeCode, string $type, ChannelInterface $channel, array $tags, int $limit): array + { + $queryBuilder = $this->createListQueryBuilderByType($localeCode, $type) + ->andWhere(':channel MEMBER OF ba.channels') + ->andWhere('ba.enabled = true') + ->andWhere('ba.state = :state') + ->setParameter('channel', $channel) + ->setParameter('state', ArticleInterface::STATE_PUBLISHED) + ->setMaxResults($limit) + ->orderBy('ba.publishedAt', 'DESC') + ; + + if (!empty($tags)) { + $queryBuilder + ->andWhere(':tags MEMBER OF ba.tags') + ->setParameter('tags', $tags) + ; + } + + return $queryBuilder + ->getQuery() + ->getResult() + ; + } + public function findOnePublishedBySlug(string $slug, string $localeCode, string $type, ChannelInterface $channel): ?ArticleInterface { return $this->createListQueryBuilderByType($localeCode, $type) diff --git a/src/Repository/ArticleRepositoryInterface.php b/src/Repository/ArticleRepositoryInterface.php index 69919f3..23a4d7e 100644 --- a/src/Repository/ArticleRepositoryInterface.php +++ b/src/Repository/ArticleRepositoryInterface.php @@ -29,6 +29,11 @@ public function createShopListQueryBuilderByType(string $localeCode, string $typ */ public function findAllEnabledAndPublishedByTag(string $localeCode, string $type, ChannelInterface $channel, TagInterface $tag, int $limit): array; + /** + * @return ArticleInterface[] + */ + public function findAllEnabledAndPublishedByTags(string $localeCode, string $type, ChannelInterface $channel, array $tags, int $limit): array; + public function findOnePublishedBySlug(string $slug, string $localeCode, string $type, ChannelInterface $channel): ?ArticleInterface; public function findAllEnabledAndPublishedByAuthor(string $localeCode, string $type, ChannelInterface $channel, AuthorInterface $author, int $limit): array; diff --git a/src/Resources/translations/messages.en.yaml b/src/Resources/translations/messages.en.yaml index b3103e1..2c47ef6 100644 --- a/src/Resources/translations/messages.en.yaml +++ b/src/Resources/translations/messages.en.yaml @@ -49,7 +49,7 @@ monsieurbiz_blog: edit_case_study: Edit case study no_results_to_display: No results to display ui_element: - case_studies_ui_element: + case_studies_ui_element: title: Case studies Element description: Display a list of case studies. fields: @@ -57,6 +57,34 @@ monsieurbiz_blog: case_studies: Case studies case_study: Case study position: Position + articles_selection_ui_element: + title: Articles selection Element + description: Display a list of manually selected articles. + fields: + title: Title + articles: Articles + button_label: Button label + button_url: Button url + article: Article + position: Position + articles_by_tags_ui_element: + title: Articles by tags Element + description: Display a list of articles by tags. + fields: + title: Title + tags: Tags + limit: Limit + button_label: Button label + button_url: Button url + help: + limit: The maximum number of articles to display + articles: + display: + label: Display + choices: + multiple_with_image: Multiple with image + multiple_without_image: Multiple without image + single: Single monsieurbiz_menu: provider: diff --git a/src/Resources/translations/messages.fr.yaml b/src/Resources/translations/messages.fr.yaml index d56ef28..f9f99df 100644 --- a/src/Resources/translations/messages.fr.yaml +++ b/src/Resources/translations/messages.fr.yaml @@ -49,7 +49,7 @@ monsieurbiz_blog: edit_case_study: Modifier l'étude de cas no_results_to_display: Aucun résultat à afficher ui_element: - case_studies_ui_element: + case_studies_ui_element: title: Lame Études de cas description: Affiche une liste d'études de cas. fields: @@ -57,6 +57,34 @@ monsieurbiz_blog: case_studies: Études de cas case_study: Étude de cas position: Position + articles_selection_ui_element: + title: Lame Sélection d'articles + description: Affiche une liste d'articles sélectionné manuellement. + fields: + title: Titre + articles: Articles + button_label: Libellé du bouton + button_url: URL du bouton + article: Article + position: Position + articles_by_tags_ui_element: + title: Lame Articles par tags + description: Affiche une liste d'articles par tags. + fields: + title: Titre + tags: Tags + limit: Limite + button_label: Libellé du bouton + button_url: URL du bouton + help: + limit: Le nombre maximum d'articles à afficher + articles: + display: + label: Affichage + choices: + multiple_with_image: Multiple avec image + multiple_without_image: Multiple sans image + single: Unique monsieurbiz_menu: provider: blog_list: Liste d'articles de blog diff --git a/src/Resources/views/Admin/UiElement/articles_by_tags.html.twig b/src/Resources/views/Admin/UiElement/articles_by_tags.html.twig new file mode 100644 index 0000000..4400064 --- /dev/null +++ b/src/Resources/views/Admin/UiElement/articles_by_tags.html.twig @@ -0,0 +1,17 @@ +{# +UI Element template +type: articles_selection +element fields : + title + display + tags + limit + buttonLabel + buttonUrl +element methods: + getArticles +#} + +{% set articles = ui_element.getArticles(element.tags|default([]), element.limit|default(3)) %} + +{{ include('@MonsieurBizSyliusBlogPlugin/Admin/UiElement/articles_cards.html.twig') }} diff --git a/src/Resources/views/Admin/UiElement/articles_cards.html.twig b/src/Resources/views/Admin/UiElement/articles_cards.html.twig new file mode 100644 index 0000000..23f6033 --- /dev/null +++ b/src/Resources/views/Admin/UiElement/articles_cards.html.twig @@ -0,0 +1,94 @@ +{% if articles|length > 0 %} + {% set display = element.display.display %} +