diff --git a/CHANGELOG.md b/CHANGELOG.md index aaf8ec1..7761553 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,41 @@ ## [Unreleased] +### Added +* feat(repository): add RepositoryQuery::addCaseInsensitiveFilters() +* feat(repository): add RepositoryQuery::addForcedFilterHandler() + +### Fixed +* fix(crud/index): fix row attribute value render +* fix(crud/navigation_setting): fix form action +* fix(crud/template): use default route params + +## [v1.27.0] - 2025-12-22 +### Fixed +* fix(crud): use route params to redirect after a delation + +### Added +* add option `removeItemButton: true` when applying choices.js +* feat(builder): allow to define `allowed_widgets` in form options +* feat(collection): add delete_attr, add_attr options +* feat(builder): allow to add block between children +* feat(builder): improve UI to add new block +* feat(settings): allow to edit a setting in plain page +* feat(crud/index): allow to define row attributes + +## [v1.26.0] - 2025-03-17 +### Added +* FileUploadHandler: allow to upload multiple files +* CrudController: allow to add callables after creation, update and delation +### Fixed +* fix(crud): use context variable to retrieve the form and the form options +* fix(node): use `false` instead of `O` in query +### Changed +* CrudConfiguration::setShowActions: add context when setting this parameter + +## [v1.25.2] - 2025-02-07 +### Added +* allow to set `data-*` attributes on modal + ## [v1.25.1] - 2024-05-13 ### Added * add drag&drop in the block builder diff --git a/src/core/BuilderBlock/Block/Bootstrap/BootstrapBlock.php b/src/core/BuilderBlock/Block/Bootstrap/BootstrapBlock.php index 12fc12e..1f332ba 100644 --- a/src/core/BuilderBlock/Block/Bootstrap/BootstrapBlock.php +++ b/src/core/BuilderBlock/Block/Bootstrap/BootstrapBlock.php @@ -5,7 +5,7 @@ namespace App\Core\BuilderBlock\Block\Bootstrap; use App\Core\BuilderBlock\BuilderBlock; use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; -class BootstrapBlock extends BuilderBlock +abstract class BootstrapBlock extends BuilderBlock { public function configure() { diff --git a/src/core/BuilderBlock/Block/Bootstrap/ColumnBlock.php b/src/core/BuilderBlock/Block/Bootstrap/ColumnBlock.php index b17c101..71be682 100644 --- a/src/core/BuilderBlock/Block/Bootstrap/ColumnBlock.php +++ b/src/core/BuilderBlock/Block/Bootstrap/ColumnBlock.php @@ -17,7 +17,7 @@ class ColumnBlock extends BootstrapBlock ->setLabel('Column') ->setIsContainer(true) ->setOrder(3) - ->setClass('col-12 col-lg-2 pr-md-1') + ->setClass('col-12 col-lg-3 pr-md-1') ->setTemplate('@Core/builder_block/bootstrap/column.html.twig') ->setIcon('') ->addSetting(name: 'size', label: 'Extra small', type: 'number', attributes: ['min' => 0, 'max' => 12]) @@ -25,6 +25,7 @@ class ColumnBlock extends BootstrapBlock ->addSetting(name: 'sizeMd', label: 'Medium', type: 'number', attributes: ['min' => 0, 'max' => 12]) ->addSetting(name: 'sizeLg', label: 'Large', type: 'number', attributes: ['min' => 0, 'max' => 12]) ->addSetting(name: 'sizeXl', label: 'Extra large', type: 'number', attributes: ['min' => 0, 'max' => 12]) + ->addSetting(name: 'customClass', label: 'Custom class', type: 'text') ; } } diff --git a/src/core/BuilderBlock/Block/Bootstrap/ContainerBlock.php b/src/core/BuilderBlock/Block/Bootstrap/ContainerBlock.php index ff40827..3382d28 100644 --- a/src/core/BuilderBlock/Block/Bootstrap/ContainerBlock.php +++ b/src/core/BuilderBlock/Block/Bootstrap/ContainerBlock.php @@ -20,6 +20,7 @@ class ContainerBlock extends BootstrapBlock ->setTemplate('@Core/builder_block/bootstrap/container.html.twig') ->setIcon('') ->addSetting(name: 'isFluid', label: 'Fluid', type: 'checkbox') + ->addSetting(name: 'customClass', label: 'Custom class', type: 'text') ; } } diff --git a/src/core/BuilderBlock/Block/Bootstrap/RowBlock.php b/src/core/BuilderBlock/Block/Bootstrap/RowBlock.php index 3eb5edf..5ade3da 100644 --- a/src/core/BuilderBlock/Block/Bootstrap/RowBlock.php +++ b/src/core/BuilderBlock/Block/Bootstrap/RowBlock.php @@ -20,6 +20,7 @@ class RowBlock extends BootstrapBlock ->setIcon('') ->setTemplate('@Core/builder_block/bootstrap/row.html.twig') ->addWidget('bsColumn') + ->addSetting(name: 'customClass', label: 'Custom class', type: 'text') ; } } diff --git a/src/core/BuilderBlock/Block/Editor/EditorBlock.php b/src/core/BuilderBlock/Block/Editor/EditorBlock.php index e8e377b..3172048 100644 --- a/src/core/BuilderBlock/Block/Editor/EditorBlock.php +++ b/src/core/BuilderBlock/Block/Editor/EditorBlock.php @@ -5,10 +5,10 @@ namespace App\Core\BuilderBlock\Block\Editor; use App\Core\BuilderBlock\BuilderBlock; use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; -class EditorBlock extends BuilderBlock +abstract class EditorBlock extends BuilderBlock { public function configure() { - $this->setCategory('Editor'); + $this->setCategory('Editors'); } } diff --git a/src/core/BuilderBlock/Block/Editor/TextareaBlock.php b/src/core/BuilderBlock/Block/Editor/TextareaBlock.php index b216eb5..bec2db6 100644 --- a/src/core/BuilderBlock/Block/Editor/TextareaBlock.php +++ b/src/core/BuilderBlock/Block/Editor/TextareaBlock.php @@ -21,6 +21,7 @@ class TextareaBlock extends EditorBlock ->addSetting(name: 'nl2br', label: 'Insert line breaks', type: 'checkbox', default: true) ->addSetting(name: 'allowHtml', label: 'Allow HTML', type: 'checkbox', default: false) ->addSetting(name: 'value', type: 'textarea') + ->setPreview('value') ; } } diff --git a/src/core/BuilderBlock/Block/Editor/TinymceBlock.php b/src/core/BuilderBlock/Block/Editor/TinymceBlock.php index 05f140a..130088a 100644 --- a/src/core/BuilderBlock/Block/Editor/TinymceBlock.php +++ b/src/core/BuilderBlock/Block/Editor/TinymceBlock.php @@ -19,6 +19,7 @@ class TinymceBlock extends EditorBlock ->setIcon('') ->setTemplate('@Core/builder_block/editor/tinymce.html.twig') ->addSetting(name: 'value', type: 'textarea', attributes: ['data-tinymce' => '']) + ->setPreview('value') ; } } diff --git a/src/core/BuilderBlock/BuilderBlock.php b/src/core/BuilderBlock/BuilderBlock.php index d323cf2..e5660be 100644 --- a/src/core/BuilderBlock/BuilderBlock.php +++ b/src/core/BuilderBlock/BuilderBlock.php @@ -12,6 +12,7 @@ abstract class BuilderBlock protected array $widgets = []; protected array $vars = []; protected string $template = ''; + protected ?string $preview = null; protected bool $isContainer = false; protected ?string $icon = null; protected int $order = 1; @@ -135,19 +136,6 @@ abstract class BuilderBlock return $this->template; } - public function toArray(): array - { - return [ - 'label' => $this->getLabel(), - 'category' => $this->getCategory(), - 'isContainer' => $this->getIsContainer(), - 'widgets' => $this->getWidgets(), - 'settings' => $this->getSettings(), - 'class' => $this->getClass(), - 'icon' => $this->getIcon(), - ]; - } - public function setClass(?string $class): self { $this->class = $class; @@ -184,7 +172,19 @@ abstract class BuilderBlock return $this->order; } - public function buildVars(array $data) + public function setPreview(?string $preview): self + { + $this->preview = $preview; + + return $this; + } + + public function getPreview(): ?string + { + return $this->preview; + } + + public function buildVars(array $data, array $context) { } @@ -192,4 +192,18 @@ abstract class BuilderBlock { return $this->vars; } + + public function toArray(): array + { + return [ + 'label' => $this->getLabel(), + 'category' => $this->getCategory(), + 'isContainer' => $this->getIsContainer(), + 'widgets' => $this->getWidgets(), + 'settings' => $this->getSettings(), + 'class' => $this->getClass(), + 'icon' => $this->getIcon(), + 'preview' => $this->getPreview(), + ]; + } } diff --git a/src/core/BuilderBlock/BuilderBlockContainer.php b/src/core/BuilderBlock/BuilderBlockContainer.php index 0a2e4ff..dcda7b5 100644 --- a/src/core/BuilderBlock/BuilderBlockContainer.php +++ b/src/core/BuilderBlock/BuilderBlockContainer.php @@ -6,11 +6,20 @@ class BuilderBlockContainer { protected array $widgets = []; - public function addWidget(BuilderBlock $widget): void + public function addWidget(BuilderBlock $widget): self { $widget->configure(); $this->widgets[$widget->getName()] = $widget; + + return $this; + } + + public function removeWidget(string $name) + { + unset($this->widgets[$name]); + + return $this; } public function getWidgets(): array @@ -20,6 +29,11 @@ class BuilderBlockContainer return $this->widgets; } + public function hasWidget(string $name) + { + return isset($this->widgets[$name]); + } + public function getWidget(string $name): BuilderBlock { return $this->widgets[$name]; diff --git a/src/core/Controller/Admin/Crud/CrudController.php b/src/core/Controller/Admin/Crud/CrudController.php index 22e4f25..810087f 100644 --- a/src/core/Controller/Admin/Crud/CrudController.php +++ b/src/core/Controller/Admin/Crud/CrudController.php @@ -26,7 +26,13 @@ abstract class CrudController extends AdminController abstract protected function getConfiguration(): CrudConfiguration; - protected function doIndex(int $page, RepositoryQuery $query, Request $request, Session $session, string $context = 'index'): Response + protected function doIndex( + int $page, + RepositoryQuery $query, + Request $request, + Session $session, + string $context = 'index' + ): Response { $configuration = $this->getConfiguration(); @@ -50,13 +56,20 @@ abstract class CrudController extends AdminController ]); } - protected function doNew(EntityInterface $entity, EntityManager $entityManager, Request $request, callable $beforeCreate = null, string $context = 'new'): Response + protected function doNew( + EntityInterface $entity, + EntityManager $entityManager, + Request $request, + callable $beforeCreate = null, + callable $afterCreate = null, + string $context = 'new' + ): Response { $configuration = $this->getConfiguration(); $this->prepareEntity($entity); - $form = $this->createForm($configuration->getForm('new'), $entity, $configuration->getFormOptions($context)); + $form = $this->createForm($configuration->getForm($context), $entity, $configuration->getFormOptions($context)); if ($request->isMethod('POST')) { $form->handleRequest($request); @@ -67,6 +80,11 @@ abstract class CrudController extends AdminController } $entityManager->create($entity); + + if (null !== $afterCreate) { + call_user_func_array($afterCreate, [$entity, $form, $request]); + } + $this->addFlash('success', 'The data has been saved.'); return $this->redirectToRoute($configuration->getPageRoute('edit'), array_merge( @@ -96,13 +114,20 @@ abstract class CrudController extends AdminController ]); } - protected function doEdit(EntityInterface $entity, EntityManager $entityManager, Request $request, callable $beforeUpdate = null, string $context = 'edit'): Response + protected function doEdit( + EntityInterface $entity, + EntityManager $entityManager, + Request $request, + callable $beforeUpdate = null, + callable $afterUpdate = null, + string $context = 'edit' + ): Response { $configuration = $this->getConfiguration(); $this->prepareEntity($entity); - $form = $this->createForm($configuration->getForm('edit'), $entity, $configuration->getFormOptions($context)); + $form = $this->createForm($configuration->getForm($context), $entity, $configuration->getFormOptions($context)); if ($request->isMethod('POST')) { $form->handleRequest($request); @@ -113,6 +138,11 @@ abstract class CrudController extends AdminController } $entityManager->update($entity); + + if (null !== $afterUpdate) { + call_user_func_array($afterUpdate, [$entity, $form, $request]); + } + $this->addFlash('success', 'The data has been saved.'); return $this->redirectToRoute($configuration->getPageRoute($context), array_merge( @@ -131,7 +161,15 @@ abstract class CrudController extends AdminController ]); } - protected function doInlineEdit(string $context, string $label, EntityInterface $entity, EntityManager $entityManager, Request $request, callable $beforeUpdate = null): Response + protected function doInlineEdit( + string $context, + string $label, + EntityInterface $entity, + EntityManager $entityManager, + Request $request, + callable $beforeUpdate = null, + callable $afterUpdate = null + ): Response { $configuration = $this->getConfiguration(); @@ -192,6 +230,11 @@ abstract class CrudController extends AdminController $session->remove($lastRequestId); $entityManager->update($entity); + + if (null !== $afterUpdate) { + call_user_func_array($afterUpdate, [$entity, $form, $request]); + } + $this->addFlash('success', 'The data has been saved.'); return $this->redirect($redirectTo); @@ -217,7 +260,13 @@ abstract class CrudController extends AdminController ]); } - protected function doSort(int $page, RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session): Response + protected function doSort( + int $page, + RepositoryQuery $query, + EntityManager $entityManager, + Request $request, + Session $session + ): Response { $configuration = $this->getConfiguration(); $context = $request->query->get('context', 'index'); @@ -255,7 +304,13 @@ abstract class CrudController extends AdminController return $this->json([]); } - protected function doBatch(int $page, RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session): Response + protected function doBatch( + int $page, + RepositoryQuery $query, + EntityManager $entityManager, + Request $request, + Session $session + ): Response { $configuration = $this->getConfiguration(); $datas = $request->request->get('batch', []); @@ -326,7 +381,14 @@ abstract class CrudController extends AdminController return $this->json([]); } - protected function doDelete(EntityInterface $entity, EntityManager $entityManager, Request $request, callable $beforeDelete = null, string $route = 'index'): Response + protected function doDelete( + EntityInterface $entity, + EntityManager $entityManager, + Request $request, + callable $beforeDelete = null, + callable $afterDelete = null, + string $route = 'index' + ): Response { $configuration = $this->getConfiguration(); @@ -337,22 +399,26 @@ abstract class CrudController extends AdminController $entityManager->delete($entity); + if (null !== $afterDelete) { + call_user_func($afterDelete, $entity); + } + $this->addFlash('success', 'The data has been removed.'); } - return $this->redirectToRoute($configuration->getPageRoute($route)); + return $this->redirectToRoute($configuration->getPageRoute($route), $configuration->getPageRouteParams($route)); } protected function doFilter(Session $session, string $context = 'filter'): Response { $configuration = $this->getConfiguration(); - $type = $configuration->getForm('filter'); + $type = $configuration->getForm($context); if (null === $type) { throw $this->createNotFoundException(); } - $form = $this->createForm($type, null, $configuration->getFormOptions('filter')); + $form = $this->createForm($type, null, $configuration->getFormOptions($context)); $form->submit($session->get($form->getName(), [])); return $this->render($configuration->getView($context), [ diff --git a/src/core/Controller/Editor/BuilderBlockController.php b/src/core/Controller/Editor/BuilderBlockController.php index 9aeb0b4..2bded85 100644 --- a/src/core/Controller/Editor/BuilderBlockController.php +++ b/src/core/Controller/Editor/BuilderBlockController.php @@ -30,6 +30,7 @@ class BuilderBlockController extends AbstractController protected function translate(array $data) { $data['label'] = $this->translator->trans($data['label']); + $data['category'] = $this->translator->trans($data['category']); foreach ($data['settings'] as $key => $value) { $data['settings'][$key]['label'] = $this->translator->trans($data['settings'][$key]['label']); diff --git a/src/core/Controller/Setting/NavigationSettingAdminController.php b/src/core/Controller/Setting/NavigationSettingAdminController.php index 4a3be8f..74bbb68 100644 --- a/src/core/Controller/Setting/NavigationSettingAdminController.php +++ b/src/core/Controller/Setting/NavigationSettingAdminController.php @@ -35,6 +35,8 @@ class NavigationSettingAdminController extends AdminController $session = $request->getSession(); $lastRequestId = sprintf('setting_request_%s_%s', get_class($entity), $entity->getId()); $lastRequest = $session->get($lastRequestId); + $options = $entity->getOptions(); + $optionView = $options['view'] ?? 'modal'; if (null !== $lastRequest && !$request->isMethod('POST')) { $fakeRequest = Request::create( @@ -64,17 +66,19 @@ class NavigationSettingAdminController extends AdminController $session->set($lastRequestId, $request->request->get('form')); $this->addFlash('warning', 'The form is not valid.'); - return $this->redirect(sprintf( - '%s?data-modal=%s', - $redirectTo, - urlencode($request->getUri()) - )); + if ($optionView === 'modal') { + return $this->redirect(sprintf( + '%s?data-modal=%s', + $redirectTo, + urlencode($request->getUri()) + )); + } } return $this->render('@Core/setting/navigation_setting_admin/edit.html.twig', [ 'form' => $form->createView(), 'entity' => $entity, - 'options' => $event->getData()['options'], + 'options' => $options, 'redirectTo' => $redirectTo, ]); } diff --git a/src/core/Controller/Setting/SettingAdminController.php b/src/core/Controller/Setting/SettingAdminController.php index 317fe77..707a19e 100644 --- a/src/core/Controller/Setting/SettingAdminController.php +++ b/src/core/Controller/Setting/SettingAdminController.php @@ -55,6 +55,8 @@ class SettingAdminController extends AdminController $session = $request->getSession(); $lastRequestId = sprintf('setting_request_%s_%s', get_class($entity), $entity->getId()); $lastRequest = $session->get($lastRequestId); + $options = $entity->getOptions(); + $optionView = $options['view'] ?? 'modal'; if (null !== $lastRequest && !$request->isMethod('POST')) { $fakeRequest = Request::create( @@ -82,11 +84,13 @@ class SettingAdminController extends AdminController $session->set($lastRequestId, $request->request->get('form')); $this->addFlash('warning', 'The form is not valid.'); - return $this->redirect(sprintf( - '%s?data-modal=%s', - $redirectTo, - urlencode($request->getUri()) - )); + if ($optionView === 'modal') { + return $this->redirect(sprintf( + '%s?data-modal=%s', + $redirectTo, + urlencode($request->getUri()) + )); + } } return $this->render('@Core/setting/setting_admin/edit.html.twig', [ diff --git a/src/core/Crud/CrudConfiguration.php b/src/core/Crud/CrudConfiguration.php index badea06..c083d9d 100644 --- a/src/core/Crud/CrudConfiguration.php +++ b/src/core/Crud/CrudConfiguration.php @@ -2,6 +2,8 @@ namespace App\Core\Crud; +use App\Core\Entity\EntityInterface; + /** * class CrudConfiguration. * @@ -27,7 +29,8 @@ class CrudConfiguration protected array $isSortableCollection = []; protected string $sortableCollectionProperty = 'sortOrder'; protected ?string $defaultLocale = null; - protected bool $showActions = true; + protected array $showActions = []; + protected array $listRowAttributes = []; protected static $self; @@ -247,6 +250,26 @@ class CrudConfiguration return $this->viewDatas[$context][$name] ?? $defaultValue; } + public function setListRowAttributes(string $context, array $attributes): self + { + $this->listRowAttributes[$context] = $attributes; + + return $this; + } + + public function getListRowAttributes(string $context, EntityInterface $entity): array + { + $attributes = $this->listRowAttributes[$context] ?? []; + + foreach ($attributes as $key => $attribute) { + if (is_callable($attribute)) { + $attributes[$key] = $attribute($entity); + } + } + + return $attributes; + } + // -- public function setField(string $context, string $label, string $field, array $options): self @@ -369,15 +392,15 @@ class CrudConfiguration return $this->sortableCollectionProperty; } - public function setShowActions(bool $showActions): self + public function setShowActions(string $page, bool $showActions): self { - $this->showActions = $showActions; + $this->showActions[$page] = $showActions; return $this; } - public function getShowActions(): bool + public function getShowActions(string $page): bool { - return $this->showActions; + return $this->showActions[$page] ?? true; } } diff --git a/src/core/Entity/NavigationSetting.php b/src/core/Entity/NavigationSetting.php index bfaa2ac..8adfb36 100644 --- a/src/core/Entity/NavigationSetting.php +++ b/src/core/Entity/NavigationSetting.php @@ -26,6 +26,9 @@ class NavigationSetting implements EntityInterface #[ORM\Column(type: 'text', nullable: true)] protected $value; + #[ORM\Column(type: 'text', nullable: true)] + protected $options; + #[ORM\ManyToOne(targetEntity: Navigation::class, inversedBy: 'navigationSettings')] #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] protected $navigation; @@ -94,4 +97,16 @@ class NavigationSetting implements EntityInterface return $this; } + + public function getOptions() + { + return json_decode($this->options, true) ?? []; + } + + public function setOptions(?array $options): self + { + $this->options = json_encode($options ?? []); + + return $this; + } } diff --git a/src/core/Entity/Setting.php b/src/core/Entity/Setting.php index d4a1036..032e74a 100644 --- a/src/core/Entity/Setting.php +++ b/src/core/Entity/Setting.php @@ -25,6 +25,9 @@ class Setting implements EntityInterface #[ORM\Column(type: 'text', nullable: true)] protected $value; + #[ORM\Column(type: 'text', nullable: true)] + protected $options; + public function getId(): ?int { return $this->id; @@ -77,4 +80,16 @@ class Setting implements EntityInterface return $this; } + + public function getOptions() + { + return json_decode($this->options, true) ?? []; + } + + public function setOptions(?array $options): self + { + $this->options = json_encode($options ?? []); + + return $this; + } } diff --git a/src/core/FileManager/FsFileManager.php b/src/core/FileManager/FsFileManager.php index b49eac0..8c85407 100644 --- a/src/core/FileManager/FsFileManager.php +++ b/src/core/FileManager/FsFileManager.php @@ -233,7 +233,7 @@ class FsFileManager $directory .= '/'.trim(dirname($fullPaths[$key]), '/'); } - $this->uploadHandler->handleForm($file, $directory, null, true); + $this->uploadHandler->handleForm($file, $directory, keepOriginalFilename: true); } } diff --git a/src/core/Form/FileUploadHandler.php b/src/core/Form/FileUploadHandler.php index 5dd6c50..d3b68bf 100644 --- a/src/core/Form/FileUploadHandler.php +++ b/src/core/Form/FileUploadHandler.php @@ -20,27 +20,53 @@ class FileUploadHandler return $this; } - public function handleForm(?UploadedFile $uploadedFile, string $path, ?callable $afterUploadCallback = null, bool $keepOriginalFilename = false): void + public function handleForm( + null|array|UploadedFile $uploadedFile, + string $path, + ?callable $afterUploadCallback = null, + ?callable $afterUploadsCallback = null, + bool $keepOriginalFilename = false + ): null|array|string { if (null === $uploadedFile) { - return; + return null; } - $originalFilename = pathinfo($uploadedFile->getClientOriginalName(), PATHINFO_FILENAME); + if (is_array($uploadedFile)) { + $filenames = []; - if ($keepOriginalFilename) { - $filename = $originalFilename.'.'.$uploadedFile->guessExtension(); - } elseif (!is_callable($this->filenameGenerator)) { - $safeFilename = transliterator_transliterate('Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()', $originalFilename); - $filename = date('Ymd-his').$safeFilename.'.'.$uploadedFile->guessExtension(); + foreach ($uploadedFile as $file) { + $filename = $this->handleForm($file, $path, $afterUploadCallback, null, $keepOriginalFilename); + + if ($filename !== null) { + $filenames[] = $filename; + } + } + + if (!empty($filenames) && $afterUploadsCallback) { + $afterUploadsCallback($filenames); + } + + return $filenames; } else { - $filename = call_user_func($this->filenameGenerator, $uploadedFile); - } + $originalFilename = pathinfo($uploadedFile->getClientOriginalName(), PATHINFO_FILENAME); - $uploadedFile->move($path, $filename); + if ($keepOriginalFilename) { + $filename = $originalFilename.'.'.$uploadedFile->guessExtension(); + } elseif (!is_callable($this->filenameGenerator)) { + $safeFilename = transliterator_transliterate('Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()', $originalFilename); + $filename = date('Ymd-his').$safeFilename.'.'.$uploadedFile->guessExtension(); + } else { + $filename = call_user_func($this->filenameGenerator, $uploadedFile); + } - if ($afterUploadCallback) { - $afterUploadCallback($filename); + $uploadedFile->move($path, $filename); + + if ($afterUploadCallback) { + $afterUploadCallback($filename); + } + + return $filename; } } } diff --git a/src/core/Form/Type/BuilderType.php b/src/core/Form/Type/BuilderType.php index d1f32d5..1f2fd09 100644 --- a/src/core/Form/Type/BuilderType.php +++ b/src/core/Form/Type/BuilderType.php @@ -19,6 +19,7 @@ class BuilderType extends AbstractType parent::buildView($view, $form, $options); $view->vars = array_replace($view->vars, [ + 'allowed_widgets' => $options['allowed_widgets'], ]); } @@ -28,7 +29,10 @@ class BuilderType extends AbstractType $resolver->setDefaults([ 'compound' => false, + 'allowed_widgets' => [], ]); + + $resolver->setAllowedTypes('allowed_widgets', 'array'); } public function getBlockPrefix() diff --git a/src/core/Form/Type/CollectionType.php b/src/core/Form/Type/CollectionType.php index 3670c38..5c91d90 100644 --- a/src/core/Form/Type/CollectionType.php +++ b/src/core/Form/Type/CollectionType.php @@ -16,12 +16,23 @@ class CollectionType extends BaseCollectionType { parent::buildView($view, $form, $options); + $classes = [ + 'add_attr' => 'collection-add', + 'delete_attr' => 'text-right', + ]; + + foreach ($classes as $key => $class) { + $options[$key]['class'] = $class.' '.($options[$key]['class'] ?? ''); + } + $view->vars = array_replace($view->vars, [ 'collection_name' => $options['collection_name'], 'label_add' => $options['label_add'], 'label_delete' => $options['label_delete'], 'allow_add' => $options['allow_add'], 'allow_delete' => $options['allow_delete'], + 'add_attr' => $options['add_attr'], + 'delete_attr' => $options['delete_attr'], 'template_before_item' => $options['template_before_item'], 'template_after_item' => $options['template_after_item'], ]); @@ -37,6 +48,8 @@ class CollectionType extends BaseCollectionType 'label_delete' => 'Delete', 'template_before_item' => null, 'template_after_item' => null, + 'add_attr' => [], + 'delete_attr' => [], ]); } diff --git a/src/core/Maker/MakeBuilderBlock.php b/src/core/Maker/MakeBuilderBlock.php new file mode 100644 index 0000000..9027db4 --- /dev/null +++ b/src/core/Maker/MakeBuilderBlock.php @@ -0,0 +1,97 @@ +addArgument( + 'builder-block-class', + InputArgument::OPTIONAL, + 'Choose a name for your block class (e.g. ExampleBlock>)' + ) + ->setHelp('') + ; + } + + public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator) + { + $blockClassNameDetails = $generator->createClassNameDetails( + $input->getArgument('builder-block-class'), + 'BuilderBlock\\', + 'Block' + ); + + $templatePath = sprintf( + 'builder_block/%s.html.twig', + Str::asSnakeCase(preg_replace('/Block$/', '', $blockClassNameDetails->getShortName())) + ); + + $options = [ + 'entity' => $blockClassNameDetails->getFullName(), + 'template' => $templatePath, + 'label' => Str::asHumanWords($blockClassNameDetails->getShortName()) + ]; + + $blockPath = $generator->generateController( + $blockClassNameDetails->getFullName(), + __DIR__.'/../Resources/maker/builder/Block.tpl.php', + $options + ); + + $generator->writeChanges(); + $realTemplatePath = 'templates/'.$templatePath; + + $filesystem = new Filesystem(); + + if (!$filesystem->exists($templatePath)) { + $filesystem->mkdir(dirname($realTemplatePath)); + $filesystem->dumpFile($realTemplatePath, $this->getTemplate()); + + $io->comment(sprintf('created>: %s', $realTemplatePath)); + } + + $this->writeSuccessMessage($io); + } + + protected function getTemplate(): string + { + return <<< EOF + + {% for item in children %} + {{ item|block_to_html(context) }} + {% endfor %} + + +EOF; + } + + public function configureDependencies(DependencyBuilder $dependencies) + { + } +} diff --git a/src/core/Maker/MakePage.php b/src/core/Maker/MakePage.php index 12a02b2..5ea4316 100644 --- a/src/core/Maker/MakePage.php +++ b/src/core/Maker/MakePage.php @@ -149,6 +149,7 @@ EOF 'textarea' => null, 'choice' => 'BlockEntity\\ChoiceBlock::class', 'collection' => 'BlockEntity\\CollectionBlock::class', + 'builder' => 'BlockEntity\\BuilderBlock::class', 'editor_js_textarea' => null, 'file' => 'BlockEntity\\FileBlock::class', 'file_picker' => null, diff --git a/src/core/Murph.php b/src/core/Murph.php index 2c46c1e..e46eda8 100644 --- a/src/core/Murph.php +++ b/src/core/Murph.php @@ -3,7 +3,7 @@ namespace App\Core; if (!defined('MURPH_VERSION')) { - define('MURPH_VERSION', 'v1.25.1'); + define('MURPH_VERSION', 'v1.27.0'); } /** diff --git a/src/core/Repository/RepositoryQuery.php b/src/core/Repository/RepositoryQuery.php index 904c965..e2943b0 100644 --- a/src/core/Repository/RepositoryQuery.php +++ b/src/core/Repository/RepositoryQuery.php @@ -18,6 +18,7 @@ abstract class RepositoryQuery protected PaginatorInterface $paginator; protected string $id; protected array $forcedFilterHandlers = []; + protected array $caseInsensitiveFilters = []; public function __construct(ServiceEntityRepository $repository, string $id, PaginatorInterface $paginator = null) { @@ -88,7 +89,11 @@ abstract class RepositoryQuery $this->andWhere('.'.$name.' = :'.$name); $this->setParameter(':'.$name, $value); } elseif (is_string($value)) { - $this->andWhere('.'.$name.' LIKE :'.$name); + if (in_array($name, $this->caseInsensitiveFilters)) { + $this->andWhere(sprintf('LOWER ( .%1$s) LIKE LOWER(:%1$s)', $name)); + } else { + $this->andWhere('.'.$name.' LIKE :'.$name); + } $this->setParameter(':'.$name, '%'.$value.'%'); } else { $this->filterHandler($name, $value); @@ -142,4 +147,18 @@ abstract class RepositoryQuery protected function filterHandler(string $name, $value) { } + + protected function addCaseInsensitiveFilters(string $name): self + { + $this->caseInsensitiveFilters[] = $name; + + return $this; + } + + protected function addForcedFilterHandlers(string $name): self + { + $this->forcedFilterHandlers[] = $name; + + return $this; + } } diff --git a/src/core/Repository/Site/NodeRepository.php b/src/core/Repository/Site/NodeRepository.php index b962835..7f34466 100644 --- a/src/core/Repository/Site/NodeRepository.php +++ b/src/core/Repository/Site/NodeRepository.php @@ -18,7 +18,7 @@ class NodeRepository extends NestedTreeRepository $query = $this->createQueryBuilder('n') ->join('n.menu', 'm') ->where('n.url = :url') - ->andWhere('n.disableUrl = 0') + ->andWhere('n.disableUrl = false') ->andWhere('n.aliasNode is null') ->andWhere('m.navigation = :navigation') ->setParameter(':url', $url) diff --git a/src/core/Resources/assets/css/admin.scss b/src/core/Resources/assets/css/admin.scss index c1e0729..b11c614 100644 --- a/src/core/Resources/assets/css/admin.scss +++ b/src/core/Resources/assets/css/admin.scss @@ -761,23 +761,25 @@ label.required::after { } .builder-widget { + .container { + max-width: 100%; + } + .block { - border: 1px solid rgba(map-get($theme-colors, 'dark-blue'), 0.3); - padding: 10px; + box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 4px; + padding: 15px; border-radius: 4px; - margin-bottom: 10px; background: rgba(map-get($theme-colors, 'dark-blue'), 0.02); } > .block { - border: 1px solid map-get($theme-colors, 'dark-blue'); + box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 4px; } .block-header { .block-header-item { font-size: 12px; display: inline-block; - margin-bottom: 10px; padding: 2px 6px; border-radius: 4px; margin-right: 2px; @@ -785,21 +787,55 @@ label.required::after { } } - .block-label { - background: map-get($theme-colors, 'dark-blue'); - border: 1px solid map-get($theme-colors, 'dark-blue'); - color: lighten(map-get($theme-colors, 'dark-blue'), 100%); + .block .block-icon { + > * { + display: inline-block; + margin-right: 3px; + } } - .block-settings-inverse { - background: none; - border: 1px solid map-get($theme-colors, 'dark-blue'); - color: map-get($theme-colors, 'dark-blue'); + .builder-add { + &-top { + margin-top: 7px; + } + + &-button { + cursor: pointer; + background: rgba(map-get($theme-colors, 'dark-blue'), 0.1); + text-align: center; + padding-bottom: 5px; + margin: 8px 0; + border-radius: 4px; + + &:hover { + background: rgba(map-get($theme-colors, 'dark-blue'), 0.2); + } + + .btn { + font-size: 12px; + line-height: 14px; + padding: 3px 5px; + } + } + } + + .block-root { + border: 1px solid map-get($theme-colors, 'dark-blue'); + box-shadow: none; + } + + .block-root > .container .builder-add { + margin-top: 0; } .block-settings { padding: 4px; + margin-top: 10px; margin-bottom: 5px; + + .form-control { + margin-top: 0.5rem !important; + } } .block-id { @@ -812,4 +848,52 @@ label.required::after { min-height: 40px; } } + + .block-preview { + white-space: nowrap; + max-width: 30%; + text-overflow: ellipsis; + overflow: hidden; + } + + .builder-code-editor { + border: 0; + padding: 0; + background: none; + position: fixed; + top: 15px; + + textarea { + font-family: Monospace; + min-height: 50vh; + } + } + + .dragger { + cursor: pointer; + color: #6c757d; + border-color: #6c757d; + text-align: center; + vertical-align: middle; + } + + $block-colors: #E183F5 #E3F7C6 #82DDF5 #F5BA82 #A088A6; + $block-colors-length: length($block-colors); + + @for $i from 1 through 100 { + $block-color-index: ($block-colors-length + $i) % $block-colors-length + 1; + + .block-depth-#{$i} { + .block-label { + background: nth($block-colors, $block-color-index); + border: 1px solid darken(nth($block-colors, $block-color-index), 50%); + color: darken(nth($block-colors, $block-color-index), 50%); + } + + .builder-add-button:hover { + background: nth($block-colors, $block-color-index); + color: darken(nth($block-colors, $block-color-index), 50%); + } + } + } } diff --git a/src/core/Resources/assets/js/components/builder-block/BuilderBlock.vue b/src/core/Resources/assets/js/components/builder-block/BuilderBlock.vue index f80f969..b13233c 100644 --- a/src/core/Resources/assets/js/components/builder-block/BuilderBlock.vue +++ b/src/core/Resources/assets/js/components/builder-block/BuilderBlock.vue @@ -1,39 +1,77 @@ - - - {{ toJson(value) }} - + + + + + + + + + + + + + + + {{ toJson(value) }} + + diff --git a/src/core/Resources/assets/js/components/builder-block/BuilderBlockCreate.vue b/src/core/Resources/assets/js/components/builder-block/BuilderBlockCreate.vue index 1fcd0d5..5e035ea 100644 --- a/src/core/Resources/assets/js/components/builder-block/BuilderBlockCreate.vue +++ b/src/core/Resources/assets/js/components/builder-block/BuilderBlockCreate.vue @@ -1,77 +1,92 @@ - - - - + + + + + - + + + + + + {{ category.label }} + + + + - - - - - - {{ widget.label }} - - - - + + + + + + + {{ widget.label }} @@ -96,10 +111,20 @@ export default { type: Array, required: true }, + openedBlocks: { + type: Object, + required: true + }, + position: { + type: [String, Number], + required: true + }, }, data() { return { showPicker: false, + activeCategory: 'all', + search: '', } }, methods: { @@ -110,14 +135,25 @@ export default { settings[i] = widget.settings[i].default } - this.container.push({ + const block = { id: this.makeId(), widget: name, settings, children: [], - }) + } + + + if (this.position === 'bottom') { + this.container.push(block) + this.$emit('updateContainer', this.container) + } else if (this.position === 'top') { + this.container.unshift(block) + } else { + this.container.splice(this.position+1, 0, block) + } this.$emit('updateContainer', this.container) + this.openedBlocks[block.id] = true this.togglePicker() }, makeId() { @@ -131,17 +167,26 @@ export default { return `block-${result}` }, + matchSearch(name) { + if (!this.search.trim().length) { + return true + } + + return name.toLowerCase().includes(this.search.toLowerCase()) + }, togglePicker() { this.showPicker = !this.showPicker }, categories() { - let items = {} + let items = { + all: {label: 'All', widgets: {}}, + } for (let widgetName in this.widgets) { let value = this.widgets[widgetName] if (!value.category) { - value.category = 'none' + value.category = 'all' } if (typeof items[value.category] === 'undefined') { @@ -153,6 +198,7 @@ export default { if (!this.allowedWidgets.length || this.allowedWidgets.includes(widgetName)) { items[value.category].widgets[widgetName] = value + items['all'].widgets[widgetName] = value } } diff --git a/src/core/Resources/assets/js/components/builder-block/BuilderBlockItem.vue b/src/core/Resources/assets/js/components/builder-block/BuilderBlockItem.vue index bab4f7d..854eaee 100644 --- a/src/core/Resources/assets/js/components/builder-block/BuilderBlockItem.vue +++ b/src/core/Resources/assets/js/components/builder-block/BuilderBlockItem.vue @@ -1,88 +1,157 @@ - - - - - {{ item.id }} - - - - - - - {{ widget.label }} - - - - - - - - + + + + + + + - - - + + + + + + + + + + {{ truncate(item.settings[widget.preview]) }} + + + + + {{ item.id }} + + + + + + + + + + + + + + + - - - - + + + + + + + + + - - + + + + + + +