diff --git a/CHANGELOG.md b/CHANGELOG.md index da01117..7761553 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ ## [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 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/Editor/EditorBlock.php b/src/core/BuilderBlock/Block/Editor/EditorBlock.php index e89296a..3172048 100644 --- a/src/core/BuilderBlock/Block/Editor/EditorBlock.php +++ b/src/core/BuilderBlock/Block/Editor/EditorBlock.php @@ -5,7 +5,7 @@ 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() { diff --git a/src/core/Controller/Admin/Crud/CrudController.php b/src/core/Controller/Admin/Crud/CrudController.php index 7d7092f..810087f 100644 --- a/src/core/Controller/Admin/Crud/CrudController.php +++ b/src/core/Controller/Admin/Crud/CrudController.php @@ -406,7 +406,7 @@ abstract class CrudController extends AdminController $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 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 c8d8935..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. * @@ -28,6 +30,7 @@ class CrudConfiguration protected string $sortableCollectionProperty = 'sortOrder'; protected ?string $defaultLocale = null; 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 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/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/Murph.php b/src/core/Murph.php index 621f419..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.26.0'); + 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/Resources/assets/css/admin.scss b/src/core/Resources/assets/css/admin.scss index a38fb98..b11c614 100644 --- a/src/core/Resources/assets/css/admin.scss +++ b/src/core/Resources/assets/css/admin.scss @@ -761,16 +761,19 @@ 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 { @@ -784,9 +787,6 @@ label.required::after { } } - $block-colors: #E183F5 #E3F7C6 #82DDF5 #F5BA82 #A088A6; - $block-colors-length: length($block-colors); - .block .block-icon { > * { display: inline-block; @@ -794,38 +794,36 @@ label.required::after { } } - @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 .btn { - 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 { - width: 100%; - &-top { margin-top: 7px; } - .btn { - font-size: 12px; - line-height: 14px; - padding: 3px 5px; + &-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; } @@ -840,10 +838,6 @@ label.required::after { } } - .block .block { - margin-top: 10px; - } - .block-id { font-size: 12px; margin-right: 5px; @@ -874,4 +868,32 @@ label.required::after { 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 88a7f6f..b13233c 100644 --- a/src/core/Resources/assets/js/components/builder-block/BuilderBlock.vue +++ b/src/core/Resources/assets/js/components/builder-block/BuilderBlock.vue @@ -8,7 +8,7 @@ :container="value" :widgets="widgets" :openedBlocks="openedBlocks" - :allowedWidgets="[]" + :allowedWidgets="allowedWidgets" v-if="value.length > 0" position="top" /> @@ -34,16 +34,26 @@ @remove-item="removeBlock(key)" @drag-start="dragStart" @drag-end="dragEnd" - /> -