Compare commits
128 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
ac3781f01c |
|||
|
173dc17a89 |
|||
|
5ba5e93d14 |
|||
|
005dddcea2 |
|||
|
7fbbad9909 |
|||
|
0fb486a383 |
|||
|
0fa748fd35 |
|||
|
8a780be0b0 |
|||
|
2e2b0bce10 |
|||
|
f17969db04 |
|||
|
14465d4982 |
|||
|
d3e0ba7ca5 |
|||
|
5a01d76883 |
|||
|
bd5d900b82 |
|||
|
b1b01b202c |
|||
|
895de8a445 |
|||
|
19a78b9359 |
|||
|
784288ae1d |
|||
|
799ae00730 |
|||
|
63c7ef3b46 |
|||
|
b609c2fb2e |
|||
|
e851deb17f |
|||
|
ab7b15f2c0 |
|||
|
15aec91a51 |
|||
|
acf5255e4e |
|||
|
42900d7e14 |
|||
|
eb5fd22c27 |
|||
|
836e685fda |
|||
|
22e596bed9 |
|||
|
8ac148b5c0 |
|||
|
46bde2bb2d |
|||
|
f6d79ec6c2 |
|||
|
1f2f05a575 |
|||
|
b91438a647 |
|||
|
cef0179fa3 |
|||
|
8785b2dd88 |
|||
|
6da7ad5f30 |
|||
|
318ba4fded |
|||
|
b61cdbf322 |
|||
|
978fff3cdb |
|||
|
4e8d96888d |
|||
|
014e4b824f |
|||
|
6c8363a654 |
|||
|
82da6c6cf4 |
|||
|
850098d966 |
|||
|
784d89d2c3 |
|||
|
e7bf50fc87 |
|||
|
1800558624 |
|||
|
65a86c315f |
|||
| 275f12dd8d | |||
| b33235b4b2 | |||
| 1c977a91a1 | |||
| 12f3eaa6ac | |||
| 5f07ad2c28 | |||
| a791618369 | |||
| f125d04af7 | |||
| 637a19f5e1 | |||
| aac4e21a65 | |||
| c16a73b1ff | |||
| c0db55d467 | |||
| 90202475ab | |||
| 05ed084986 | |||
| 0b6155c7e0 | |||
| 6a977ca029 | |||
| 9e1d4a7ae9 | |||
| 3755fcccf1 | |||
| 9da624f363 | |||
| 4227400cf9 | |||
| 9255e69737 | |||
| e17f0beaa0 | |||
| eb376dc36c | |||
|
8e947b0b77 |
|||
| fd1a45c0e7 | |||
|
bc148f0b6b |
|||
| 4a3619919c | |||
|
4054f6ccff |
|||
| 01ecac272e | |||
|
89efd5475f |
|||
| 029d296bd8 | |||
|
1120c20f05 |
|||
| f4a70bec66 | |||
|
d4ba8dc619 |
|||
| 26e422619b | |||
|
eb3c1a8879 |
|||
| 9963c4b3cf | |||
| e322973e67 | |||
| a15cf1559b | |||
|
006b8cdbe6 |
|||
| 46f13d817d | |||
|
0d5f248ca9 |
|||
| d1ca98eddd | |||
|
62fbd936c6 |
|||
| d66fb7da78 | |||
|
57dd7da162 |
|||
| d7ce3cb12f | |||
|
fea5319dc5 |
|||
| ff0ab092e4 | |||
|
a0211026ba |
|||
| c5dde3b184 | |||
|
b1ea641374 |
|||
| 62de6416c5 | |||
|
246c249d3e |
|||
| be26d15b22 | |||
|
3f5bd4f950 |
|||
| a80dc9999c | |||
|
b9566853ef |
|||
| bc7e8ef263 | |||
|
25efd11ea3 |
|||
| e23f7d3c73 | |||
|
85f054956c |
|||
| b12f4db16f | |||
|
6bb29dd5c3 |
|||
| 7824e96bab | |||
|
56177c14da |
|||
| 4f1c5b8f14 | |||
|
e8c9520378 |
|||
| 283223446a | |||
|
46e01f504f |
|||
| a80ee03fcd | |||
|
d1649a4959 |
|||
| 38791f1d7a | |||
|
d74cd52711 |
|||
| b451df61e8 | |||
|
7c124008c0 |
|||
| 9cf95dba64 | |||
|
b680946daf |
|||
| 1995298977 | |||
|
4acba618cb |
53 changed files with 1391 additions and 334 deletions
36
CHANGELOG.md
36
CHANGELOG.md
|
|
@ -1,5 +1,41 @@
|
||||||
## [Unreleased]
|
## [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
|
## [v1.25.1] - 2024-05-13
|
||||||
### Added
|
### Added
|
||||||
* add drag&drop in the block builder
|
* add drag&drop in the block builder
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ namespace App\Core\BuilderBlock\Block\Bootstrap;
|
||||||
use App\Core\BuilderBlock\BuilderBlock;
|
use App\Core\BuilderBlock\BuilderBlock;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
|
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
|
||||||
|
|
||||||
class BootstrapBlock extends BuilderBlock
|
abstract class BootstrapBlock extends BuilderBlock
|
||||||
{
|
{
|
||||||
public function configure()
|
public function configure()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ class ColumnBlock extends BootstrapBlock
|
||||||
->setLabel('Column')
|
->setLabel('Column')
|
||||||
->setIsContainer(true)
|
->setIsContainer(true)
|
||||||
->setOrder(3)
|
->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')
|
->setTemplate('@Core/builder_block/bootstrap/column.html.twig')
|
||||||
->setIcon('<i class="fas fa-columns"></i>')
|
->setIcon('<i class="fas fa-columns"></i>')
|
||||||
->addSetting(name: 'size', label: 'Extra small', type: 'number', attributes: ['min' => 0, 'max' => 12])
|
->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: 'sizeMd', label: 'Medium', type: 'number', attributes: ['min' => 0, 'max' => 12])
|
||||||
->addSetting(name: 'sizeLg', label: 'Large', 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: 'sizeXl', label: 'Extra large', type: 'number', attributes: ['min' => 0, 'max' => 12])
|
||||||
|
->addSetting(name: 'customClass', label: 'Custom class', type: 'text')
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ class ContainerBlock extends BootstrapBlock
|
||||||
->setTemplate('@Core/builder_block/bootstrap/container.html.twig')
|
->setTemplate('@Core/builder_block/bootstrap/container.html.twig')
|
||||||
->setIcon('<i class="fas fa-th"></i>')
|
->setIcon('<i class="fas fa-th"></i>')
|
||||||
->addSetting(name: 'isFluid', label: 'Fluid', type: 'checkbox')
|
->addSetting(name: 'isFluid', label: 'Fluid', type: 'checkbox')
|
||||||
|
->addSetting(name: 'customClass', label: 'Custom class', type: 'text')
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ class RowBlock extends BootstrapBlock
|
||||||
->setIcon('<i class="fas fa-align-justify"></i>')
|
->setIcon('<i class="fas fa-align-justify"></i>')
|
||||||
->setTemplate('@Core/builder_block/bootstrap/row.html.twig')
|
->setTemplate('@Core/builder_block/bootstrap/row.html.twig')
|
||||||
->addWidget('bsColumn')
|
->addWidget('bsColumn')
|
||||||
|
->addSetting(name: 'customClass', label: 'Custom class', type: 'text')
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@ namespace App\Core\BuilderBlock\Block\Editor;
|
||||||
use App\Core\BuilderBlock\BuilderBlock;
|
use App\Core\BuilderBlock\BuilderBlock;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
|
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
|
||||||
|
|
||||||
class EditorBlock extends BuilderBlock
|
abstract class EditorBlock extends BuilderBlock
|
||||||
{
|
{
|
||||||
public function configure()
|
public function configure()
|
||||||
{
|
{
|
||||||
$this->setCategory('Editor');
|
$this->setCategory('Editors');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ class TextareaBlock extends EditorBlock
|
||||||
->addSetting(name: 'nl2br', label: 'Insert line breaks', type: 'checkbox', default: true)
|
->addSetting(name: 'nl2br', label: 'Insert line breaks', type: 'checkbox', default: true)
|
||||||
->addSetting(name: 'allowHtml', label: 'Allow HTML', type: 'checkbox', default: false)
|
->addSetting(name: 'allowHtml', label: 'Allow HTML', type: 'checkbox', default: false)
|
||||||
->addSetting(name: 'value', type: 'textarea')
|
->addSetting(name: 'value', type: 'textarea')
|
||||||
|
->setPreview('value')
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ class TinymceBlock extends EditorBlock
|
||||||
->setIcon('<i class="fas fa-pencil-alt"></i>')
|
->setIcon('<i class="fas fa-pencil-alt"></i>')
|
||||||
->setTemplate('@Core/builder_block/editor/tinymce.html.twig')
|
->setTemplate('@Core/builder_block/editor/tinymce.html.twig')
|
||||||
->addSetting(name: 'value', type: 'textarea', attributes: ['data-tinymce' => ''])
|
->addSetting(name: 'value', type: 'textarea', attributes: ['data-tinymce' => ''])
|
||||||
|
->setPreview('value')
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ abstract class BuilderBlock
|
||||||
protected array $widgets = [];
|
protected array $widgets = [];
|
||||||
protected array $vars = [];
|
protected array $vars = [];
|
||||||
protected string $template = '';
|
protected string $template = '';
|
||||||
|
protected ?string $preview = null;
|
||||||
protected bool $isContainer = false;
|
protected bool $isContainer = false;
|
||||||
protected ?string $icon = null;
|
protected ?string $icon = null;
|
||||||
protected int $order = 1;
|
protected int $order = 1;
|
||||||
|
|
@ -135,19 +136,6 @@ abstract class BuilderBlock
|
||||||
return $this->template;
|
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
|
public function setClass(?string $class): self
|
||||||
{
|
{
|
||||||
$this->class = $class;
|
$this->class = $class;
|
||||||
|
|
@ -184,7 +172,19 @@ abstract class BuilderBlock
|
||||||
return $this->order;
|
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;
|
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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,20 @@ class BuilderBlockContainer
|
||||||
{
|
{
|
||||||
protected array $widgets = [];
|
protected array $widgets = [];
|
||||||
|
|
||||||
public function addWidget(BuilderBlock $widget): void
|
public function addWidget(BuilderBlock $widget): self
|
||||||
{
|
{
|
||||||
$widget->configure();
|
$widget->configure();
|
||||||
|
|
||||||
$this->widgets[$widget->getName()] = $widget;
|
$this->widgets[$widget->getName()] = $widget;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeWidget(string $name)
|
||||||
|
{
|
||||||
|
unset($this->widgets[$name]);
|
||||||
|
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getWidgets(): array
|
public function getWidgets(): array
|
||||||
|
|
@ -20,6 +29,11 @@ class BuilderBlockContainer
|
||||||
return $this->widgets;
|
return $this->widgets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function hasWidget(string $name)
|
||||||
|
{
|
||||||
|
return isset($this->widgets[$name]);
|
||||||
|
}
|
||||||
|
|
||||||
public function getWidget(string $name): BuilderBlock
|
public function getWidget(string $name): BuilderBlock
|
||||||
{
|
{
|
||||||
return $this->widgets[$name];
|
return $this->widgets[$name];
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,13 @@ abstract class CrudController extends AdminController
|
||||||
|
|
||||||
abstract protected function getConfiguration(): CrudConfiguration;
|
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();
|
$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();
|
$configuration = $this->getConfiguration();
|
||||||
|
|
||||||
$this->prepareEntity($entity);
|
$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')) {
|
if ($request->isMethod('POST')) {
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
@ -67,6 +80,11 @@ abstract class CrudController extends AdminController
|
||||||
}
|
}
|
||||||
|
|
||||||
$entityManager->create($entity);
|
$entityManager->create($entity);
|
||||||
|
|
||||||
|
if (null !== $afterCreate) {
|
||||||
|
call_user_func_array($afterCreate, [$entity, $form, $request]);
|
||||||
|
}
|
||||||
|
|
||||||
$this->addFlash('success', 'The data has been saved.');
|
$this->addFlash('success', 'The data has been saved.');
|
||||||
|
|
||||||
return $this->redirectToRoute($configuration->getPageRoute('edit'), array_merge(
|
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();
|
$configuration = $this->getConfiguration();
|
||||||
|
|
||||||
$this->prepareEntity($entity);
|
$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')) {
|
if ($request->isMethod('POST')) {
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
@ -113,6 +138,11 @@ abstract class CrudController extends AdminController
|
||||||
}
|
}
|
||||||
|
|
||||||
$entityManager->update($entity);
|
$entityManager->update($entity);
|
||||||
|
|
||||||
|
if (null !== $afterUpdate) {
|
||||||
|
call_user_func_array($afterUpdate, [$entity, $form, $request]);
|
||||||
|
}
|
||||||
|
|
||||||
$this->addFlash('success', 'The data has been saved.');
|
$this->addFlash('success', 'The data has been saved.');
|
||||||
|
|
||||||
return $this->redirectToRoute($configuration->getPageRoute($context), array_merge(
|
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();
|
$configuration = $this->getConfiguration();
|
||||||
|
|
||||||
|
|
@ -192,6 +230,11 @@ abstract class CrudController extends AdminController
|
||||||
|
|
||||||
$session->remove($lastRequestId);
|
$session->remove($lastRequestId);
|
||||||
$entityManager->update($entity);
|
$entityManager->update($entity);
|
||||||
|
|
||||||
|
if (null !== $afterUpdate) {
|
||||||
|
call_user_func_array($afterUpdate, [$entity, $form, $request]);
|
||||||
|
}
|
||||||
|
|
||||||
$this->addFlash('success', 'The data has been saved.');
|
$this->addFlash('success', 'The data has been saved.');
|
||||||
|
|
||||||
return $this->redirect($redirectTo);
|
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();
|
$configuration = $this->getConfiguration();
|
||||||
$context = $request->query->get('context', 'index');
|
$context = $request->query->get('context', 'index');
|
||||||
|
|
@ -255,7 +304,13 @@ abstract class CrudController extends AdminController
|
||||||
return $this->json([]);
|
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();
|
$configuration = $this->getConfiguration();
|
||||||
$datas = $request->request->get('batch', []);
|
$datas = $request->request->get('batch', []);
|
||||||
|
|
@ -326,7 +381,14 @@ abstract class CrudController extends AdminController
|
||||||
return $this->json([]);
|
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();
|
$configuration = $this->getConfiguration();
|
||||||
|
|
||||||
|
|
@ -337,22 +399,26 @@ abstract class CrudController extends AdminController
|
||||||
|
|
||||||
$entityManager->delete($entity);
|
$entityManager->delete($entity);
|
||||||
|
|
||||||
|
if (null !== $afterDelete) {
|
||||||
|
call_user_func($afterDelete, $entity);
|
||||||
|
}
|
||||||
|
|
||||||
$this->addFlash('success', 'The data has been removed.');
|
$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
|
protected function doFilter(Session $session, string $context = 'filter'): Response
|
||||||
{
|
{
|
||||||
$configuration = $this->getConfiguration();
|
$configuration = $this->getConfiguration();
|
||||||
$type = $configuration->getForm('filter');
|
$type = $configuration->getForm($context);
|
||||||
|
|
||||||
if (null === $type) {
|
if (null === $type) {
|
||||||
throw $this->createNotFoundException();
|
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(), []));
|
$form->submit($session->get($form->getName(), []));
|
||||||
|
|
||||||
return $this->render($configuration->getView($context), [
|
return $this->render($configuration->getView($context), [
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ class BuilderBlockController extends AbstractController
|
||||||
protected function translate(array $data)
|
protected function translate(array $data)
|
||||||
{
|
{
|
||||||
$data['label'] = $this->translator->trans($data['label']);
|
$data['label'] = $this->translator->trans($data['label']);
|
||||||
|
$data['category'] = $this->translator->trans($data['category']);
|
||||||
|
|
||||||
foreach ($data['settings'] as $key => $value) {
|
foreach ($data['settings'] as $key => $value) {
|
||||||
$data['settings'][$key]['label'] = $this->translator->trans($data['settings'][$key]['label']);
|
$data['settings'][$key]['label'] = $this->translator->trans($data['settings'][$key]['label']);
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ class NavigationSettingAdminController extends AdminController
|
||||||
$session = $request->getSession();
|
$session = $request->getSession();
|
||||||
$lastRequestId = sprintf('setting_request_%s_%s', get_class($entity), $entity->getId());
|
$lastRequestId = sprintf('setting_request_%s_%s', get_class($entity), $entity->getId());
|
||||||
$lastRequest = $session->get($lastRequestId);
|
$lastRequest = $session->get($lastRequestId);
|
||||||
|
$options = $entity->getOptions();
|
||||||
|
$optionView = $options['view'] ?? 'modal';
|
||||||
|
|
||||||
if (null !== $lastRequest && !$request->isMethod('POST')) {
|
if (null !== $lastRequest && !$request->isMethod('POST')) {
|
||||||
$fakeRequest = Request::create(
|
$fakeRequest = Request::create(
|
||||||
|
|
@ -64,17 +66,19 @@ class NavigationSettingAdminController extends AdminController
|
||||||
$session->set($lastRequestId, $request->request->get('form'));
|
$session->set($lastRequestId, $request->request->get('form'));
|
||||||
$this->addFlash('warning', 'The form is not valid.');
|
$this->addFlash('warning', 'The form is not valid.');
|
||||||
|
|
||||||
return $this->redirect(sprintf(
|
if ($optionView === 'modal') {
|
||||||
'%s?data-modal=%s',
|
return $this->redirect(sprintf(
|
||||||
$redirectTo,
|
'%s?data-modal=%s',
|
||||||
urlencode($request->getUri())
|
$redirectTo,
|
||||||
));
|
urlencode($request->getUri())
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->render('@Core/setting/navigation_setting_admin/edit.html.twig', [
|
return $this->render('@Core/setting/navigation_setting_admin/edit.html.twig', [
|
||||||
'form' => $form->createView(),
|
'form' => $form->createView(),
|
||||||
'entity' => $entity,
|
'entity' => $entity,
|
||||||
'options' => $event->getData()['options'],
|
'options' => $options,
|
||||||
'redirectTo' => $redirectTo,
|
'redirectTo' => $redirectTo,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,8 @@ class SettingAdminController extends AdminController
|
||||||
$session = $request->getSession();
|
$session = $request->getSession();
|
||||||
$lastRequestId = sprintf('setting_request_%s_%s', get_class($entity), $entity->getId());
|
$lastRequestId = sprintf('setting_request_%s_%s', get_class($entity), $entity->getId());
|
||||||
$lastRequest = $session->get($lastRequestId);
|
$lastRequest = $session->get($lastRequestId);
|
||||||
|
$options = $entity->getOptions();
|
||||||
|
$optionView = $options['view'] ?? 'modal';
|
||||||
|
|
||||||
if (null !== $lastRequest && !$request->isMethod('POST')) {
|
if (null !== $lastRequest && !$request->isMethod('POST')) {
|
||||||
$fakeRequest = Request::create(
|
$fakeRequest = Request::create(
|
||||||
|
|
@ -82,11 +84,13 @@ class SettingAdminController extends AdminController
|
||||||
$session->set($lastRequestId, $request->request->get('form'));
|
$session->set($lastRequestId, $request->request->get('form'));
|
||||||
$this->addFlash('warning', 'The form is not valid.');
|
$this->addFlash('warning', 'The form is not valid.');
|
||||||
|
|
||||||
return $this->redirect(sprintf(
|
if ($optionView === 'modal') {
|
||||||
'%s?data-modal=%s',
|
return $this->redirect(sprintf(
|
||||||
$redirectTo,
|
'%s?data-modal=%s',
|
||||||
urlencode($request->getUri())
|
$redirectTo,
|
||||||
));
|
urlencode($request->getUri())
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->render('@Core/setting/setting_admin/edit.html.twig', [
|
return $this->render('@Core/setting/setting_admin/edit.html.twig', [
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace App\Core\Crud;
|
namespace App\Core\Crud;
|
||||||
|
|
||||||
|
use App\Core\Entity\EntityInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* class CrudConfiguration.
|
* class CrudConfiguration.
|
||||||
*
|
*
|
||||||
|
|
@ -27,7 +29,8 @@ class CrudConfiguration
|
||||||
protected array $isSortableCollection = [];
|
protected array $isSortableCollection = [];
|
||||||
protected string $sortableCollectionProperty = 'sortOrder';
|
protected string $sortableCollectionProperty = 'sortOrder';
|
||||||
protected ?string $defaultLocale = null;
|
protected ?string $defaultLocale = null;
|
||||||
protected bool $showActions = true;
|
protected array $showActions = [];
|
||||||
|
protected array $listRowAttributes = [];
|
||||||
|
|
||||||
protected static $self;
|
protected static $self;
|
||||||
|
|
||||||
|
|
@ -247,6 +250,26 @@ class CrudConfiguration
|
||||||
return $this->viewDatas[$context][$name] ?? $defaultValue;
|
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
|
public function setField(string $context, string $label, string $field, array $options): self
|
||||||
|
|
@ -369,15 +392,15 @@ class CrudConfiguration
|
||||||
return $this->sortableCollectionProperty;
|
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;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getShowActions(): bool
|
public function getShowActions(string $page): bool
|
||||||
{
|
{
|
||||||
return $this->showActions;
|
return $this->showActions[$page] ?? true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,9 @@ class NavigationSetting implements EntityInterface
|
||||||
#[ORM\Column(type: 'text', nullable: true)]
|
#[ORM\Column(type: 'text', nullable: true)]
|
||||||
protected $value;
|
protected $value;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'text', nullable: true)]
|
||||||
|
protected $options;
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: Navigation::class, inversedBy: 'navigationSettings')]
|
#[ORM\ManyToOne(targetEntity: Navigation::class, inversedBy: 'navigationSettings')]
|
||||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
protected $navigation;
|
protected $navigation;
|
||||||
|
|
@ -94,4 +97,16 @@ class NavigationSetting implements EntityInterface
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getOptions()
|
||||||
|
{
|
||||||
|
return json_decode($this->options, true) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setOptions(?array $options): self
|
||||||
|
{
|
||||||
|
$this->options = json_encode($options ?? []);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,9 @@ class Setting implements EntityInterface
|
||||||
#[ORM\Column(type: 'text', nullable: true)]
|
#[ORM\Column(type: 'text', nullable: true)]
|
||||||
protected $value;
|
protected $value;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'text', nullable: true)]
|
||||||
|
protected $options;
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
|
|
@ -77,4 +80,16 @@ class Setting implements EntityInterface
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getOptions()
|
||||||
|
{
|
||||||
|
return json_decode($this->options, true) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setOptions(?array $options): self
|
||||||
|
{
|
||||||
|
$this->options = json_encode($options ?? []);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -233,7 +233,7 @@ class FsFileManager
|
||||||
$directory .= '/'.trim(dirname($fullPaths[$key]), '/');
|
$directory .= '/'.trim(dirname($fullPaths[$key]), '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->uploadHandler->handleForm($file, $directory, null, true);
|
$this->uploadHandler->handleForm($file, $directory, keepOriginalFilename: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,27 +20,53 @@ class FileUploadHandler
|
||||||
return $this;
|
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) {
|
if (null === $uploadedFile) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$originalFilename = pathinfo($uploadedFile->getClientOriginalName(), PATHINFO_FILENAME);
|
if (is_array($uploadedFile)) {
|
||||||
|
$filenames = [];
|
||||||
|
|
||||||
if ($keepOriginalFilename) {
|
foreach ($uploadedFile as $file) {
|
||||||
$filename = $originalFilename.'.'.$uploadedFile->guessExtension();
|
$filename = $this->handleForm($file, $path, $afterUploadCallback, null, $keepOriginalFilename);
|
||||||
} elseif (!is_callable($this->filenameGenerator)) {
|
|
||||||
$safeFilename = transliterator_transliterate('Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()', $originalFilename);
|
if ($filename !== null) {
|
||||||
$filename = date('Ymd-his').$safeFilename.'.'.$uploadedFile->guessExtension();
|
$filenames[] = $filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($filenames) && $afterUploadsCallback) {
|
||||||
|
$afterUploadsCallback($filenames);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $filenames;
|
||||||
} else {
|
} 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) {
|
$uploadedFile->move($path, $filename);
|
||||||
$afterUploadCallback($filename);
|
|
||||||
|
if ($afterUploadCallback) {
|
||||||
|
$afterUploadCallback($filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $filename;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ class BuilderType extends AbstractType
|
||||||
parent::buildView($view, $form, $options);
|
parent::buildView($view, $form, $options);
|
||||||
|
|
||||||
$view->vars = array_replace($view->vars, [
|
$view->vars = array_replace($view->vars, [
|
||||||
|
'allowed_widgets' => $options['allowed_widgets'],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -28,7 +29,10 @@ class BuilderType extends AbstractType
|
||||||
|
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
'compound' => false,
|
'compound' => false,
|
||||||
|
'allowed_widgets' => [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$resolver->setAllowedTypes('allowed_widgets', 'array');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getBlockPrefix()
|
public function getBlockPrefix()
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,23 @@ class CollectionType extends BaseCollectionType
|
||||||
{
|
{
|
||||||
parent::buildView($view, $form, $options);
|
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, [
|
$view->vars = array_replace($view->vars, [
|
||||||
'collection_name' => $options['collection_name'],
|
'collection_name' => $options['collection_name'],
|
||||||
'label_add' => $options['label_add'],
|
'label_add' => $options['label_add'],
|
||||||
'label_delete' => $options['label_delete'],
|
'label_delete' => $options['label_delete'],
|
||||||
'allow_add' => $options['allow_add'],
|
'allow_add' => $options['allow_add'],
|
||||||
'allow_delete' => $options['allow_delete'],
|
'allow_delete' => $options['allow_delete'],
|
||||||
|
'add_attr' => $options['add_attr'],
|
||||||
|
'delete_attr' => $options['delete_attr'],
|
||||||
'template_before_item' => $options['template_before_item'],
|
'template_before_item' => $options['template_before_item'],
|
||||||
'template_after_item' => $options['template_after_item'],
|
'template_after_item' => $options['template_after_item'],
|
||||||
]);
|
]);
|
||||||
|
|
@ -37,6 +48,8 @@ class CollectionType extends BaseCollectionType
|
||||||
'label_delete' => 'Delete',
|
'label_delete' => 'Delete',
|
||||||
'template_before_item' => null,
|
'template_before_item' => null,
|
||||||
'template_after_item' => null,
|
'template_after_item' => null,
|
||||||
|
'add_attr' => [],
|
||||||
|
'delete_attr' => [],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
97
src/core/Maker/MakeBuilderBlock.php
Normal file
97
src/core/Maker/MakeBuilderBlock.php
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Maker;
|
||||||
|
|
||||||
|
use Doctrine\Common\Annotations\Annotation;
|
||||||
|
use Symfony\Bundle\MakerBundle\ConsoleStyle;
|
||||||
|
use Symfony\Bundle\MakerBundle\DependencyBuilder;
|
||||||
|
use Symfony\Bundle\MakerBundle\Generator;
|
||||||
|
use Symfony\Bundle\MakerBundle\InputConfiguration;
|
||||||
|
use Symfony\Bundle\MakerBundle\Maker\AbstractMaker;
|
||||||
|
use Symfony\Bundle\MakerBundle\Str;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Question\Question;
|
||||||
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
|
|
||||||
|
class MakeBuilderBlock extends AbstractMaker
|
||||||
|
{
|
||||||
|
public static function getCommandName(): string
|
||||||
|
{
|
||||||
|
return 'make:builder-block';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getCommandDescription(): string
|
||||||
|
{
|
||||||
|
return 'Creates a new builder block class';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureCommand(Command $command, InputConfiguration $inputConf)
|
||||||
|
{
|
||||||
|
$command
|
||||||
|
->addArgument(
|
||||||
|
'builder-block-class',
|
||||||
|
InputArgument::OPTIONAL,
|
||||||
|
'Choose a name for your block class (e.g. <fg=yellow>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('<fg=blue>created</>: %s', $realTemplatePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->writeSuccessMessage($io);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getTemplate(): string
|
||||||
|
{
|
||||||
|
return <<< EOF
|
||||||
|
<div id="{{ id }}">
|
||||||
|
{% for item in children %}
|
||||||
|
{{ item|block_to_html(context) }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
EOF;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureDependencies(DependencyBuilder $dependencies)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -149,6 +149,7 @@ EOF
|
||||||
'textarea' => null,
|
'textarea' => null,
|
||||||
'choice' => 'BlockEntity\\ChoiceBlock::class',
|
'choice' => 'BlockEntity\\ChoiceBlock::class',
|
||||||
'collection' => 'BlockEntity\\CollectionBlock::class',
|
'collection' => 'BlockEntity\\CollectionBlock::class',
|
||||||
|
'builder' => 'BlockEntity\\BuilderBlock::class',
|
||||||
'editor_js_textarea' => null,
|
'editor_js_textarea' => null,
|
||||||
'file' => 'BlockEntity\\FileBlock::class',
|
'file' => 'BlockEntity\\FileBlock::class',
|
||||||
'file_picker' => null,
|
'file_picker' => null,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
namespace App\Core;
|
namespace App\Core;
|
||||||
|
|
||||||
if (!defined('MURPH_VERSION')) {
|
if (!defined('MURPH_VERSION')) {
|
||||||
define('MURPH_VERSION', 'v1.25.1');
|
define('MURPH_VERSION', 'v1.27.0');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ abstract class RepositoryQuery
|
||||||
protected PaginatorInterface $paginator;
|
protected PaginatorInterface $paginator;
|
||||||
protected string $id;
|
protected string $id;
|
||||||
protected array $forcedFilterHandlers = [];
|
protected array $forcedFilterHandlers = [];
|
||||||
|
protected array $caseInsensitiveFilters = [];
|
||||||
|
|
||||||
public function __construct(ServiceEntityRepository $repository, string $id, PaginatorInterface $paginator = null)
|
public function __construct(ServiceEntityRepository $repository, string $id, PaginatorInterface $paginator = null)
|
||||||
{
|
{
|
||||||
|
|
@ -88,7 +89,11 @@ abstract class RepositoryQuery
|
||||||
$this->andWhere('.'.$name.' = :'.$name);
|
$this->andWhere('.'.$name.' = :'.$name);
|
||||||
$this->setParameter(':'.$name, $value);
|
$this->setParameter(':'.$name, $value);
|
||||||
} elseif (is_string($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.'%');
|
$this->setParameter(':'.$name, '%'.$value.'%');
|
||||||
} else {
|
} else {
|
||||||
$this->filterHandler($name, $value);
|
$this->filterHandler($name, $value);
|
||||||
|
|
@ -142,4 +147,18 @@ abstract class RepositoryQuery
|
||||||
protected function filterHandler(string $name, $value)
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ class NodeRepository extends NestedTreeRepository
|
||||||
$query = $this->createQueryBuilder('n')
|
$query = $this->createQueryBuilder('n')
|
||||||
->join('n.menu', 'm')
|
->join('n.menu', 'm')
|
||||||
->where('n.url = :url')
|
->where('n.url = :url')
|
||||||
->andWhere('n.disableUrl = 0')
|
->andWhere('n.disableUrl = false')
|
||||||
->andWhere('n.aliasNode is null')
|
->andWhere('n.aliasNode is null')
|
||||||
->andWhere('m.navigation = :navigation')
|
->andWhere('m.navigation = :navigation')
|
||||||
->setParameter(':url', $url)
|
->setParameter(':url', $url)
|
||||||
|
|
|
||||||
|
|
@ -761,23 +761,25 @@ label.required::after {
|
||||||
}
|
}
|
||||||
|
|
||||||
.builder-widget {
|
.builder-widget {
|
||||||
|
.container {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.block {
|
.block {
|
||||||
border: 1px solid rgba(map-get($theme-colors, 'dark-blue'), 0.3);
|
box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 4px;
|
||||||
padding: 10px;
|
padding: 15px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-bottom: 10px;
|
|
||||||
background: rgba(map-get($theme-colors, 'dark-blue'), 0.02);
|
background: rgba(map-get($theme-colors, 'dark-blue'), 0.02);
|
||||||
}
|
}
|
||||||
|
|
||||||
> .block {
|
> .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 {
|
||||||
.block-header-item {
|
.block-header-item {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-bottom: 10px;
|
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
|
|
@ -785,21 +787,55 @@ label.required::after {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-label {
|
.block .block-icon {
|
||||||
background: map-get($theme-colors, 'dark-blue');
|
> * {
|
||||||
border: 1px solid map-get($theme-colors, 'dark-blue');
|
display: inline-block;
|
||||||
color: lighten(map-get($theme-colors, 'dark-blue'), 100%);
|
margin-right: 3px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-settings-inverse {
|
.builder-add {
|
||||||
background: none;
|
&-top {
|
||||||
border: 1px solid map-get($theme-colors, 'dark-blue');
|
margin-top: 7px;
|
||||||
color: map-get($theme-colors, 'dark-blue');
|
}
|
||||||
|
|
||||||
|
&-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 {
|
.block-settings {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
margin-top: 10px;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
margin-top: 0.5rem !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-id {
|
.block-id {
|
||||||
|
|
@ -812,4 +848,52 @@ label.required::after {
|
||||||
min-height: 40px;
|
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%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,77 @@
|
||||||
<template>
|
<template>
|
||||||
<Draggable
|
<div
|
||||||
v-if="Object.keys(widgets).length"
|
class="block block-root"
|
||||||
v-model="value"
|
v-if="Object.keys(widgets).length && value !== null"
|
||||||
:key="blockKey"
|
|
||||||
:animation="200"
|
|
||||||
group="children"
|
|
||||||
ghost-class="ghost"
|
|
||||||
@start="dragStart"
|
|
||||||
@end="dragEnd"
|
|
||||||
handle=".dragger"
|
|
||||||
:class="{'block-show-dropzone': showDragDrop}"
|
|
||||||
class="block"
|
|
||||||
>
|
>
|
||||||
<BuilderBlockItem
|
|
||||||
v-for="(block, key) in value"
|
|
||||||
:key="block.id + '-' + key"
|
|
||||||
:item="block"
|
|
||||||
:widgets="widgets"
|
|
||||||
@remove-item="removeBlock(key)"
|
|
||||||
@drag-start="dragStart"
|
|
||||||
@drag-end="dragEnd"
|
|
||||||
/>
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<BuilderBlockCreate
|
<BuilderBlockCreate
|
||||||
:container="value"
|
:container="value"
|
||||||
:widgets="widgets"
|
:widgets="widgets"
|
||||||
:allowedWidgets="[]"
|
:openedBlocks="openedBlocks"
|
||||||
|
:allowedWidgets="allowedWidgets"
|
||||||
|
v-if="value.length > 0"
|
||||||
|
position="top"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<textarea :name="name" class="d-none">{{ toJson(value) }}</textarea>
|
<Draggable
|
||||||
</Draggable>
|
v-model="value"
|
||||||
|
:key="blockKey"
|
||||||
|
:animation="200"
|
||||||
|
group="children"
|
||||||
|
ghost-class="ghost"
|
||||||
|
@start="dragStart"
|
||||||
|
@end="dragEnd"
|
||||||
|
handle=".dragger"
|
||||||
|
:class="{'block-show-dropzone': showDragDrop}"
|
||||||
|
>
|
||||||
|
<BuilderBlockItem
|
||||||
|
v-for="(block, key) in value"
|
||||||
|
:key="block.id + '-' + key"
|
||||||
|
:item="block"
|
||||||
|
:widgets="widgets"
|
||||||
|
:openedBlocks="openedBlocks"
|
||||||
|
:depth="1"
|
||||||
|
@remove-item="removeBlock(key)"
|
||||||
|
@drag-start="dragStart"
|
||||||
|
@drag-end="dragEnd"
|
||||||
|
>
|
||||||
|
<template #action v-if="(key+1) !== value.length">
|
||||||
|
<BuilderBlockCreate
|
||||||
|
:container="value"
|
||||||
|
:widgets="widgets"
|
||||||
|
:openedBlocks="openedBlocks"
|
||||||
|
:allowedWidgets="allowedWidgets"
|
||||||
|
:position="key"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</BuilderBlockItem>
|
||||||
|
<div class="container">
|
||||||
|
<BuilderBlockCreate
|
||||||
|
:container="value"
|
||||||
|
:widgets="widgets"
|
||||||
|
:openedBlocks="openedBlocks"
|
||||||
|
:allowedWidgets="allowedWidgets"
|
||||||
|
position="bottom"
|
||||||
|
/>
|
||||||
|
<div class="text-right">
|
||||||
|
<div>
|
||||||
|
<BuilderBlockCodeEditor
|
||||||
|
ref="dialog"
|
||||||
|
:value="value"
|
||||||
|
:widgets="widgets"
|
||||||
|
@update="codeUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea :name="name" class="d-none">{{ toJson(value) }}</textarea>
|
||||||
|
</Draggable>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import BuilderBlockItem from './BuilderBlockItem'
|
import BuilderBlockItem from './BuilderBlockItem'
|
||||||
|
import BuilderBlockCodeEditor from './BuilderBlockCodeEditor'
|
||||||
import BuilderBlockCreate from './BuilderBlockCreate'
|
import BuilderBlockCreate from './BuilderBlockCreate'
|
||||||
import Routing from '../../../../../../../../../friendsofsymfony/jsrouting-bundle/Resources/public/js/router.min.js'
|
import Routing from '../../../../../../../../../friendsofsymfony/jsrouting-bundle/Resources/public/js/router.min.js'
|
||||||
import Draggable from 'vuedraggable'
|
import Draggable from 'vuedraggable'
|
||||||
|
|
@ -44,6 +82,12 @@ Routing.setRoutingData(routes)
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'BuilderBlock',
|
name: 'BuilderBlock',
|
||||||
|
components: {
|
||||||
|
BuilderBlockItem,
|
||||||
|
BuilderBlockCreate,
|
||||||
|
Draggable,
|
||||||
|
BuilderBlockCodeEditor,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
id: {
|
id: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
@ -56,12 +100,19 @@ export default {
|
||||||
initialValue: {
|
initialValue: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
|
},
|
||||||
|
allowedWidgets: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
value: this.initialValue,
|
value: null,
|
||||||
|
nextValue: null,
|
||||||
widgets: {},
|
widgets: {},
|
||||||
|
openedBlocks: {},
|
||||||
blockKey: 0,
|
blockKey: 0,
|
||||||
showDragDrop: false,
|
showDragDrop: false,
|
||||||
}
|
}
|
||||||
|
|
@ -73,6 +124,9 @@ export default {
|
||||||
triggerBuilderBlockEvent() {
|
triggerBuilderBlockEvent() {
|
||||||
document.querySelector('body').dispatchEvent(new Event('builder_block.update'))
|
document.querySelector('body').dispatchEvent(new Event('builder_block.update'))
|
||||||
},
|
},
|
||||||
|
codeUpdate(nextValue) {
|
||||||
|
this.value = nextValue
|
||||||
|
},
|
||||||
removeBlock(key) {
|
removeBlock(key) {
|
||||||
let newValue = []
|
let newValue = []
|
||||||
|
|
||||||
|
|
@ -94,19 +148,42 @@ export default {
|
||||||
|
|
||||||
++this.blockKey
|
++this.blockKey
|
||||||
},
|
},
|
||||||
},
|
fixSettings(data) {
|
||||||
components: {
|
if (Array.isArray(data)) {
|
||||||
BuilderBlockItem,
|
data.forEach((value, key) => {
|
||||||
BuilderBlockCreate,
|
data[key] = this.fixSettings(value)
|
||||||
Draggable,
|
})
|
||||||
|
} else {
|
||||||
|
const widget = this.widgets[data.widget]
|
||||||
|
|
||||||
|
if (!widget) {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextSettings = {}
|
||||||
|
|
||||||
|
for (let setting in widget.settings) {
|
||||||
|
if (data.settings.hasOwnProperty(setting)) {
|
||||||
|
nextSettings[setting] = data.settings[setting]
|
||||||
|
} else {
|
||||||
|
nextSettings[setting] = widget.settings[setting].default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data.settings = nextSettings
|
||||||
|
data.children = this.fixSettings(data.children)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.triggerBuilderBlockEvent()
|
|
||||||
const that = this
|
const that = this
|
||||||
|
|
||||||
axios.get(Routing.generate('admin_editor_builder_block_widgets'))
|
axios.get(Routing.generate('admin_editor_builder_block_widgets'))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
that.widgets = response.data
|
that.widgets = response.data
|
||||||
|
that.value = that.fixSettings(that.initialValue)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
v-on:click="open"
|
||||||
|
>
|
||||||
|
<i class="fas fa-code"></i>
|
||||||
|
</button>
|
||||||
|
<dialog ref="dialog" class="modal-dialog modal-dialog-large builder-code-editor">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Code editor</h5>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="close"
|
||||||
|
v-on:click="close"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<textarea
|
||||||
|
class="form-control"
|
||||||
|
rows="10"
|
||||||
|
ref="codeEditor"
|
||||||
|
v-model="nextValue"
|
||||||
|
>
|
||||||
|
</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
v-on:click="close"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary"
|
||||||
|
v-on:click="checkAndSaveNextValue"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'BuilderBlockCodeEditor',
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
widgets: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
nextValue: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toJson(value) {
|
||||||
|
return JSON.stringify(value, null, 2)
|
||||||
|
},
|
||||||
|
open() {
|
||||||
|
this.nextValue = this.toJson(this.value)
|
||||||
|
this.$refs.dialog.showModal()
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
this.$refs.codeEditor.classList.toggle('is-invalid', false)
|
||||||
|
this.$refs.dialog.close()
|
||||||
|
},
|
||||||
|
isNextValueItemValueValid(item) {
|
||||||
|
const hasId = typeof item.id === 'string'
|
||||||
|
const hasWidget = typeof item.widget === 'string' && this.widgets[item.widget]
|
||||||
|
const hasSettings = typeof item.settings === 'object'
|
||||||
|
const hasChildren = Array.isArray(item.children)
|
||||||
|
|
||||||
|
if (!hasId || !hasWidget || !hasSettings || !hasChildren) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let child of item.children) {
|
||||||
|
if (!this.isNextValueItemValueValid(child)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
updateNextValueRecursiveIds(data) {
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
data.forEach((value, key) => {
|
||||||
|
data[key] = this.updateNextValueRecursiveIds(value)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
data.id = this.makeId()
|
||||||
|
data.children = this.updateNextValueRecursiveIds(data.children)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
checkAndSaveNextValue() {
|
||||||
|
this.$refs.codeEditor.classList.toggle('is-invalid', false)
|
||||||
|
let hasError = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
let data = JSON.parse(this.nextValue)
|
||||||
|
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
hasError = true
|
||||||
|
} else {
|
||||||
|
for (let item of data) {
|
||||||
|
if (!this.isNextValueItemValueValid(item)) {
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasError) {
|
||||||
|
this.$emit('update', this.updateNextValueRecursiveIds(data))
|
||||||
|
this.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.$refs.codeEditor.classList.toggle('is-invalid', hasError)
|
||||||
|
},
|
||||||
|
makeId() {
|
||||||
|
let result = ''
|
||||||
|
const characters = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
||||||
|
const charactersLength = characters.length
|
||||||
|
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
result += characters.charAt(Math.floor(Math.random() * charactersLength))
|
||||||
|
}
|
||||||
|
|
||||||
|
return `block-${result}`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
},
|
||||||
|
updated() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -1,77 +1,92 @@
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.categories {
|
.builder-block-picker {
|
||||||
padding: 10px;
|
padding: 8px;
|
||||||
text-align: left;
|
border: 1px solid #333;
|
||||||
}
|
|
||||||
|
|
||||||
.category {
|
|
||||||
padding: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-label {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget {
|
|
||||||
min-width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-content {
|
|
||||||
background: #fff;
|
|
||||||
padding: 10px;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-block-picker-menu {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-block-picker-widgets {
|
||||||
|
width: calc(100% - 150px - 10px);
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-right: 5px;
|
width: 100%;
|
||||||
margin-bottom: 5px;
|
|
||||||
border: 1px solid #1e2430;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-icon {
|
.widget-icon {
|
||||||
margin-top: 5px;
|
margin-right: 3px;
|
||||||
font-size: 25px;
|
}
|
||||||
|
|
||||||
|
.widget {
|
||||||
|
background: #fff;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 7px;
|
||||||
|
margin-bottom: 9px;
|
||||||
|
border: 1px solid #b4b4b4;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget:hover {
|
.widget:hover {
|
||||||
background: #eee;
|
background: #eee;
|
||||||
|
border: 1px solid #1e2430;
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-label {
|
.search {
|
||||||
font-weight: bold;
|
max-width: 300px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="builder-add">
|
<div class="builder-add" :class="{'builder-add-top': position === 'top'}">
|
||||||
<span class="btn btn-sm btn-secondary" v-on:click="togglePicker">
|
<div class="builder-add-button" v-on:click="togglePicker">
|
||||||
<span class="fa fa-plus"></span>
|
+
|
||||||
</span>
|
</div>
|
||||||
|
|
||||||
<div class="categories mt-2" :class="{'d-none': !showPicker}">
|
<div class="builder-block-picker mt-2 row" :class="{'d-none': !showPicker}">
|
||||||
|
<div class="col-auto builder-block-picker-menu">
|
||||||
|
<ul class="nav nav-pills pl-0">
|
||||||
|
<li
|
||||||
|
v-for="(category, key) in categories()"
|
||||||
|
v-if="Object.keys(category.widgets).length"
|
||||||
|
class="nav-item d-block"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="nav-link d-block mb-1"
|
||||||
|
:class="{'active': activeCategory == key}"
|
||||||
|
v-on:click="activeCategory = key"
|
||||||
|
>
|
||||||
|
{{ category.label }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="category in categories()"
|
v-for="(category, key) in categories()"
|
||||||
v-if="Object.keys(category.widgets).length"
|
v-if="Object.keys(category.widgets).length"
|
||||||
class="category row"
|
class="col-auto builder-block-picker-widgets"
|
||||||
|
:class="{'d-none': activeCategory !== key}"
|
||||||
>
|
>
|
||||||
<div
|
<div class="row">
|
||||||
v-if="category.label != 'none'"
|
<div class="col-12 mb-4">
|
||||||
v-text="category.label"
|
<input v-model="search" placeholder="Type to search..." class="form-control search">
|
||||||
class="category-label col-12"
|
</div>
|
||||||
></div>
|
<div
|
||||||
|
v-for="(widget, name) in category.widgets"
|
||||||
<div
|
v-on:click="add(name, widget)"
|
||||||
v-for="(widget, name) in category.widgets"
|
v-if="matchSearch(widget.label) || matchSearch(name)"
|
||||||
v-on:click="add(name, widget)"
|
class="widget col-auto"
|
||||||
class="widget col-auto"
|
>
|
||||||
>
|
<span class="widget-icon" v-if="widget.icon" v-html="widget.icon"></span>
|
||||||
<div class="widget-content">
|
{{ widget.label }}
|
||||||
<div class="widget-label">
|
|
||||||
{{ widget.label }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="widget-icon" v-if="widget.icon" v-html="widget.icon">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -96,10 +111,20 @@ export default {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
openedBlocks: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showPicker: false,
|
showPicker: false,
|
||||||
|
activeCategory: 'all',
|
||||||
|
search: '',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -110,14 +135,25 @@ export default {
|
||||||
settings[i] = widget.settings[i].default
|
settings[i] = widget.settings[i].default
|
||||||
}
|
}
|
||||||
|
|
||||||
this.container.push({
|
const block = {
|
||||||
id: this.makeId(),
|
id: this.makeId(),
|
||||||
widget: name,
|
widget: name,
|
||||||
settings,
|
settings,
|
||||||
children: [],
|
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.$emit('updateContainer', this.container)
|
||||||
|
this.openedBlocks[block.id] = true
|
||||||
this.togglePicker()
|
this.togglePicker()
|
||||||
},
|
},
|
||||||
makeId() {
|
makeId() {
|
||||||
|
|
@ -131,17 +167,26 @@ export default {
|
||||||
|
|
||||||
return `block-${result}`
|
return `block-${result}`
|
||||||
},
|
},
|
||||||
|
matchSearch(name) {
|
||||||
|
if (!this.search.trim().length) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return name.toLowerCase().includes(this.search.toLowerCase())
|
||||||
|
},
|
||||||
togglePicker() {
|
togglePicker() {
|
||||||
this.showPicker = !this.showPicker
|
this.showPicker = !this.showPicker
|
||||||
},
|
},
|
||||||
categories() {
|
categories() {
|
||||||
let items = {}
|
let items = {
|
||||||
|
all: {label: 'All', widgets: {}},
|
||||||
|
}
|
||||||
|
|
||||||
for (let widgetName in this.widgets) {
|
for (let widgetName in this.widgets) {
|
||||||
let value = this.widgets[widgetName]
|
let value = this.widgets[widgetName]
|
||||||
|
|
||||||
if (!value.category) {
|
if (!value.category) {
|
||||||
value.category = 'none'
|
value.category = 'all'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof items[value.category] === 'undefined') {
|
if (typeof items[value.category] === 'undefined') {
|
||||||
|
|
@ -153,6 +198,7 @@ export default {
|
||||||
|
|
||||||
if (!this.allowedWidgets.length || this.allowedWidgets.includes(widgetName)) {
|
if (!this.allowedWidgets.length || this.allowedWidgets.includes(widgetName)) {
|
||||||
items[value.category].widgets[widgetName] = value
|
items[value.category].widgets[widgetName] = value
|
||||||
|
items['all'].widgets[widgetName] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,88 +1,157 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="block" v-if="widget" :key="blockKey">
|
<div>
|
||||||
<div class="block-header">
|
<div
|
||||||
<div class="float-right">
|
class="block"
|
||||||
<span class="block-id">
|
:class="'block-depth-' + depth"
|
||||||
{{ item.id }}
|
v-if="widget"
|
||||||
</span>
|
:key="blockKey"
|
||||||
<div class="block-header-item text-white bg-danger" v-on:click="removeMe(item)">
|
>
|
||||||
<span class="fa fa-trash"></span>
|
<div class="block-header d-flex justify-content-between">
|
||||||
</div>
|
<div>
|
||||||
</div>
|
<div
|
||||||
<div
|
class="block-header-item block-label"
|
||||||
class="block-header-item block-label"
|
:title="item.widget"
|
||||||
:title="item.widget"
|
>
|
||||||
>
|
<span
|
||||||
{{ widget.label }}
|
class="block-icon"
|
||||||
</div>
|
v-if="widget.icon"
|
||||||
<div
|
v-html="widget.icon"
|
||||||
class="block-header-item block-settings-inverse"
|
>
|
||||||
v-on:click="toggleSettings"
|
</span>
|
||||||
v-if="Object.keys(widget.settings).length"
|
|
||||||
>
|
|
||||||
<span class="fa fa-cog"></span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="block-header-item block-settings-inverse dragger"
|
|
||||||
>
|
|
||||||
<span class="fa fa-arrows-alt"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="block-settings" v-if="Object.keys(widget.settings).length" :class="{'d-none': !showSettings}">
|
{{ widget.label }}
|
||||||
<div class="row">
|
</div>
|
||||||
<BuilderBlockSetting
|
|
||||||
class="mb-0"
|
<button
|
||||||
v-for="(params, setting) in widget.settings"
|
type="button"
|
||||||
:key="item.id + '-' + setting"
|
class="block-header-item btn btn-sm btn-outline-secondary"
|
||||||
:class="widget.class"
|
v-on:click="toggleSettings"
|
||||||
:item="item"
|
v-if="Object.keys(widget.settings).length"
|
||||||
:params="params"
|
>
|
||||||
:setting="setting"
|
<span class="fa fa-cog"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span class="fa fa-arrows-alt dragger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="widget.preview && typeof item.settings[widget.preview] == 'string'"
|
||||||
|
class="block-preview"
|
||||||
|
>
|
||||||
|
{{ truncate(item.settings[widget.preview]) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="block-id">
|
||||||
|
{{ item.id }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="block-header-item btn btn-sm text-white bg-danger"
|
||||||
|
v-on:click="removeMe(item)"
|
||||||
|
>
|
||||||
|
<span class="fa fa-trash"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block-settings" v-if="Object.keys(widget.settings).length" :class="{'d-none': !showSettings}">
|
||||||
|
<div class="row">
|
||||||
|
<BuilderBlockSetting
|
||||||
|
class="mb-0"
|
||||||
|
v-for="(params, setting) in widget.settings"
|
||||||
|
:key="item.id + '-' + setting"
|
||||||
|
:class="widget.class"
|
||||||
|
:item="item"
|
||||||
|
:params="params"
|
||||||
|
:setting="setting"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="widget.isContainer" class="container">
|
||||||
|
<BuilderBlockCreate
|
||||||
|
:container="item.children"
|
||||||
|
:widgets="widgets"
|
||||||
|
:openedBlocks="openedBlocks"
|
||||||
|
:allowedWidgets="widget.widgets"
|
||||||
|
v-if="item.children.length > 0"
|
||||||
|
position="top"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Draggable
|
<Draggable
|
||||||
v-if="widget.isContainer"
|
v-if="widget.isContainer"
|
||||||
v-model="item.children"
|
v-model="item.children"
|
||||||
ghost-class="ghost"
|
ghost-class="ghost"
|
||||||
group="children"
|
group="children"
|
||||||
@start="dragStart"
|
@start="dragStart"
|
||||||
@end="dragEnd"
|
@end="dragEnd"
|
||||||
:animation="200"
|
:animation="200"
|
||||||
handle=".dragger"
|
handle=".dragger"
|
||||||
class="block-dropzone"
|
class="block-dropzone"
|
||||||
>
|
>
|
||||||
<BuilderBlockItem
|
<template v-if="item.children !== null && item.children.length > 0">
|
||||||
v-if="item.children !== null && item.children.length > 0"
|
<BuilderBlockItem
|
||||||
v-for="(child, key) in item.children"
|
v-for="(child, key) in item.children"
|
||||||
:key="child.id"
|
:key="child.id"
|
||||||
:item="child"
|
:item="child"
|
||||||
:widgets="widgets"
|
:widgets="widgets"
|
||||||
@remove-item="removeBlock(key)"
|
:openedBlocks="openedBlocks"
|
||||||
@drag-start="dragStart"
|
:depth="depth + 1"
|
||||||
@drag-end="dragEnd"
|
@remove-item="removeBlock(key)"
|
||||||
/>
|
@drag-start="dragStart"
|
||||||
</Draggable>
|
@drag-end="dragEnd"
|
||||||
|
>
|
||||||
|
<template #action v-if="(key+1) !== item.children.length">
|
||||||
|
<BuilderBlockCreate
|
||||||
|
:container="item.children"
|
||||||
|
:widgets="widgets"
|
||||||
|
:position="key"
|
||||||
|
:openedBlocks="openedBlocks"
|
||||||
|
:allowedWidgets="widget.widgets"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</BuilderBlockItem>
|
||||||
|
</template>
|
||||||
|
</Draggable>
|
||||||
|
|
||||||
<div v-if="widget.isContainer" class="container">
|
<div v-if="widget.isContainer" class="container">
|
||||||
<BuilderBlockCreate
|
<BuilderBlockCreate
|
||||||
:container="item.children"
|
:container="item.children"
|
||||||
:widgets="widgets"
|
:widgets="widgets"
|
||||||
:allowedWidgets="widget.widgets"
|
:openedBlocks="openedBlocks"
|
||||||
/>
|
:allowedWidgets="widget.widgets"
|
||||||
|
position="bottom"
|
||||||
|
/>
|
||||||
|
<div class="text-right">
|
||||||
|
<BuilderBlockCodeEditor
|
||||||
|
ref="dialog"
|
||||||
|
:value="item.children"
|
||||||
|
:widgets="widgets"
|
||||||
|
@update="codeUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<slot name="action"></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import BuilderBlockCreate from './BuilderBlockCreate'
|
import BuilderBlockCreate from './BuilderBlockCreate'
|
||||||
|
import BuilderBlockCodeEditor from './BuilderBlockCodeEditor'
|
||||||
import BuilderBlockSetting from './BuilderBlockSetting'
|
import BuilderBlockSetting from './BuilderBlockSetting'
|
||||||
import Draggable from 'vuedraggable'
|
import Draggable from 'vuedraggable'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'BuilderBlockItem',
|
name: 'BuilderBlockItem',
|
||||||
|
components: {
|
||||||
|
BuilderBlockCreate,
|
||||||
|
BuilderBlockSetting,
|
||||||
|
BuilderBlockCodeEditor,
|
||||||
|
Draggable,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
widgets: {
|
widgets: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|
@ -92,21 +161,36 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
openedBlocks: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
depth: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
widget: null,
|
widget: null,
|
||||||
showSettings: false,
|
showSettings: this.openedBlocks[this.item.id] === true,
|
||||||
blockKey: 0,
|
blockKey: 0,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleSettings() {
|
toggleSettings() {
|
||||||
|
this.openedBlocks[this.item.id] = !this.openedBlocks[this.item.id]
|
||||||
this.showSettings = !this.showSettings
|
this.showSettings = !this.showSettings
|
||||||
},
|
},
|
||||||
|
truncate(value) {
|
||||||
|
return value.replace(/(<([^>]+)>)/ig, '').trim()
|
||||||
|
},
|
||||||
removeMe() {
|
removeMe() {
|
||||||
this.$emit('remove-item')
|
this.$emit('remove-item')
|
||||||
},
|
},
|
||||||
|
codeUpdate(nextValue) {
|
||||||
|
this.item.children = nextValue
|
||||||
|
},
|
||||||
removeBlock(key) {
|
removeBlock(key) {
|
||||||
let children = []
|
let children = []
|
||||||
|
|
||||||
|
|
@ -127,11 +211,6 @@ export default {
|
||||||
++this.blockKey
|
++this.blockKey
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
components: {
|
|
||||||
BuilderBlockCreate,
|
|
||||||
BuilderBlockSetting,
|
|
||||||
Draggable,
|
|
||||||
},
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.widget = this.widgets[this.item.widget]
|
this.widget = this.widgets[this.item.widget]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<span v-if="params.label && params.type !== 'checkbox'" v-text="params.label"></span>
|
<span v-if="params.label && params.type !== 'checkbox'" v-text="params.label"></span>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
v-if="['number', 'checkbox', 'text'].includes(params.type)"
|
v-if="['number', 'checkbox', 'text', 'color', 'range'].includes(params.type)"
|
||||||
v-model="item.settings[setting]"
|
v-model="item.settings[setting]"
|
||||||
v-bind="params.attr"
|
v-bind="params.attr"
|
||||||
:type="params.type"
|
:type="params.type"
|
||||||
|
|
|
||||||
|
|
@ -202,6 +202,7 @@ import FileIcon from './FileIcon'
|
||||||
|
|
||||||
const axios = require('axios').default
|
const axios = require('axios').default
|
||||||
const routes = require('../../../../../../../../../../public/js/fos_js_routes.json')
|
const routes = require('../../../../../../../../../../public/js/fos_js_routes.json')
|
||||||
|
const $ = require('jquery')
|
||||||
|
|
||||||
Routing.setRoutingData(routes)
|
Routing.setRoutingData(routes)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,14 @@ module.exports = () => {
|
||||||
el: component,
|
el: component,
|
||||||
template: `<BuilderBlock
|
template: `<BuilderBlock
|
||||||
:initialValue="value"
|
:initialValue="value"
|
||||||
|
:allowedWidgets="allowedWidgets"
|
||||||
name="${component.getAttribute('data-name')}"
|
name="${component.getAttribute('data-name')}"
|
||||||
id="${component.getAttribute('data-id')}"
|
id="${component.getAttribute('data-id')}"
|
||||||
/>`,
|
/>`,
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
value: JSON.parse(component.getAttribute('data-value'))
|
value: JSON.parse(component.getAttribute('data-value')),
|
||||||
|
allowedWidgets: JSON.parse(component.getAttribute('data-allowedwidgets')),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ module.exports = () => {
|
||||||
document.querySelectorAll('*[data-jschoice]').forEach((item) => {
|
document.querySelectorAll('*[data-jschoice]').forEach((item) => {
|
||||||
return new Choices(item, {
|
return new Choices(item, {
|
||||||
searchFields: ['label'],
|
searchFields: ['label'],
|
||||||
|
removeItemButton: true,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
const $ = require('jquery')
|
const $ = require('jquery')
|
||||||
|
|
||||||
const openModal = function (url, createModal) {
|
const openModal = function (url, createModal, dataAttributes) {
|
||||||
if (createModal) {
|
if (createModal) {
|
||||||
var id = 'modal-container-' + parseInt(Math.floor(Math.random() * 1000))
|
var id = 'modal-container-' + parseInt(Math.floor(Math.random() * 1000))
|
||||||
} else {
|
} else {
|
||||||
var id = 'modal-container'
|
var id = 'modal-container'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dataAttributes = dataAttributes ?? []
|
||||||
|
|
||||||
let container = $(`#${id}`)
|
let container = $(`#${id}`)
|
||||||
const body = $('body')
|
const body = $('body')
|
||||||
const doTrigger = true
|
const doTrigger = true
|
||||||
|
|
@ -15,6 +17,10 @@ const openModal = function (url, createModal) {
|
||||||
const doTrigger = false
|
const doTrigger = false
|
||||||
container = $(`<div id="${id}" class="modal">`)
|
container = $(`<div id="${id}" class="modal">`)
|
||||||
|
|
||||||
|
dataAttributes.forEach((attribute) => {
|
||||||
|
container.attr(attribute.name, attribute.value)
|
||||||
|
})
|
||||||
|
|
||||||
body.append(container)
|
body.append(container)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,8 +93,18 @@ module.exports = function () {
|
||||||
const element = $(e.target).is('[data-modal]') ? $(e.target) : $(e.target).parents('*[data-modal]').first()
|
const element = $(e.target).is('[data-modal]') ? $(e.target) : $(e.target).parents('*[data-modal]').first()
|
||||||
const url = element.attr('data-modal')
|
const url = element.attr('data-modal')
|
||||||
const createModal = element.is('[data-modal-create]')
|
const createModal = element.is('[data-modal-create]')
|
||||||
|
const attributes = element[0].attributes
|
||||||
|
const dataAttributes = []
|
||||||
|
|
||||||
openModal(url, createModal)
|
for (let i = 0, len = attributes.length; i < len; i++) {
|
||||||
|
let item = attributes.item(i)
|
||||||
|
|
||||||
|
if (item.name.startsWith('data-') && !['data-modal', 'data-modal-create'].includes(item.name)) {
|
||||||
|
dataAttributes.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openModal(url, createModal, dataAttributes)
|
||||||
}, 250)
|
}, 250)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
38
src/core/Resources/maker/builder/Block.tpl.php
Normal file
38
src/core/Resources/maker/builder/Block.tpl.php
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php echo "<?php\n"; ?>
|
||||||
|
|
||||||
|
namespace <?php echo $namespace; ?>;
|
||||||
|
|
||||||
|
use App\Core\BuilderBlock\BuilderBlock;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
|
||||||
|
|
||||||
|
#[AutoconfigureTag('builder_block.widget')]
|
||||||
|
class <?php echo $class_name; ?> extends BuilderBlock
|
||||||
|
{
|
||||||
|
public function configure()
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setName('<?php echo $class_name; ?>')
|
||||||
|
->setCategory('Custom')
|
||||||
|
->setTemplate('<?php echo $template; ?>')
|
||||||
|
->setLabel('<?php echo $label; ?>')
|
||||||
|
// ->setOrder(1)
|
||||||
|
// ->setIsContainer(false)
|
||||||
|
// ->setIcon('<i class="fas fa-pencil-alt"></i>')
|
||||||
|
// ->setClass('col-md-12')
|
||||||
|
// ->addSetting(name: 'value1', label: 'Text', type: 'text', attributes: [], default: '')
|
||||||
|
// ->addSetting(name: 'value2', label: 'Checkbox', type: 'checkbox', attributes: [], default: true)
|
||||||
|
// ->addSetting(name: 'value2', label: 'Number', type: 'checkbox', attributes: [], default: true)
|
||||||
|
// ->addSetting(name: 'value3', label: 'Textarea', type: 'textarea', attributes: [], default: '')
|
||||||
|
// ->addSetting(name: 'value4', label: 'Choice', type: 'select', attributes: [], default: '', extraOptions: [
|
||||||
|
// 'options' => [
|
||||||
|
// ['text' => 'Choice 1', 'value' => 'choice1'],
|
||||||
|
// ['text' => 'Choice 2', 'value' => 'choice2'],
|
||||||
|
// ],
|
||||||
|
// ])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildVars(array $data, array $context)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -232,3 +232,6 @@
|
||||||
"Level": "Niveau"
|
"Level": "Niveau"
|
||||||
"Insert line breaks": "Ajouter les retours chariot"
|
"Insert line breaks": "Ajouter les retours chariot"
|
||||||
'Allow HTML': "Autoriser l'HTML"
|
'Allow HTML': "Autoriser l'HTML"
|
||||||
|
"Custom class": "Classe personnalisée"
|
||||||
|
"Editors": "Éditeurs"
|
||||||
|
"All": "Tous"
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,7 @@
|
||||||
</th>
|
</th>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if configuration.showActions %}
|
{% if configuration.showActions(context) %}
|
||||||
<th class="crud-action-column">
|
<th class="crud-action-column">
|
||||||
{{ 'Actions'|trans }}
|
{{ 'Actions'|trans }}
|
||||||
</th>
|
</th>
|
||||||
|
|
@ -183,15 +183,20 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% block list_item %}
|
{% block list_item %}
|
||||||
{%- set dbClick %}
|
{%- set dbClick -%}
|
||||||
{% if configuration.action(context, 'show', true, [item]) %}
|
{%- if configuration.doubleClick(context) -%}
|
||||||
{{ path(configuration.pageRoute('show'), {entity: item.id}|merge(configuration.pageRouteParams('show'))) }}
|
{%- if configuration.action(context, 'show', true, [item]) -%}
|
||||||
{% elseif configuration.action(context, 'edit', true, [item]) %}
|
{{- path(configuration.pageRoute('show'), {entity: item.id}|merge(configuration.pageRouteParams('show'))) -}}
|
||||||
{{ path(configuration.pageRoute('edit'), {entity: item.id}|merge(configuration.pageRouteParams('show'))) }}
|
{%- elseif configuration.action(context, 'edit', true, [item]) -%}
|
||||||
{% endif %}
|
{{- path(configuration.pageRoute('edit'), {entity: item.id}|merge(configuration.pageRouteParams('show'))) -}}
|
||||||
{% endset -%}
|
{%- endif -%}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endset -%}
|
||||||
|
|
||||||
<tr {{ dataSortableItem|raw }} {% if configuration.doubleClick(context) %}data-dblclick="{{ dbClick }}"{% endif %} class="{{ loop.index is odd ? 'is-odd' : 'is-even' }}">
|
{% set rowAttributes = configuration.listRowAttributes(context, item) %}
|
||||||
|
{% set rowClasses = rowAttributes['class']|default('') %}
|
||||||
|
|
||||||
|
<tr {{ dataSortableItem|raw }} {% if configuration.doubleClick(context) %}data-dblclick="{{- dbClick -}}"{% endif %} class="{{ loop.index is odd ? 'is-odd' : 'is-even' }} {{ rowClasses }}" {% for attr, attrValue in rowAttributes %}{% if attr != 'class' %}{{ attr }}="{{ attrValue }}" {% endif %}{% endfor %}>
|
||||||
{% if configuration.hasBatchAction(context) %}
|
{% if configuration.hasBatchAction(context) %}
|
||||||
<td class="crud-batch-column">
|
<td class="crud-batch-column">
|
||||||
<input type="checkbox" class="batch_form" name="batch[items][{{ loop.index }}]" form="form-batch" value="{{ loop.index }}">
|
<input type="checkbox" class="batch_form" name="batch[items][{{ loop.index }}]" form="form-batch" value="{{ loop.index }}">
|
||||||
|
|
@ -228,7 +233,7 @@
|
||||||
</td>
|
</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if configuration.showActions %}
|
{% if configuration.showActions(context) %}
|
||||||
<td class="crud-action-column">
|
<td class="crud-action-column">
|
||||||
{% block list_item_actions %}
|
{% block list_item_actions %}
|
||||||
{% block list_item_actions_before %}{% endblock %}
|
{% block list_item_actions_before %}{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
{% block header_actions_before %}{% endblock %}
|
{% block header_actions_before %}{% endblock %}
|
||||||
|
|
||||||
{% if configuration.action(context, 'back', true) %}
|
{% if configuration.action(context, 'back', true) %}
|
||||||
<a href="{{ path(configuration.pageRoute('index')) }}" class="btn btn-light">
|
<a href="{{ path(configuration.pageRoute('index'), configuration.pageRouteParams('index')) }}" class="btn btn-light">
|
||||||
<span class="fa fa-list pr-1"></span>
|
<span class="fa fa-list pr-1"></span>
|
||||||
<span class="d-none d-md-inline">
|
<span class="d-none d-md-inline">
|
||||||
{{ configuration.actionTitle(context, 'back', 'Back to the list')|trans }}
|
{{ configuration.actionTitle(context, 'back', 'Back to the list')|trans }}
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if configuration.action(context, 'edit', true, [entity]) %}
|
{% if configuration.action(context, 'edit', true, [entity]) %}
|
||||||
<a href="{{ path(configuration.pageRoute('edit'), {entity: entity.id}) }}" class="btn btn-primary">
|
<a href="{{ path(configuration.pageRoute('edit'), {entity: entity.id}|merge(configuration.pageRouteParams('edit'))) }}" class="btn btn-primary">
|
||||||
<span class="fa fa-edit pr-1"></span>
|
<span class="fa fa-edit pr-1"></span>
|
||||||
|
|
||||||
<span class="d-none d-md-inline">
|
<span class="d-none d-md-inline">
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
{% apply spaceless %}
|
{% apply spaceless %}
|
||||||
|
{% block html %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
|
@ -62,4 +63,5 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
{% endblock %}
|
||||||
{% endapply %}
|
{% endapply %}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="alert {% if settings.level|default(null) %}alert-{{ settings.level }}{% endif %}" id="{{ id }}">
|
<div class="alert {% if settings.level|default(null) %}alert-{{ settings.level }}{% endif %}" id="{{ id }}">
|
||||||
{% for item in children %}
|
{% for item in children %}
|
||||||
{{ item|block_to_html }}
|
{{ item|block_to_html(context) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
'col-xl-': settings.sizeXl|default(null),
|
'col-xl-': settings.sizeXl|default(null),
|
||||||
} %}
|
} %}
|
||||||
|
|
||||||
<div class="col {% for i, v in sizes%}{% if v|length %}{{ i }}{{ v }} {% endif %}{% endfor -%}" id="{{ id }}">
|
<div class="col {% for i, v in sizes%}{% if v|length %}{{ i }}{{ v }} {% endif %}{% endfor -%} {{ settings.customClass|default(null) }}" id="{{ id }}">
|
||||||
{% for item in children %}
|
{% for item in children %}
|
||||||
{{ item|block_to_html }}
|
{{ item|block_to_html(context) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="container{% if settings.isFluid|default(false) %}-fluid{% endif %}" id="{{ id }}">
|
<div class="container{% if settings.isFluid|default(false) %}-fluid{% endif %} {{ settings.customClass|default(null) }}" id="{{ id }}">
|
||||||
{% for item in children %}
|
{% for item in children %}
|
||||||
{{ item|block_to_html }}
|
{{ item|block_to_html(context) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="row" id="{{ id }}">
|
<div class="row {{ settings.customClass|default(null) }}" id="{{ id }}">
|
||||||
{% for item in children %}
|
{% for item in children %}
|
||||||
{{ item|block_to_html }}
|
{{ item|block_to_html(context) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,15 @@
|
||||||
|
|
||||||
{% block builder_widget %}
|
{% block builder_widget %}
|
||||||
{% set row_attr = row_attr|merge({class: 'builder-widget ' ~ (row_attr.class ?? '')}) %}
|
{% set row_attr = row_attr|merge({class: 'builder-widget ' ~ (row_attr.class ?? '')}) %}
|
||||||
|
{% set value = value is iterable ? value|json_encode : value %}
|
||||||
|
{% set allowed_widgets = allowed_widgets is iterable ? allowed_widgets|json_encode : allowed_widgets %}
|
||||||
|
|
||||||
|
{% if value == '' %}
|
||||||
|
{% set value = '[]' %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div {% for attr, value in row_attr %}{{ attr }}="{{ value }}" {% endfor %}>
|
<div {% for attr, value in row_attr %}{{ attr }}="{{ value }}" {% endfor %}>
|
||||||
<div class="builder-widget-component" data-value="{{ value is iterable ? value|json_encode : value }}" data-name="{{ full_name }}" data-id="{{ id }}">
|
<div class="builder-widget-component" data-value="{{ value }}" data-name="{{ full_name }}" data-id="{{ id }}" data-allowedwidgets="{{ allowed_widgets }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -28,7 +34,7 @@
|
||||||
{% block file_widget -%}
|
{% block file_widget -%}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
{% set value = form.vars.data %}
|
{% set data = form.vars.data %}
|
||||||
|
|
||||||
{% if form.parent.vars.file_type is defined %}
|
{% if form.parent.vars.file_type is defined %}
|
||||||
{% set fileType = form.parent.vars.file_type %}
|
{% set fileType = form.parent.vars.file_type %}
|
||||||
|
|
@ -36,31 +42,33 @@
|
||||||
{% set fileType = 'auto' %}
|
{% set fileType = 'auto' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if value %}
|
{% if data and form.vars.errors|length == 0 %}
|
||||||
{% if fileType in ['auto', 'image'] and value.extension in ['jpeg', 'jpg', 'gif', 'png', 'svg', 'webp'] %}
|
{% if not data is iterable %}
|
||||||
<div class="card">
|
{% set data = [data] %}
|
||||||
<div class="card-img-top bg-tiles text-center">
|
{% endif %}
|
||||||
<a href="{{ asset(value.pathname) }}" target="_blank">
|
|
||||||
<img src="{{ asset(value.pathname) }}" class="img-fluid">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
{{- parent() -}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
{{- parent() -}}
|
|
||||||
|
|
||||||
<div class="p-2 text-center">
|
<div class="card">
|
||||||
<a class="btn btn-primary" href="{{ asset(value.pathname) }}" target="_blank">
|
{% for item in data %}
|
||||||
{{ 'Download'|trans }}
|
{% if fileType in ['auto', 'image'] and item.extension in ['jpeg', 'jpg', 'gif', 'png', 'svg', 'webp'] %}
|
||||||
|
<div class="card-img-top bg-tiles text-center">
|
||||||
|
<a href="{{ asset(item.pathname) }}" target="_blank">
|
||||||
|
<img src="{{ asset(item.pathname) }}" class="img-fluid">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% else %}
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="p-2 text-center">
|
||||||
|
<a class="btn btn-primary" href="{{ asset(item.pathname) }}" target="_blank">
|
||||||
|
{{ 'Download'|trans }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<div class="card-body">
|
||||||
|
{{- parent() -}}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{- parent() -}}
|
{{- parent() -}}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -71,6 +79,8 @@
|
||||||
{% block collection_block_widget %}
|
{% block collection_block_widget %}
|
||||||
{% set allow_delete = allow_delete|default(false) %}
|
{% set allow_delete = allow_delete|default(false) %}
|
||||||
{% set allow_add = allow_add|default(false) %}
|
{% set allow_add = allow_add|default(false) %}
|
||||||
|
{% set add_attr = add_attr|default({}) %}
|
||||||
|
{% set delete_attr = delete_attr|default({}) %}
|
||||||
|
|
||||||
<div data-collection="collection-{{ collection_name }}">
|
<div data-collection="collection-{{ collection_name }}">
|
||||||
{% for item in form.value %}
|
{% for item in form.value %}
|
||||||
|
|
@ -80,7 +90,7 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if allow_delete %}
|
{% if allow_delete %}
|
||||||
<div class="text-right">
|
<div {% for k, v in delete_attr %}{{ k }}="{{ v }}"{% endfor %}>
|
||||||
<span data-collection-delete-container class="btn btn-sm btn-danger">
|
<span data-collection-delete-container class="btn btn-sm btn-danger">
|
||||||
<span data-collection-delete="{{ loop.index }}">
|
<span data-collection-delete="{{ loop.index }}">
|
||||||
<span data-collection-delete="{{ loop.index }}" class="fa fa-trash"></span>
|
<span data-collection-delete="{{ loop.index }}" class="fa fa-trash"></span>
|
||||||
|
|
@ -94,7 +104,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if allow_add %}
|
{% if allow_add %}
|
||||||
<div data-collection-add="collection-{{ collection_name }}" class="collection-add">
|
<div data-collection-add="collection-{{ collection_name }}" {% for k, v in add_attr %}{{ k }}="{{ v }}"{% endfor %}>
|
||||||
<span class="btn btn-sm btn-primary" data-collection-add="collection-{{ collection_name }}">
|
<span class="btn btn-sm btn-primary" data-collection-add="collection-{{ collection_name }}">
|
||||||
<span data-collection-add="collection-{{ collection_name }}" class="fa fa-plus"></span>
|
<span data-collection-add="collection-{{ collection_name }}" class="fa fa-plus"></span>
|
||||||
{{ label_add|trans }}
|
{{ label_add|trans }}
|
||||||
|
|
@ -106,7 +116,7 @@
|
||||||
{{ form_rest(form.value.vars.prototype) }}
|
{{ form_rest(form.value.vars.prototype) }}
|
||||||
|
|
||||||
{% if allow_delete %}
|
{% if allow_delete %}
|
||||||
<div class="text-right">
|
<div {% for k, v in delete_attr %}{{ k }}="{{ v }}"{% endfor %}>
|
||||||
<span data-collection-delete-container class="btn btn-sm btn-danger">
|
<span data-collection-delete-container class="btn btn-sm btn-danger">
|
||||||
{{ label_delete|trans }}
|
{{ label_delete|trans }}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -121,6 +131,8 @@
|
||||||
{% set attrs = attr|merge({class: 'mb-1 ' ~ (attr.class ?? '')}) %}
|
{% set attrs = attr|merge({class: 'mb-1 ' ~ (attr.class ?? '')}) %}
|
||||||
{% set allow_delete = allow_delete|default(false) %}
|
{% set allow_delete = allow_delete|default(false) %}
|
||||||
{% set allow_add = allow_add|default(false) %}
|
{% set allow_add = allow_add|default(false) %}
|
||||||
|
{% set add_attr = add_attr|default({}) %}
|
||||||
|
{% set delete_attr = delete_attr|default({}) %}
|
||||||
|
|
||||||
<div data-collection="collection-{{ collection_name }}" {% for attr, value in row_attr %}{{ attr }}="{{ value }}"{% endfor %}>
|
<div data-collection="collection-{{ collection_name }}" {% for attr, value in row_attr %}{{ attr }}="{{ value }}"{% endfor %}>
|
||||||
{% for item in form %}
|
{% for item in form %}
|
||||||
|
|
@ -138,7 +150,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if allow_delete %}
|
{% if allow_delete %}
|
||||||
<div class="text-right">
|
<div {% for k, v in delete_attr %}{{ k }}="{{ v }}"{% endfor %}>
|
||||||
<span data-collection-delete-container class="btn btn-sm btn-danger">
|
<span data-collection-delete-container class="btn btn-sm btn-danger">
|
||||||
<span data-collection-delete="{{ loop.index }}">
|
<span data-collection-delete="{{ loop.index }}">
|
||||||
<span data-collection-delete="{{ loop.index }}" class="fa fa-trash"></span>
|
<span data-collection-delete="{{ loop.index }}" class="fa fa-trash"></span>
|
||||||
|
|
@ -152,7 +164,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if allow_add %}
|
{% if allow_add %}
|
||||||
<div data-collection-add="collection-{{ collection_name }}" class="collection-add">
|
<div data-collection-add="collection-{{ collection_name }}" {% for k, v in add_attr %}{{ k }}="{{ v }}"{% endfor %}>
|
||||||
<span class="btn btn-sm btn-primary" data-collection-add="collection-{{ collection_name }}">
|
<span class="btn btn-sm btn-primary" data-collection-add="collection-{{ collection_name }}">
|
||||||
<span data-collection-add="collection-{{ collection_name }}" class="fa fa-plus"></span>
|
<span data-collection-add="collection-{{ collection_name }}" class="fa fa-plus"></span>
|
||||||
{{ label_add|trans }}
|
{{ label_add|trans }}
|
||||||
|
|
@ -164,7 +176,7 @@
|
||||||
{{ form_rest(form.vars.prototype) }}
|
{{ form_rest(form.vars.prototype) }}
|
||||||
|
|
||||||
{% if allow_delete %}
|
{% if allow_delete %}
|
||||||
<div class="text-right">
|
<div {% for k, v in delete_attr %}{{ k }}="{{ v }}"{% endfor %}>
|
||||||
<span data-collection-delete-container class="btn btn-sm btn-danger">
|
<span data-collection-delete-container class="btn btn-sm btn-danger">
|
||||||
{{ label_delete|trans }}
|
{{ label_delete|trans }}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,77 @@
|
||||||
<div class="modal-dialog {% if options['view']|default('') == 'large' %}modal-dialog-large{% endif %}">
|
{% extends '@Core/admin/layout.html.twig' %}
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">{{ entity.section|trans }}</h5>
|
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<form action="{{ path('admin_navigation_setting_edit', {entity: entity.id, redirectTo: redirectTo}) }}" id="form-entity-edit" method="POST">
|
|
||||||
{{ include('@Core/setting/navigation_setting_admin/_form.html.twig') }}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ 'Cancel'|trans }}</button>
|
|
||||||
<button type="submit" form="form-entity-edit" class="btn btn-primary">{{ 'Save'|trans }}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{% set view = entity.options['view']|default('modal') %}
|
||||||
|
|
||||||
|
{% block html %}
|
||||||
|
{% if view == 'modal' %}
|
||||||
|
<div class="modal-dialog {% if options['view']|default('') == 'large' %}modal-dialog-large{% endif %}">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">{{ entity.section|trans }}</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form action="{{ path('admin_navigation_setting_edit', {entity: entity.id, redirectTo: redirectTo}) }}" id="form-entity-edit" method="POST">
|
||||||
|
{{ include('@Core/setting/navigation_setting_admin/_form.html.twig') }}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ 'Cancel'|trans }}</button>
|
||||||
|
<button type="submit" form="form-entity-edit" class="btn btn-primary">{{ 'Save'|trans }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{{ parent() }}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body_class %}has-form{% endblock %}
|
||||||
|
|
||||||
|
{% block title %}{{ entity.label }}{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{% block header %}
|
||||||
|
<div class="bg-light">
|
||||||
|
<div class="crud-header">
|
||||||
|
{% block header_title %}
|
||||||
|
<h1 class="crud-header-title">{{ entity.label }}</h1>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header_actions %}
|
||||||
|
<div class="crud-header-actions">
|
||||||
|
<div class="btn-group">
|
||||||
|
<a href="{{ path('admin_setting_index') }}" class="btn btn-light">
|
||||||
|
<span class="fa fa-list pr-1"></span>
|
||||||
|
<span class="d-none d-md-inline">
|
||||||
|
{{ 'Back to the list'|trans }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button type="submit" form="form-main" class="btn btn-primary">
|
||||||
|
<span class="fa fa-save pr-1"></span>
|
||||||
|
<span class="d-none d-md-inline">
|
||||||
|
{{ 'Save'|trans|build_string(entity) }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block form %}
|
||||||
|
<form action="{{ app.request.uri }}" method="post" id="form-main" enctype="multipart/form-data">
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="tab-pane active">
|
||||||
|
<div class="tab-form">
|
||||||
|
{{ include('@Core/setting/navigation_setting_admin/_form.html.twig') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,77 @@
|
||||||
<div class="modal-dialog {% if options['view']|default('') == 'large' %}modal-dialog-large{% endif %}">
|
{% extends '@Core/admin/layout.html.twig' %}
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">{{ entity.section|trans }}</h5>
|
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<form action="{{ path('admin_setting_edit', {entity: entity.id, redirectTo: redirectTo}) }}" id="form-entity-edit" method="POST">
|
|
||||||
{{ include('@Core/setting/setting_admin/_form.html.twig') }}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ 'Cancel'|trans }}</button>
|
|
||||||
<button type="submit" form="form-entity-edit" class="btn btn-primary">{{ 'Save'|trans }}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{% set view = entity.options['view']|default('modal') %}
|
||||||
|
|
||||||
|
{% block html %}
|
||||||
|
{% if view == 'modal' %}
|
||||||
|
<div class="modal-dialog {% if options['view']|default('') == 'large' %}modal-dialog-large{% endif %}">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">{{ entity.section|trans }}</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form action="{{ path('admin_setting_edit', {entity: entity.id, redirectTo: redirectTo}) }}" id="form-entity-edit" method="POST">
|
||||||
|
{{ include('@Core/setting/setting_admin/_form.html.twig') }}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ 'Cancel'|trans }}</button>
|
||||||
|
<button type="submit" form="form-entity-edit" class="btn btn-primary">{{ 'Save'|trans }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{{ parent() }}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body_class %}has-form{% endblock %}
|
||||||
|
|
||||||
|
{% block title %}{{ entity.label }}{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{% block header %}
|
||||||
|
<div class="bg-light">
|
||||||
|
<div class="crud-header">
|
||||||
|
{% block header_title %}
|
||||||
|
<h1 class="crud-header-title">{{ entity.label }}</h1>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header_actions %}
|
||||||
|
<div class="crud-header-actions">
|
||||||
|
<div class="btn-group">
|
||||||
|
<a href="{{ path('admin_setting_index') }}" class="btn btn-light">
|
||||||
|
<span class="fa fa-list pr-1"></span>
|
||||||
|
<span class="d-none d-md-inline">
|
||||||
|
{{ 'Back to the list'|trans }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button type="submit" form="form-main" class="btn btn-primary">
|
||||||
|
<span class="fa fa-save pr-1"></span>
|
||||||
|
<span class="d-none d-md-inline">
|
||||||
|
{{ 'Save'|trans|build_string(entity) }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block form %}
|
||||||
|
<form action="{{ app.request.uri }}" method="post" id="form-main" enctype="multipart/form-data">
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="tab-pane active">
|
||||||
|
<div class="tab-form">
|
||||||
|
{{ include('@Core/setting/setting_admin/_form.html.twig') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -22,21 +22,35 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for item in pager %}
|
{% for item in pager %}
|
||||||
|
{% set view = item.options['view']|default('modal') %}
|
||||||
{% set edit = path('admin_setting_edit', {entity: item.id, redirectTo: app.request.pathInfo}) %}
|
{% set edit = path('admin_setting_edit', {entity: item.id, redirectTo: app.request.pathInfo}) %}
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="col-5">
|
<td class="col-5">
|
||||||
<a href="#" data-modal="{{ edit }}" data-modal-create class="font-weight-bold text-body d-block">
|
{% if view == 'modal' %}
|
||||||
{{ item.label|trans }}
|
<a href="#" data-modal="{{ edit }}" data-modal-create class="font-weight-bold text-body d-block">
|
||||||
</a>
|
{{ item.label }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ edit }}" class="font-weight-bold text-body d-block">
|
||||||
|
{{ item.label }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="col-5">
|
<td class="col-5">
|
||||||
<span class="btn btn-light">{{ item.section|trans }}</span>
|
<span class="btn btn-light">{{ item.section|trans }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="col-2 miw-100 text-right">
|
<td class="col-2 miw-100 text-right">
|
||||||
<span data-modal="{{ edit }}" data-modal-create class="btn btn-sm btn-primary mr-1">
|
{% if view == 'modal' %}
|
||||||
<span data-modal="{{ edit }}" class="fa fa-edit"></span>
|
<span data-modal="{{ edit }}" data-modal-create class="btn btn-sm btn-primary mr-1">
|
||||||
</span>
|
<span data-modal="{{ edit }}" class="fa fa-edit"></span>
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ edit }}" class="btn btn-sm btn-primary mr-1">
|
||||||
|
<span class="fa fa-edit"></span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<button type="submit" form="form-delete-{{ item.id }}" class="btn btn-sm btn-danger">
|
<button type="submit" form="form-delete-{{ item.id }}" class="btn btn-sm btn-danger">
|
||||||
<span class="fa fa-trash"></span>
|
<span class="fa fa-trash"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -54,21 +54,36 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for item in datas.settings %}
|
{% for item in datas.settings %}
|
||||||
|
{% set view = item.options['view']|default('modal') %}
|
||||||
|
|
||||||
{% set edit = path('admin_navigation_setting_edit', {entity: item.id, redirectTo: app.request.pathInfo}) %}
|
{% set edit = path('admin_navigation_setting_edit', {entity: item.id, redirectTo: app.request.pathInfo}) %}
|
||||||
|
|
||||||
<tr data-dblclick="{{ edit }}">
|
<tr data-dblclick="{{ edit }}">
|
||||||
<td class="col-5">
|
<td class="col-5">
|
||||||
<a href="#" data-modal="{{ edit }}" class="font-weight-bold text-body d-block">
|
{% if view == 'modal' %}
|
||||||
{{ item.label|trans }}
|
<a href="#" data-modal="{{ edit }}" data-modal-create class="font-weight-bold text-body d-block">
|
||||||
</a>
|
{{ item.label }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ edit }}" class="font-weight-bold text-body d-block">
|
||||||
|
{{ item.label }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="col-5">
|
<td class="col-5">
|
||||||
<span class="btn btn-light">{{ item.section|trans }}</span>
|
<span class="btn btn-light">{{ item.section|trans }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="col-2 miw-100 text-right">
|
<td class="col-2 miw-100 text-right">
|
||||||
<span data-modal="{{ edit }}" class="btn btn-sm btn-primary mr-1">
|
{% if view == 'modal' %}
|
||||||
<span data-modal="{{ edit }}" class="fa fa-edit"></span>
|
<span data-modal="{{ edit }}" data-modal-create class="btn btn-sm btn-primary mr-1">
|
||||||
</span>
|
<span data-modal="{{ edit }}" class="fa fa-edit"></span>
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ edit }}" class="btn btn-sm btn-primary mr-1">
|
||||||
|
<span class="fa fa-edit"></span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<button type="submit" form="form-delete-{{ item.id }}" class="btn btn-sm btn-danger">
|
<button type="submit" form="form-delete-{{ item.id }}" class="btn btn-sm btn-danger">
|
||||||
<span class="fa fa-trash"></span>
|
<span class="fa fa-trash"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,14 @@ class NavigationSettingManager
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function init($navigation, string $code, string $section, string $label, $value = null)
|
public function init(
|
||||||
|
$navigation,
|
||||||
|
string $code,
|
||||||
|
string $section,
|
||||||
|
string $label,
|
||||||
|
$value = null,
|
||||||
|
array $options = [],
|
||||||
|
)
|
||||||
{
|
{
|
||||||
$entity = $this->get($this->getNavigation($navigation), $code);
|
$entity = $this->get($this->getNavigation($navigation), $code);
|
||||||
$isNew = null === $entity;
|
$isNew = null === $entity;
|
||||||
|
|
@ -37,6 +44,7 @@ class NavigationSettingManager
|
||||||
$entity
|
$entity
|
||||||
->setSection($section)
|
->setSection($section)
|
||||||
->setLabel($label)
|
->setLabel($label)
|
||||||
|
->setOptions($options)
|
||||||
;
|
;
|
||||||
|
|
||||||
if ($isNew) {
|
if ($isNew) {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,13 @@ class SettingManager
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function init(string $code, string $section, string $label, $value = null)
|
public function init(
|
||||||
|
string $code,
|
||||||
|
string $section,
|
||||||
|
string $label,
|
||||||
|
$value = null,
|
||||||
|
array $options = [],
|
||||||
|
)
|
||||||
{
|
{
|
||||||
$entity = $this->get($code);
|
$entity = $this->get($code);
|
||||||
$isNew = null === $entity;
|
$isNew = null === $entity;
|
||||||
|
|
@ -34,6 +40,7 @@ class SettingManager
|
||||||
$entity
|
$entity
|
||||||
->setSection($section)
|
->setSection($section)
|
||||||
->setLabel($label)
|
->setLabel($label)
|
||||||
|
->setOptions($options)
|
||||||
;
|
;
|
||||||
|
|
||||||
if ($isNew) {
|
if ($isNew) {
|
||||||
|
|
|
||||||
|
|
@ -23,27 +23,40 @@ class BuilderExtension extends AbstractExtension
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function buildHtml($data): string
|
public function buildHtml(null|array|string $data, array $context = []): ?string
|
||||||
{
|
{
|
||||||
if (null === $data) {
|
if (null === $data) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (is_string($data)) {
|
||||||
|
$data = json_decode($data, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($data)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($data['widget'])) {
|
if (isset($data['widget'])) {
|
||||||
|
if (!$this->container->hasWidget($data['widget'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
$widget = $this->container->getWidget($data['widget']);
|
$widget = $this->container->getWidget($data['widget']);
|
||||||
$widget->buildVars($data);
|
$widget->buildVars($data, $context);
|
||||||
|
|
||||||
return $this->twig->render($widget->getTemplate(), [
|
return $this->twig->render($widget->getTemplate(), [
|
||||||
'id' => $data['id'],
|
'id' => $data['id'],
|
||||||
'settings' => $data['settings'],
|
'settings' => $data['settings'],
|
||||||
'children' => $data['children'],
|
'children' => $data['children'],
|
||||||
|
'context' => $context,
|
||||||
'vars' => $widget->getVars(),
|
'vars' => $widget->getVars(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$render = '';
|
$render = '';
|
||||||
foreach ($data as $item) {
|
foreach ($data as $item) {
|
||||||
$render .= $this->buildHtml($item);
|
$render .= $this->buildHtml($item, $context);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $render;
|
return $render;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue