Compare commits

...

143 commits

Author SHA1 Message Date
Simon Vieille fd1a45c0e7 Merge branch 'develop' 2024-05-26 22:14:35 +02:00
Simon Vieille bc148f0b6b
builder block: add icon in blocks 2024-05-26 22:14:33 +02:00
Simon Vieille 4a3619919c Merge branch 'develop' 2024-05-26 21:43:35 +02:00
Simon Vieille 4054f6ccff
builder block: add search 2024-05-26 21:43:29 +02:00
Simon Vieille 01ecac272e Merge branch 'develop' 2024-05-26 21:42:33 +02:00
Simon Vieille 89efd5475f
builder block: add search 2024-05-26 21:42:31 +02:00
Simon Vieille 029d296bd8 Merge branch 'develop' 2024-05-26 21:36:44 +02:00
Simon Vieille 1120c20f05
builder block: add search 2024-05-26 21:36:42 +02:00
Simon Vieille f4a70bec66 Merge branch 'develop' 2024-05-26 21:15:49 +02:00
Simon Vieille d4ba8dc619
builder block: add translations 2024-05-26 21:15:46 +02:00
Simon Vieille 26e422619b Merge branch 'develop' 2024-05-26 21:13:36 +02:00
Simon Vieille eb3c1a8879
builder block: add translations 2024-05-26 21:13:34 +02:00
Simon Vieille 9963c4b3cf Merge branch 'develop' 2024-05-26 21:08:38 +02:00
Simon Vieille e322973e67 block builder: make to picker larger 2024-05-26 21:08:20 +02:00
Simon Vieille a15cf1559b Merge branch 'develop' 2024-05-26 14:51:18 +02:00
Simon Vieille 006b8cdbe6
block builde: add custom class setting 2024-05-26 14:51:12 +02:00
Simon Vieille 46f13d817d Merge branch 'develop' 2024-05-25 22:27:25 +02:00
Simon Vieille 0d5f248ca9
fix file manager: add jquery on Files component 2024-05-25 22:27:19 +02:00
Simon Vieille d1ca98eddd Merge branch 'develop' 2024-05-24 15:47:18 +02:00
Simon Vieille 62fbd936c6
block builde: add prview 2024-05-24 15:47:15 +02:00
Simon Vieille d66fb7da78 Merge branch 'develop' 2024-05-24 15:44:00 +02:00
Simon Vieille 57dd7da162
block builde: add prview 2024-05-24 15:43:58 +02:00
Simon Vieille d7ce3cb12f Merge branch 'develop' 2024-05-24 15:43:22 +02:00
Simon Vieille fea5319dc5
block builde: add prview 2024-05-24 15:43:15 +02:00
Simon Vieille ff0ab092e4 Merge branch 'develop' 2024-05-24 13:50:41 +02:00
Simon Vieille a0211026ba
block code editor: fix drag and drop 2024-05-24 13:50:37 +02:00
Simon Vieille c5dde3b184 Merge branch 'develop' 2024-05-24 13:21:17 +02:00
Simon Vieille b1ea641374
block code editor: allow to add block on the top 2024-05-24 13:21:11 +02:00
Simon Vieille 62de6416c5 Merge branch 'develop' 2024-05-24 08:56:06 +02:00
Simon Vieille 246c249d3e
block code editor: remove is-invalid class when the modal is closed 2024-05-24 08:56:03 +02:00
Simon Vieille be26d15b22 Merge branch 'develop' 2024-05-23 23:59:03 +02:00
Simon Vieille 3f5bd4f950
replace div with buttons
add code editor

handle new settings
2024-05-23 23:58:39 +02:00
Simon Vieille a80dc9999c Merge branch 'develop' 2024-05-17 22:02:18 +02:00
Simon Vieille b9566853ef
allow buildHtml to get null, array or string 2024-05-17 22:02:16 +02:00
Simon Vieille bc7e8ef263 Merge branch 'develop' 2024-05-17 21:38:50 +02:00
Simon Vieille 25efd11ea3
add BuilderBlockContainer::removeBlock 2024-05-17 21:38:48 +02:00
Simon Vieille e23f7d3c73 Merge branch 'develop' 2024-05-17 21:34:16 +02:00
Simon Vieille 85f054956c
sanitize builder component value 2024-05-17 21:34:08 +02:00
Simon Vieille b12f4db16f Merge branch 'develop' 2024-05-16 21:42:42 +02:00
Simon Vieille 6bb29dd5c3
add margin and border color to builder block widgets 2024-05-16 21:42:40 +02:00
Simon Vieille 7824e96bab Merge branch 'develop' 2024-05-16 11:17:10 +02:00
Simon Vieille 56177c14da
improve block builder picker 2024-05-16 11:16:48 +02:00
Simon Vieille 4f1c5b8f14 Merge branch 'develop' 2024-05-16 08:56:14 +02:00
Simon Vieille e8c9520378
update builder block default template 2024-05-16 08:56:06 +02:00
Simon Vieille 283223446a Merge branch 'develop' 2024-05-15 23:24:18 +02:00
Simon Vieille 46e01f504f
add maker for builder block 2024-05-15 23:24:11 +02:00
Simon Vieille a80ee03fcd Merge branch 'develop' 2024-05-15 18:39:15 +02:00
Simon Vieille d1649a4959
add builder as new type in the page maker 2024-05-15 18:39:09 +02:00
Simon Vieille 38791f1d7a Merge branch 'develop' 2024-05-15 18:27:22 +02:00
Simon Vieille d74cd52711
add builer block context 2024-05-15 18:27:19 +02:00
Simon Vieille b451df61e8 Merge branch 'develop' 2024-05-15 18:19:11 +02:00
Simon Vieille 7c124008c0
add builer block context 2024-05-15 18:18:56 +02:00
Simon Vieille 9cf95dba64 Merge branch 'develop' 2024-05-14 22:13:44 +02:00
Simon Vieille b680946daf
block builder: add test to check if the widget exists 2024-05-14 22:13:36 +02:00
Simon Vieille 1995298977 Merge branch 'develop' 2024-05-14 20:24:58 +02:00
Simon Vieille 4acba618cb
block builder: add colors depending of depth and keep open/closed settings when moved 2024-05-14 20:24:34 +02:00
Simon Vieille 92eaaba699 Merge branch 'develop' 2024-05-13 17:58:11 +02:00
Simon Vieille 357856f8ce
release v1.25.1 2024-05-13 17:58:08 +02:00
Simon Vieille dfbe282d0e
add drag and drop in the block builder 2024-05-13 17:40:05 +02:00
Simon Vieille 857fcd9897
add blocks 2024-05-13 13:58:07 +02:00
Simon Vieille 48ca5a96e6
rename blocks 2024-05-13 13:57:58 +02:00
Simon Vieille 065bb0db22
rename blocks 2024-05-13 13:57:49 +02:00
Simon Vieille bf6777054f
add order property 2024-05-13 13:57:34 +02:00
Simon Vieille 51acd82432 order blocks 2024-05-13 13:57:01 +02:00
Simon Vieille c6cbc405e4 Merge branch 'develop' 2024-05-13 11:20:11 +02:00
Simon Vieille 150f3afd6a
add build block vars 2024-05-13 11:19:56 +02:00
Simon Vieille 4a0e616897 Merge branch 'develop' 2024-05-13 09:27:18 +02:00
Simon Vieille 4eb13a4022
add diffent border color on the root block of the builder
add default class in blocks
2024-05-13 09:26:31 +02:00
Simon Vieille 8bb6267a77 Merge branch 'develop' 2024-05-12 22:49:46 +02:00
Simon Vieille 8044ff3605
update changelog 2024-05-12 22:49:43 +02:00
Simon Vieille 71d3d40d5f Merge branch 'develop' 2024-05-12 22:48:33 +02:00
Simon Vieille 9324837bd9
update changelog 2024-05-12 22:48:30 +02:00
Simon Vieille b8dfaaed10 Merge branch 'feature/blocks' into develop 2024-05-12 22:41:44 +02:00
Simon Vieille 2d49b8ddee
move build add button 2024-05-12 22:40:52 +02:00
Simon Vieille 232b92267e
add build block loader
add builder block rendering

fix issues with components
2024-05-12 22:24:51 +02:00
Simon Vieille ca23a22807
rollback indentation 2024-05-12 22:24:10 +02:00
Simon Vieille 78965bbf10
allow to move blocks
update builder view
2024-05-12 14:30:28 +02:00
Simon Vieille 7fe1acd47d
add base of the block builder 2024-05-11 17:45:57 +02:00
Simon Vieille 26cbaa8469 Merge branch 'develop' 2024-05-03 17:58:22 +02:00
Simon Vieille 7897bafcc2
fix undefined window.tinymce.murph 2024-05-03 17:57:43 +02:00
Simon Vieille 63a8a60e2d Merge branch 'develop' 2024-03-31 16:57:56 +02:00
Simon Vieille d01e9d618b
add border color on tinymce editor 2024-03-31 16:57:46 +02:00
Simon Vieille 96347a1730 Merge branch 'develop' 2024-03-31 16:50:47 +02:00
Simon Vieille aeb0e6c109 allow to use window.tinymceModes to add or override tinymce modes 2024-03-31 16:50:18 +02:00
Simon Vieille 64258a2d8c Merge branch 'develop' 2024-03-25 15:27:12 +01:00
Simon Vieille 70329ceeda
fix template of CrudController (maker) 2024-03-25 15:27:06 +01:00
Simon Vieille 1adb1ebe2b Merge branch 'develop' 2024-03-25 13:04:17 +01:00
Simon Vieille f57cc8e4d6
fix hidden save button in file manager 2024-03-25 13:04:16 +01:00
Simon Vieille b21967028e Merge branch 'develop' 2024-03-25 13:03:01 +01:00
Simon Vieille 5b22851674
fix hidden save button in file manager 2024-03-25 13:02:20 +01:00
Simon Vieille 4082bb171a
fix use of IsGranted is CrudController 2024-02-04 16:48:44 +01:00
Simon Vieille dc19617fb1 Merge branch 'develop' 2024-02-04 16:33:34 +01:00
Simon Vieille dbd2036fb0
rollback commit 614ae40 (add IsGranted in all methods of the CrudController maker template) 2024-02-04 16:33:30 +01:00
Simon Vieille 8e2566abc8
rollback commit 614ae40 (add IsGranted in all methods of the CrudController maker template) 2024-02-04 16:33:22 +01:00
Simon Vieille 66e1a9c87f
rollback commit 614ae40 (add IsGranted in all methods of the CrudController maker template) 2024-02-04 16:31:21 +01:00
Simon Vieille 6709c0a303
update changelog 2024-02-04 16:17:49 +01:00
Simon Vieille 614ae40901 add IsGranted in all methods of the CrudController maker template 2024-02-04 16:16:22 +01:00
Simon Vieille 430bff9433
fix default crud sort 2024-02-02 20:04:19 +01:00
Simon Vieille 6441da8a27
fix default crud sort 2024-02-02 20:03:57 +01:00
Simon Vieille 801e3317e7
update changelog 2024-02-02 20:02:00 +01:00
Simon Vieille f7604d2a45
fix default crud sort 2024-02-02 20:01:23 +01:00
Simon Vieille 8a632a1b14
update Murph version constant 2024-02-01 18:46:33 +01:00
Simon Vieille 4701090134
update changelog 2024-01-27 15:49:58 +01:00
Simon Vieille c40c7e3362
fix type casting in slugifier 2024-01-27 15:49:39 +01:00
Simon Vieille 8edbf0cc08
add default default on node's code when slugify 2023-11-27 10:32:38 +01:00
Simon Vieille 175321bc2d
fix issue on file manager when a file is selected in the file in the file picker 2023-11-16 23:06:00 +01:00
Simon Vieille 5d6531d197
fix sidebar scroll algo 2023-11-14 23:21:24 +01:00
Simon Vieille 90603f62e0
add side bar scroll animation 2023-11-14 23:08:09 +01:00
Simon Vieille 053f4aa5b8
add auto-scroll on current sidebar item 2023-11-14 23:03:43 +01:00
Simon Vieille 79754d45c1
add auto-scroll on current sidebar item
remove jquery from sidebar module
2023-11-14 22:51:40 +01:00
Simon Vieille c98ea50f30
fix undefined pager on index 2023-11-13 16:06:06 +01:00
Simon Vieille 0f1bc761b2
add no-wrap around the thead sort link 2023-11-10 19:42:45 +01:00
Simon Vieille 50dbb07314
update changelog 2023-11-10 19:35:48 +01:00
Simon Vieille 72e783f865
fix render of the URL when the window is small 2023-11-10 19:35:39 +01:00
Simon Vieille ee28c9abb7
copy the pager in the bottom of the index table
remove the with class of the action column
2023-11-10 19:28:20 +01:00
Simon Vieille 2bd6836a7f
fix issue on file manager when a file is selected in the file in the file picker 2023-11-09 17:19:09 +01:00
Simon Vieille 6f961ba79b
release v1.23.0 2023-11-01 16:31:33 +01:00
Simon Vieille e095fc4197
update changelog 2023-10-27 17:03:08 +02:00
Simon Vieille 6736f94eea
set searchFields option on jschoice manager 2023-10-27 17:02:15 +02:00
Simon Vieille 93a1e7811d
change colors on js-choices element 2023-10-25 20:15:21 +02:00
Simon Vieille a0027c0b69 Merge branch 'feature/theming' into develop 2023-10-25 19:59:32 +02:00
Simon Vieille 498c71081d
add red * on required label 2023-10-25 19:59:28 +02:00
Simon Vieille 8713b401f9
rollback modal changes 2023-10-25 19:36:10 +02:00
Simon Vieille 1463f43298
update changelog 2023-10-25 19:22:56 +02:00
Simon Vieille b89e036c49 change border colors of inputs when focused 2023-10-25 19:22:44 +02:00
Simon Vieille 1d0b657c83
add sass classes to mange with of elements
fix the aspect of the actions's column in the crud

add background in the modal header

change vavigation pills colors
2023-10-25 19:12:02 +02:00
Simon Vieille 0cadf28738
update changelog 2023-10-20 09:46:57 +02:00
Simon Vieille ede8d4fdcb
remove unsed twig in mail notifier 2023-10-20 09:45:58 +02:00
Simon Vieille 5c3f2ab1e7
refactor services using constructor property promotions 2023-10-20 09:44:18 +02:00
Simon Vieille c1eb277a6a
add 'Length' constraint in forms 2023-10-19 20:51:23 +02:00
Simon Vieille d3f27d97ad
apply php linter 2023-10-12 16:15:07 +02:00
Simon Vieille 5e392d469a
update changelog 2023-10-12 16:04:49 +02:00
Simon Vieille 67f79083ef
change params given to the callback of a global batch action (page removed, add selected items) 2023-10-12 16:04:43 +02:00
Simon Vieille b9b07c1409
fix issue with CrudConfiguration::setGlobalBatchAction 2023-10-12 16:04:03 +02:00
Simon Vieille 521ed5ce64 Merge branch 'feature/batch' into develop 2023-10-12 15:28:17 +02:00
Simon Vieille dda43ef3cc
change CrudController::doBatch to manage a global batch action 2023-10-12 15:28:14 +02:00
Simon Vieille c65cc26be8
batch form is not submitted with XHR when it's a global action 2023-10-12 15:27:36 +02:00
Simon Vieille 2f884df602
add CrudConfiguration::setGlobalBatchAction method 2023-10-12 15:25:52 +02:00
Simon Vieille 8979fc5beb
fix test in RepositoryQuery::addForcedFilterHandler 2023-10-10 10:18:25 +02:00
Simon Vieille bd663838f6
allow to define templates show before and after a murph collection item 2023-10-06 12:46:51 +02:00
Simon Vieille 177b23365b
update changelog 2023-10-04 13:55:08 +02:00
Simon Vieille 645ae700d4
remove parameter $option on CrudConfiguration::setForm
fix CrudController make template
2023-10-04 13:54:37 +02:00
Simon Vieille 7614c24012
fix regression on crud sorting 2023-10-04 13:47:01 +02:00
144 changed files with 2504 additions and 637 deletions

View file

@ -1,6 +1,52 @@
## [Unreleased] ## [Unreleased]
## [v1.22.0] 2023-09-28 ## [v1.25.1] - 2024-05-13
### Added
* add drag&drop in the block builder
## [v1.25.0] - 2024-05-12
### Added
* add block builder widget
* allow to use `window.tinymceModes` to add or override tinymce modes
* add border color on tinymce editor
### Fixed
* fix default crud sort
* fix hidden save button in file manager
* fix template of CrudController (maker)
* fix undefined `window.tinymce.murph`
## [v1.24.1] - 2024-02-01
### Fixed
* update Murph version constant
## [v1.24.0] - 2024-01-27
### Added
* add CSS class `no-wrap`
* copy the pager of the CRUD at the bottom of the list
### Fixed
* fix an issue with the file manager when editing an item opened in a modal
* fix type casting in slugifier
## [v1.23.0] - 2023-11-01
### Added
* allow to define templates show before and after a murph collection item
* add global batch actions
* add constraint `Length` in forms
* add sass classes to mange with of elements
* set searchFields option on jschoice manager (search on labels)
### Changed
* refactor services using constructor property promotions
* remove twig in the mail notifier service
* change pills colors
* change border colors of inputs when focused
* change colors on js-choices element
### Fixed
* fix regression on crud sorting
* fix test in RepositoryQuery::addForcedFilterHandler
* remove parameter $option on CrudConfiguration::setForm and fix CrudController make template
* fix the aspect of the actions's column in the crud
## [v1.22.0] - 2023-09-28
### Added ### Added
* add new options in BooleanField: `toggle|checkbox_class_when_true` and `toggle|checkbox_class_when_false` * add new options in BooleanField: `toggle|checkbox_class_when_true` and `toggle|checkbox_class_when_false`
* add `count` method in repository query * add `count` method in repository query

View file

@ -10,14 +10,12 @@ namespace App\Core\Ab;
class AbTest implements AbTestInterface class AbTest implements AbTestInterface
{ {
protected $results; protected $results;
protected string $name;
protected array $variations = []; protected array $variations = [];
protected array $probabilities = []; protected array $probabilities = [];
protected int $duration = 3600 * 24; protected int $duration = 3600 * 24;
public function __construct(string $name) public function __construct(protected string $name)
{ {
$this->name = $name;
} }
public function getName(): string public function getName(): string

View file

@ -13,18 +13,16 @@ use App\Core\Repository\Analytic\ViewRepositoryQuery;
*/ */
class DateRangeAnalytic class DateRangeAnalytic
{ {
protected ViewRepositoryQuery $viewQuery;
protected RefererRepositoryQuery $refererQuery;
protected ?Node $node; protected ?Node $node;
protected ?\DateTime $from; protected ?\DateTime $from;
protected ?\DateTime $to; protected ?\DateTime $to;
protected bool $reload = true; protected bool $reload = true;
protected array $cache = []; protected array $cache = [];
public function __construct(ViewRepositoryQuery $viewQuery, RefererRepositoryQuery $refererQuery) public function __construct(
{ protected ViewRepositoryQuery $viewQuery,
$this->viewQuery = $viewQuery; protected RefererRepositoryQuery $refererQuery
$this->refererQuery = $refererQuery; ) {
} }
public function getViews(): array public function getViews(): array
@ -83,7 +81,7 @@ class DateRangeAnalytic
$datas[$index]['mobileViews'] += $entity->getMobileViews(); $datas[$index]['mobileViews'] += $entity->getMobileViews();
} }
uasort($datas, function($a, $b) { uasort($datas, function ($a, $b) {
if ($a['views'] > $b['views']) { if ($a['views'] > $b['views']) {
return -1; return -1;
} }
@ -130,7 +128,7 @@ class DateRangeAnalytic
$datas[$index]['uris'][$path] += $entity->getViews(); $datas[$index]['uris'][$path] += $entity->getViews();
} }
uasort($datas, function($a, $b) { uasort($datas, function ($a, $b) {
if ($a['views'] > $b['views']) { if ($a['views'] > $b['views']) {
return -1; return -1;
} }

View file

@ -10,16 +10,10 @@ namespace App\Core\Annotation;
#[\Attribute] #[\Attribute]
class UrlGenerator class UrlGenerator
{ {
public string $service; public function __construct(
public string $service,
public string $method; public string $method,
public array $options = []
public array $options = []; ) {
public function __construct(string $service, string $method, array $options = [])
{
$this->service = $service;
$this->method = $method;
$this->options = $options;
} }
} }

View file

@ -23,20 +23,12 @@ class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{ {
use TargetPathTrait; use TargetPathTrait;
private EntityManagerInterface $entityManager; public function __construct(
private EntityManagerInterface $entityManager,
private UrlGeneratorInterface $urlGenerator; private UrlGeneratorInterface $urlGenerator,
private CsrfTokenManagerInterface $csrfTokenManager,
private CsrfTokenManagerInterface $csrfTokenManager; private UserPasswordEncoderInterface $passwordEncoder
) {
private UserPasswordEncoderInterface $passwordEncoder;
public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder)
{
$this->entityManager = $entityManager;
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
$this->passwordEncoder = $passwordEncoder;
} }
public function supports(Request $request) public function supports(Request $request)

View file

@ -0,0 +1,47 @@
<?php
namespace App\Core\BuilderBlock\Block\Bootstrap;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
use Symfony\Contracts\Translation\TranslatorInterface;
#[AutoconfigureTag('builder_block.widget')]
class AlertBlock extends BootstrapBlock
{
public function __construct(protected TranslatorInterface $translator)
{
}
public function configure()
{
parent::configure();
$options = [];
foreach ([
'Primary' => 'primary',
'Secondary' => 'secondary',
'Info' => 'info',
'Success' => 'success',
'Danger' => 'danger',
'Warning' => 'warning',
'Light' => 'light',
'Dark' => 'dark',
] as $k => $v) {
$options[] = [
'text' => $this->translator->trans($k),
'value' => $v,
];
}
$this
->setName('bsAlert')
->setLabel('Alert')
->setOrder(4)
->setIsContainer(true)
->setIcon('<i class="fas fa-exclamation-circle"></i>')
->setTemplate('@Core/builder_block/bootstrap/alert.html.twig')
->addSetting(name: 'level', label: 'Level', type: 'select', extraOptions: ['options' => $options])
;
}
}

View file

@ -0,0 +1,14 @@
<?php
namespace App\Core\BuilderBlock\Block\Bootstrap;
use App\Core\BuilderBlock\BuilderBlock;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
class BootstrapBlock extends BuilderBlock
{
public function configure()
{
$this->setCategory('Bootstrap');
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace App\Core\BuilderBlock\Block\Bootstrap;
use App\Core\BuilderBlock\BuilderBlock;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('builder_block.widget')]
class ColumnBlock extends BootstrapBlock
{
public function configure()
{
parent::configure();
$this
->setName('bsColumn')
->setLabel('Column')
->setIsContainer(true)
->setOrder(3)
->setClass('col-12 col-lg-3 pr-md-1')
->setTemplate('@Core/builder_block/bootstrap/column.html.twig')
->setIcon('<i class="fas fa-columns"></i>')
->addSetting(name: 'size', label: 'Extra small', type: 'number', attributes: ['min' => 0, 'max' => 12])
->addSetting(name: 'sizeSm', label: 'Small', 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: 'sizeXl', label: 'Extra large', type: 'number', attributes: ['min' => 0, 'max' => 12])
->addSetting(name: 'customClass', label: 'Custom class', type: 'text')
;
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace App\Core\BuilderBlock\Block\Bootstrap;
use App\Core\BuilderBlock\BuilderBlock;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('builder_block.widget')]
class ContainerBlock extends BootstrapBlock
{
public function configure()
{
parent::configure();
$this
->setName('bsContainer')
->setLabel('Container')
->setIsContainer(true)
->setOrder(1)
->setTemplate('@Core/builder_block/bootstrap/container.html.twig')
->setIcon('<i class="fas fa-th"></i>')
->addSetting(name: 'isFluid', label: 'Fluid', type: 'checkbox')
->addSetting(name: 'customClass', label: 'Custom class', type: 'text')
;
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace App\Core\BuilderBlock\Block\Bootstrap;
use App\Core\BuilderBlock\BuilderBlock;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('builder_block.widget')]
class RowBlock extends BootstrapBlock
{
public function configure()
{
parent::configure();
$this
->setName('bsRow')
->setLabel('Row')
->setOrder(2)
->setIsContainer(true)
->setIcon('<i class="fas fa-align-justify"></i>')
->setTemplate('@Core/builder_block/bootstrap/row.html.twig')
->addWidget('bsColumn')
->addSetting(name: 'customClass', label: 'Custom class', type: 'text')
;
}
}

View file

@ -0,0 +1,14 @@
<?php
namespace App\Core\BuilderBlock\Block\Editor;
use App\Core\BuilderBlock\BuilderBlock;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
class EditorBlock extends BuilderBlock
{
public function configure()
{
$this->setCategory('Editors');
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace App\Core\BuilderBlock\Block\Editor;
use App\Core\BuilderBlock\BuilderBlock;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('builder_block.widget')]
class TextareaBlock extends EditorBlock
{
public function configure()
{
parent::configure();
$this
->setName('textarea')
->setLabel('Text')
->setIsContainer(false)
->setIcon('<i class="fas fa-pencil-alt"></i>')
->setTemplate('@Core/builder_block/editor/textarea.html.twig')
->addSetting(name: 'nl2br', label: 'Insert line breaks', type: 'checkbox', default: true)
->addSetting(name: 'allowHtml', label: 'Allow HTML', type: 'checkbox', default: false)
->addSetting(name: 'value', type: 'textarea')
->setPreview('value')
;
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Core\BuilderBlock\Block\Editor;
use App\Core\BuilderBlock\BuilderBlock;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('builder_block.widget')]
class TinymceBlock extends EditorBlock
{
public function configure()
{
parent::configure();
$this
->setName('tinymce')
->setLabel('TinyMCE')
->setIsContainer(false)
->setIcon('<i class="fas fa-pencil-alt"></i>')
->setTemplate('@Core/builder_block/editor/tinymce.html.twig')
->addSetting(name: 'value', type: 'textarea', attributes: ['data-tinymce' => ''])
->setPreview('value')
;
}
}

View file

@ -0,0 +1,209 @@
<?php
namespace App\Core\BuilderBlock;
abstract class BuilderBlock
{
protected string $name;
protected string $label;
protected ?string $class = 'col-12';
protected ?string $category = null;
protected array $settings = [];
protected array $widgets = [];
protected array $vars = [];
protected string $template = '';
protected ?string $preview = null;
protected bool $isContainer = false;
protected ?string $icon = null;
protected int $order = 1;
abstract public function configure();
public function setLabel(string $label): self
{
$this->label = $label;
return $this;
}
public function getLabel(): string
{
return $this->label;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getName(): string
{
return $this->name;
}
public function setCategory(?string $category): self
{
$this->category = $category;
return $this;
}
public function getCategory(): ?string
{
return $this->category;
}
public function setIsContainer(bool $isContainer): self
{
$this->isContainer = $isContainer;
return $this;
}
public function getIsContainer(): bool
{
return $this->isContainer;
}
public function setWidgets(array $widgets): self
{
$this->widgets = $widgets;
return $this;
}
public function addWidget(string $widget): self
{
if (!in_array($widget, $this->widgets)) {
$this->widgets[] = $widget;
}
return $this;
}
public function getWidgets(): array
{
return $this->widgets;
}
public function setSettings(array $settings): self
{
$this->settings = $settings;
return $this;
}
public function addSetting(
string $name,
string $type = 'input',
?string $label = null,
array $attributes = [],
array $extraOptions = [],
$default = null
) {
$this->settings[$name] = [
'label' => $label,
'type' => $type,
'attr' => $attributes,
'default' => $default,
];
foreach ($extraOptions as $key => $value) {
if (!in_array($key, array_keys($this->settings[$name]))) {
$this->settings[$name][$key] = $value;
}
}
return $this;
}
public function getSettings(): array
{
return $this->settings;
}
public function setTemplate(string $template): self
{
$this->template = $template;
return $this;
}
public function getTemplate(): string
{
return $this->template;
}
public function setClass(?string $class): self
{
$this->class = $class;
return $this;
}
public function getClass(): ?string
{
return $this->class;
}
public function setIcon(?string $icon): self
{
$this->icon = $icon;
return $this;
}
public function getIcon(): ?string
{
return $this->icon;
}
public function setOrder(int $order): self
{
$this->order = $order;
return $this;
}
public function getOrder(): int
{
return $this->order;
}
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)
{
}
public function getVars(): array
{
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(),
];
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace App\Core\BuilderBlock;
class BuilderBlockContainer
{
protected array $widgets = [];
public function addWidget(BuilderBlock $widget): self
{
$widget->configure();
$this->widgets[$widget->getName()] = $widget;
return $this;
}
public function removeWidget(string $name)
{
unset($this->widgets[$name]);
return $this;
}
public function getWidgets(): array
{
usort($this->widgets, fn(BuilderBlock $a, BuilderBlock $b) => $a->getOrder() <=> $b->getOrder());
return $this->widgets;
}
public function hasWidget(string $name)
{
return isset($this->widgets[$name]);
}
public function getWidget(string $name): BuilderBlock
{
return $this->widgets[$name];
}
}

View file

@ -12,8 +12,8 @@
namespace App\Core\Bundle; namespace App\Core\Bundle;
use App\Core\DependencyInjection\CoreExtension; use App\Core\DependencyInjection\CoreExtension;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class CoreBundle extends Bundle class CoreBundle extends Bundle
{ {

View file

@ -5,13 +5,13 @@ namespace App\Core\Cache;
use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Finder\Finder;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\HttpClient\Exception\ClientException;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Finder\Finder;
use Symfony\Component\HttpClient\Exception\ClientException;
use Symfony\Component\HttpClient\Exception\TransportException; use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/** /**
* class SymfonyCacheManager. * class SymfonyCacheManager.
@ -20,15 +20,11 @@ use Symfony\Component\HttpClient\Exception\TransportException;
*/ */
class SymfonyCacheManager class SymfonyCacheManager
{ {
protected KernelInterface $kernel; public function __construct(
protected HttpClientInterface $httpClient; protected KernelInterface $kernel,
protected UrlGeneratorInterface $urlGenerator; protected HttpClientInterface $httpClient,
protected UrlGeneratorInterface $urlGenerator
public function __construct(KernelInterface $kernel, HttpClientInterface $httpClient, UrlGeneratorInterface $urlGenerator) ) {
{
$this->kernel = $kernel;
$this->httpClient = $httpClient;
$this->urlGenerator = $urlGenerator;
} }
public function cleanRouting() public function cleanRouting()

View file

@ -18,19 +18,12 @@ class UserCreateCommand extends Command
{ {
protected static $defaultName = 'murph:user:create'; protected static $defaultName = 'murph:user:create';
protected static $defaultDescription = 'Creates a user'; protected static $defaultDescription = 'Creates a user';
protected UserFactory $userFactory;
protected EntityManager $entityManager;
protected TokenGeneratorInterface $tokenGenerator;
public function __construct( public function __construct(
UserFactory $userFactory, protected UserFactory $userFactory,
EntityManager $entityManager, protected EntityManager $entityManager,
TokenGeneratorInterface $tokenGenerator protected TokenGeneratorInterface $tokenGenerator
) { ) {
$this->userFactory = $userFactory;
$this->entityManager = $entityManager;
$this->tokenGenerator = $tokenGenerator;
parent::__construct(); parent::__construct();
} }

View file

@ -162,7 +162,7 @@ abstract class CrudController extends AdminController
$lastRequest = $session->get($lastRequestId); $lastRequest = $session->get($lastRequestId);
if ($lastRequest !== null && !$request->isMethod('POST')) { if (null !== $lastRequest && !$request->isMethod('POST')) {
$fakeRequest = Request::create( $fakeRequest = Request::create(
uri: $request->getUri(), uri: $request->getUri(),
method: 'POST', method: 'POST',
@ -284,16 +284,39 @@ abstract class CrudController extends AdminController
$query->useFilters($this->filters); $query->useFilters($this->filters);
if ('selection' === $target) { $useSelection = 'selection' === $target;
$isSelection = true;
$pager = $query->paginate($page, $configuration->getMaxPerPage($context)); if ($batchAction['isGlobal']) {
} else { $selection = null;
$isSelection = false;
$pager = $query->find(); if ($useSelection) {
$queryClone = clone $query;
$pager = $queryClone->paginate($page, $configuration->getMaxPerPage($context));
$selection = [];
foreach ($pager as $key => $entity) {
if (isset($items[$key + 1])) {
$selection[] = $entity;
}
}
}
$result = $callback($query, $entityManager, $selection);
if ($result instanceof Response) {
return $result;
}
return $this->redirect($request->query->get('redirectTo'));
} }
$pager = $useSelection
? $query->paginate($page, $configuration->getMaxPerPage($context))
: $query->find()
;
foreach ($pager as $key => $entity) { foreach ($pager as $key => $entity) {
if (($isSelection && isset($items[$key + 1])) || !$isSelection) { if (($useSelection && isset($items[$key + 1])) || !$useSelection) {
$callback($entity, $entityManager); $callback($entity, $entityManager);
} }
} }
@ -401,19 +424,22 @@ abstract class CrudController extends AdminController
$sessionSortName = sprintf('%s_label', $sessionId); $sessionSortName = sprintf('%s_label', $sessionId);
$sessionSortDirection = sprintf('%s_direction', $sessionId); $sessionSortDirection = sprintf('%s_direction', $sessionId);
$name = strtolower($request->query->get( $name = $request->query->get('_sort', $session->get($sessionSortName)) ?? $defaultSort['label'] ?? null;
'_sort',
$session->get($sessionSortName, $defaultSort['label'] ?? 'asc')
));
$direction = strtolower($request->query->get( $direction = strtolower(
'_sort_direction', $request->query->get(
$session->get($sessionSortDirection, $defaultSort['direction'] ?? 'asc') '_sort_direction',
)); $session->get($sessionSortDirection)
) ?? $defaultSort['direction'] ?? 'asc'
);
$session->set($sessionSortName, $name); $session->set($sessionSortName, $name);
$session->set($sessionSortDirection, $direction); $session->set($sessionSortDirection, $direction);
if (empty($name)) {
return;
}
if (!in_array($direction, ['asc', 'desc'])) { if (!in_array($direction, ['asc', 'desc'])) {
$direction = 'asc'; $direction = 'asc';
} }

View file

@ -3,8 +3,6 @@
namespace App\Core\Controller\Dashboard; namespace App\Core\Controller\Dashboard;
use App\Core\Controller\Admin\AdminController; use App\Core\Controller\Admin\AdminController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class DashboardAdminController extends AdminController class DashboardAdminController extends AdminController
{ {

View file

@ -0,0 +1,41 @@
<?php
namespace App\Core\Controller\Editor;
use App\Core\BuilderBlock\BuilderBlockContainer;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
#[Route(path: '/admin/editor/builder_block')]
class BuilderBlockController extends AbstractController
{
public function __construct(protected TranslatorInterface $translator)
{
}
#[Route(path: '/widgets', name: 'admin_editor_builder_block_widgets', options: ['expose' => true])]
public function widgets(BuilderBlockContainer $container): JsonResponse
{
$data = [];
foreach ($container->getWidgets() as $widget) {
$data[$widget->getName()] = $this->translate($widget->toArray());
}
return $this->json($data);
}
protected function translate(array $data)
{
$data['label'] = $this->translator->trans($data['label']);
$data['category'] = $this->translator->trans($data['category']);
foreach ($data['settings'] as $key => $value) {
$data['settings'][$key]['label'] = $this->translator->trans($data['settings'][$key]['label']);
}
return $data;
}
}

View file

@ -102,11 +102,11 @@ class RedirectAdminController extends CrudController
'attr' => ['class' => 'col-6'], 'attr' => ['class' => 'col-6'],
]) ])
->setField('index', 'Enabled', Field\ButtonField::class, [ ->setField('index', 'Enabled', Field\ButtonField::class, [
'property_builder' => function(EntityInterface $entity) { 'property_builder' => function (EntityInterface $entity) {
return $entity->getIsEnabled() ? 'Yes' : 'No'; return $entity->getIsEnabled() ? 'Yes' : 'No';
}, },
'attr' => ['class' => 'col-1'], 'attr' => ['class' => 'col-1'],
'button_attr_builder' => function(EntityInterface $entity) { 'button_attr_builder' => function (EntityInterface $entity) {
return ['class' => 'btn btn-sm btn-'.($entity->getIsEnabled() ? 'success' : 'primary')]; return ['class' => 'btn btn-sm btn-'.($entity->getIsEnabled() ? 'success' : 'primary')];
}, },
]) ])

View file

@ -36,7 +36,7 @@ class NavigationSettingAdminController extends AdminController
$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);
if ($lastRequest !== null && !$request->isMethod('POST')) { if (null !== $lastRequest && !$request->isMethod('POST')) {
$fakeRequest = Request::create( $fakeRequest = Request::create(
uri: $request->getUri(), uri: $request->getUri(),
method: 'POST', method: 'POST',

View file

@ -56,7 +56,7 @@ class SettingAdminController extends AdminController
$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);
if ($lastRequest !== null && !$request->isMethod('POST')) { if (null !== $lastRequest && !$request->isMethod('POST')) {
$fakeRequest = Request::create( $fakeRequest = Request::create(
uri: $request->getUri(), uri: $request->getUri(),
method: 'POST', method: 'POST',

View file

@ -67,7 +67,7 @@ class NavigationAdminController extends CrudController
} }
#[Route(path: '/admin/site/navigation/sort/{page}', name: 'admin_site_navigation_sort', methods: ['POST'], requirements: ['page' => '\d+'])] #[Route(path: '/admin/site/navigation/sort/{page}', name: 'admin_site_navigation_sort', methods: ['POST'], requirements: ['page' => '\d+'])]
public function sort(RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session, int $page = 1, ): Response public function sort(RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session, int $page = 1): Response
{ {
return $this->doSort($page, $query, $entityManager, $request, $session); return $this->doSort($page, $query, $entityManager, $request, $session);
} }

View file

@ -2,7 +2,6 @@
namespace App\Core\Controller\Site; namespace App\Core\Controller\Site;
use App\Core\Controller\Admin\AdminController;
use App\Core\Entity\Site\Node; use App\Core\Entity\Site\Node;
use App\Core\Entity\Site\Node as Entity; use App\Core\Entity\Site\Node as Entity;
use App\Core\Entity\Site\Page\Page; use App\Core\Entity\Site\Page\Page;
@ -14,15 +13,15 @@ use App\Core\Form\Site\NodeType as EntityType;
use App\Core\Manager\EntityManager; use App\Core\Manager\EntityManager;
use App\Core\Repository\Site\NodeRepository; use App\Core\Repository\Site\NodeRepository;
use App\Core\Site\ControllerLocator; use App\Core\Site\ControllerLocator;
use App\Core\Site\RoleLocator;
use App\Core\Site\PageLocator; use App\Core\Site\PageLocator;
use App\Core\Site\RoleLocator;
use App\Core\Sitemap\SitemapBuilder; use App\Core\Sitemap\SitemapBuilder;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormError;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
#[Route(path: '/admin/site/node')] #[Route(path: '/admin/site/node')]
class NodeAdminController extends AbstractController class NodeAdminController extends AbstractController
@ -145,7 +144,7 @@ class NodeAdminController extends AbstractController
$page = $entity->getPage(); $page = $entity->getPage();
if ($page !== null) { if (null !== $page) {
$pageConfiguration = $pageLocator->getPages()[get_class($page)] ?? null; $pageConfiguration = $pageLocator->getPages()[get_class($page)] ?? null;
} else { } else {
$pageConfiguration = null; $pageConfiguration = null;

View file

@ -5,19 +5,19 @@ namespace App\Core\Controller\Site;
use App\Core\Controller\Admin\Crud\CrudController; use App\Core\Controller\Admin\Crud\CrudController;
use App\Core\Crud\CrudConfiguration; use App\Core\Crud\CrudConfiguration;
use App\Core\Crud\Field; use App\Core\Crud\Field;
use App\Core\Entity\EntityInterface;
use App\Core\Entity\Site\Page\Page as Entity; use App\Core\Entity\Site\Page\Page as Entity;
use App\Core\Event\Page\PageEditEvent;
use App\Core\Form\Site\Page\Filter\PageFilterType as FilterType; use App\Core\Form\Site\Page\Filter\PageFilterType as FilterType;
use App\Core\Form\Site\Page\PageType as Type; use App\Core\Form\Site\Page\PageType as Type;
use App\Core\Manager\EntityManager; use App\Core\Manager\EntityManager;
use App\Core\Repository\Site\Page\PageRepositoryQuery as RepositoryQuery; use App\Core\Repository\Site\Page\PageRepositoryQuery as RepositoryQuery;
use App\Core\Site\PageLocator; use App\Core\Site\PageLocator;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use App\Core\Event\Page\PageEditEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use App\Core\Entity\EntityInterface;
class PageAdminController extends CrudController class PageAdminController extends CrudController
{ {
@ -114,7 +114,7 @@ class PageAdminController extends CrudController
}], }],
'attr' => ['class' => 'col-6'], 'attr' => ['class' => 'col-6'],
]) ])
->setBatchAction('index', 'delete', 'Delete', function(EntityInterface $entity, EntityManager $manager) { ->setBatchAction('index', 'delete', 'Delete', function (EntityInterface $entity, EntityManager $manager) {
$manager->delete($entity); $manager->delete($entity);
}) })
; ;

View file

@ -17,7 +17,6 @@ use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;
class UserAdminController extends CrudController class UserAdminController extends CrudController
{ {

View file

@ -40,7 +40,7 @@ class CrudConfiguration
return self::$self; return self::$self;
} }
/* -- */ // --
public function setPageTitle(string $page, string $title): self public function setPageTitle(string $page, string $title): self
{ {
@ -54,7 +54,7 @@ class CrudConfiguration
return $this->pageTitles[$page] ?? $default; return $this->pageTitles[$page] ?? $default;
} }
/* -- */ // --
public function setPageRoute(string $page, string $route): self public function setPageRoute(string $page, string $route): self
{ {
@ -80,9 +80,9 @@ class CrudConfiguration
return $this->pageRouteParams[$page] ?? []; return $this->pageRouteParams[$page] ?? [];
} }
/* -- */ // --
public function setForm(string $context, string $form, array $options = []): self public function setForm(string $context, string $form): self
{ {
$this->forms[$context] = $form; $this->forms[$context] = $form;
@ -106,7 +106,7 @@ class CrudConfiguration
return $this->formOptions[$context] ?? []; return $this->formOptions[$context] ?? [];
} }
/* -- */ // --
public function setAction(string $page, string $action, bool|callable $enabled): self public function setAction(string $page, string $action, bool|callable $enabled): self
{ {
@ -135,8 +135,24 @@ class CrudConfiguration
); );
} }
public function setBatchAction(string $page, string $action, string $label, callable $callback): self public function setGlobalBatchAction(
{ string $page,
string $action,
string $label,
callable $callback
): self {
$this->setBatchAction($page, $action, $label, $callback);
$this->batchActions[$page][$action]['isGlobal'] = true;
return $this;
}
public function setBatchAction(
string $page,
string $action,
string $label,
callable $callback
): self {
if (!isset($this->batchActions[$page])) { if (!isset($this->batchActions[$page])) {
$this->batchActions[$page] = []; $this->batchActions[$page] = [];
} }
@ -144,6 +160,7 @@ class CrudConfiguration
$this->batchActions[$page][$action] = [ $this->batchActions[$page][$action] = [
'label' => $label, 'label' => $label,
'callback' => $callback, 'callback' => $callback,
'isGlobal' => false,
]; ];
return $this; return $this;
@ -164,7 +181,7 @@ class CrudConfiguration
return !empty($this->batchActions[$page]); return !empty($this->batchActions[$page]);
} }
/* -- */ // --
public function setActionTitle(string $page, string $action, string $title): self public function setActionTitle(string $page, string $action, string $title): self
{ {
@ -182,7 +199,7 @@ class CrudConfiguration
return $this->actionTitles[$page][$action] ?? $default; return $this->actionTitles[$page][$action] ?? $default;
} }
/* -- */ // --
public function setView(string $context, string $view): self public function setView(string $context, string $view): self
{ {
@ -230,7 +247,7 @@ class CrudConfiguration
return $this->viewDatas[$context][$name] ?? $defaultValue; return $this->viewDatas[$context][$name] ?? $defaultValue;
} }
/* -- */ // --
public function setField(string $context, string $label, string $field, array $options): self public function setField(string $context, string $label, string $field, array $options): self
{ {
@ -258,7 +275,7 @@ class CrudConfiguration
return $this; return $this;
} }
/* -- */ // --
public function setMaxPerPage(string $page, int $max): self public function setMaxPerPage(string $page, int $max): self
{ {
@ -272,7 +289,7 @@ class CrudConfiguration
return $this->maxPerPage[$page] ?? $default; return $this->maxPerPage[$page] ?? $default;
} }
/* -- */ // --
public function setDoubleClick(string $page, bool $enabled): self public function setDoubleClick(string $page, bool $enabled): self
{ {
@ -286,7 +303,7 @@ class CrudConfiguration
return $this->doubleClick[$page] ?? false; return $this->doubleClick[$page] ?? false;
} }
/* -- */ // --
public function setI18n(array $locales, string $defaultLocale): self public function setI18n(array $locales, string $defaultLocale): self
{ {
@ -311,7 +328,7 @@ class CrudConfiguration
return !empty($this->locales); return !empty($this->locales);
} }
/* -- */ // --
public function setDefaultSort(string $context, string $label, string $direction = 'asc'): self public function setDefaultSort(string $context, string $label, string $direction = 'asc'): self
{ {

View file

@ -51,8 +51,8 @@ abstract class Field
$resolver->setAllowedTypes('href_attr', ['array', 'callable']); $resolver->setAllowedTypes('href_attr', ['array', 'callable']);
$resolver->setAllowedTypes('raw', 'boolean'); $resolver->setAllowedTypes('raw', 'boolean');
$resolver->setAllowedTypes('property_builder', ['null', 'callable']); $resolver->setAllowedTypes('property_builder', ['null', 'callable']);
$resolver->setAllowedValues('sort', function($value) { $resolver->setAllowedValues('sort', function ($value) {
if ($value === null) { if (null === $value) {
return true; return true;
} }

View file

@ -0,0 +1,25 @@
<?php
namespace App\Core\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use App\Core\BuilderBlock\BuilderBlockContainer;
class BuilderBlockPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->has(BuilderBlockContainer::class)) {
return;
}
$definition = $container->findDefinition(BuilderBlockContainer::class);
$taggedServices = $container->findTaggedServiceIds('builder_block.widget');
foreach ($taggedServices as $id => $tags) {
$definition->addMethodCall('addWidget', [new Reference($id)]);
}
}
}

View file

@ -139,6 +139,7 @@ class Configuration implements ConfigurationInterface
->end() ->end()
->end() ->end()
->end(); ->end();
;
return $treeBuilder; return $treeBuilder;
} }

View file

@ -2,6 +2,7 @@
namespace App\Core\DependencyInjection; namespace App\Core\DependencyInjection;
use App\Core\DependencyInjection\Compiler\BuilderBlockPass;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Extension\Extension;

View file

@ -2,10 +2,9 @@
namespace App\Core\Entity\Analytic; namespace App\Core\Entity\Analytic;
use App\Core\Entity\Site\Node;
use App\Repository\Entity\Analytic\NodeViewRepository;
use Doctrine\ORM\Mapping as ORM;
use App\Core\Entity\EntityInterface; use App\Core\Entity\EntityInterface;
use App\Core\Entity\Site\Node;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Table(name: 'analytic_referer')] #[ORM\Table(name: 'analytic_referer')]
#[ORM\Entity(repositoryClass: ViewRepository::class)] #[ORM\Entity(repositoryClass: ViewRepository::class)]

View file

@ -2,10 +2,9 @@
namespace App\Core\Entity\Analytic; namespace App\Core\Entity\Analytic;
use App\Core\Entity\Site\Node;
use App\Repository\Entity\Analytic\NodeViewRepository;
use Doctrine\ORM\Mapping as ORM;
use App\Core\Entity\EntityInterface; use App\Core\Entity\EntityInterface;
use App\Core\Entity\Site\Node;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Table(name: 'analytic_view')] #[ORM\Table(name: 'analytic_view')]
#[ORM\Entity(repositoryClass: ViewRepository::class)] #[ORM\Entity(repositoryClass: ViewRepository::class)]

View file

@ -43,7 +43,7 @@ class Navigation implements EntityInterface
protected $locale = 'en'; protected $locale = 'en';
#[ORM\Column(type: 'string', length: 7, nullable: true)] #[ORM\Column(type: 'string', length: 7, nullable: true)]
protected $color = null; protected $color;
#[ORM\Column(type: 'integer', nullable: true)] #[ORM\Column(type: 'integer', nullable: true)]
protected $sortOrder; protected $sortOrder;
@ -67,7 +67,7 @@ class Navigation implements EntityInterface
return $this->label; return $this->label;
} }
public function setLabel(string $label): self public function setLabel(?string $label): self
{ {
$this->label = $label; $this->label = $label;

View file

@ -0,0 +1,20 @@
<?php
namespace App\Core\Entity\Site\Page;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class BuilderBlock extends JsonBlock
{
public function getValue()
{
$value = parent::getValue();
if (is_string($value)) {
return json_decode($value, true);
}
return [];
}
}

View file

@ -5,15 +5,6 @@ namespace App\Core\Entity\Site\Page;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity] #[ORM\Entity]
class ChoiceBlock extends Block class ChoiceBlock extends JsonBlock
{ {
public function getValue()
{
return json_decode(parent::getValue(), true);
}
public function setValue($value): self
{
return parent::setValue(json_encode($value));
}
} }

View file

@ -0,0 +1,19 @@
<?php
namespace App\Core\Entity\Site\Page;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class JsonBlock extends Block
{
public function getValue()
{
return json_decode(parent::getValue(), true);
}
public function setValue($value): self
{
return parent::setValue(json_encode($value));
}
}

View file

@ -5,13 +5,12 @@ namespace App\Core\Entity\Site\Page;
use App\Core\Doctrine\Timestampable; use App\Core\Doctrine\Timestampable;
use App\Core\Entity\EntityInterface; use App\Core\Entity\EntityInterface;
use App\Core\Entity\Site\Node; use App\Core\Entity\Site\Node;
use App\Core\File\FileAttribute;
use App\Core\Repository\Site\Page\PageRepository; use App\Core\Repository\Site\Page\PageRepository;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\File\File;
use App\Core\File\FileAttribute;
#[ORM\Entity(repositoryClass: PageRepository::class)] #[ORM\Entity(repositoryClass: PageRepository::class)]
#[ORM\DiscriminatorColumn(name: 'class_key', type: 'string')] #[ORM\DiscriminatorColumn(name: 'class_key', type: 'string')]
@ -89,7 +88,7 @@ class Page implements EntityInterface
} }
/** /**
* @return Collection|Block[] * @return Block[]|Collection
*/ */
public function getBlocks(): Collection public function getBlocks(): Collection
{ {

View file

@ -15,11 +15,8 @@ class AbTestEvent extends Event
public const INIT_EVENT = 'ab_test.init'; public const INIT_EVENT = 'ab_test.init';
public const RUN_EVENT = 'ab_test.run'; public const RUN_EVENT = 'ab_test.run';
protected AbTest $test; public function __construct(protected AbTest $test)
public function __construct(AbTest $test)
{ {
$this->test = $test;
} }
public function getTest(): AbTest public function getTest(): AbTest

View file

@ -12,13 +12,10 @@ use Symfony\Contracts\EventDispatcher\Event;
*/ */
class PasswordRequestEvent extends Event class PasswordRequestEvent extends Event
{ {
const EVENT = 'account_event.password_request'; public const EVENT = 'account_event.password_request';
protected User $user; public function __construct(protected User $user)
public function __construct(User $user)
{ {
$this->user = $user;
} }
public function getUser(): User public function getUser(): User

View file

@ -12,18 +12,15 @@ use Symfony\Contracts\EventDispatcher\Event;
*/ */
class EntityManagerEvent extends Event class EntityManagerEvent extends Event
{ {
const CREATE_EVENT = 'entity_manager_event.create'; public const CREATE_EVENT = 'entity_manager_event.create';
const UPDATE_EVENT = 'entity_manager_event.update'; public const UPDATE_EVENT = 'entity_manager_event.update';
const DELETE_EVENT = 'entity_manager_event.delete'; public const DELETE_EVENT = 'entity_manager_event.delete';
const PRE_CREATE_EVENT = 'entity_manager_event.pre_create'; public const PRE_CREATE_EVENT = 'entity_manager_event.pre_create';
const PRE_UPDATE_EVENT = 'entity_manager_event.pre_update'; public const PRE_UPDATE_EVENT = 'entity_manager_event.pre_update';
const PRE_DELETE_EVENT = 'entity_manager_event.pre_delete'; public const PRE_DELETE_EVENT = 'entity_manager_event.pre_delete';
protected EntityInterface $entity; public function __construct(protected EntityInterface $entity)
public function __construct(EntityInterface $entity)
{ {
$this->entity = $entity;
} }
public function getEntity(): EntityInterface public function getEntity(): EntityInterface

View file

@ -2,8 +2,8 @@
namespace App\Core\Event\Page; namespace App\Core\Event\Page;
use Symfony\Contracts\EventDispatcher\Event;
use App\Core\Entity\Site\Page\Page; use App\Core\Entity\Site\Page\Page;
use Symfony\Contracts\EventDispatcher\Event;
/** /**
* class PageEditEvent. * class PageEditEvent.
@ -12,14 +12,12 @@ use App\Core\Entity\Site\Page\Page;
*/ */
class PageEditEvent extends Event class PageEditEvent extends Event
{ {
const FORM_INIT_EVENT = 'page_edit_event.form_init'; public const FORM_INIT_EVENT = 'page_edit_event.form_init';
protected Page $page;
protected array $pageBuilderOptions = []; protected array $pageBuilderOptions = [];
public function __construct(Page $page) public function __construct(protected Page $page)
{ {
$this->page = $page;
} }
public function getPage() public function getPage()

View file

@ -11,14 +11,11 @@ use Symfony\Contracts\EventDispatcher\Event;
*/ */
class NavigationSettingEvent extends Event class NavigationSettingEvent extends Event
{ {
const INIT_EVENT = 'navigation_setting_event.init'; public const INIT_EVENT = 'navigation_setting_event.init';
const FORM_INIT_EVENT = 'navigation_setting_event.form_init'; public const FORM_INIT_EVENT = 'navigation_setting_event.form_init';
protected $data; public function __construct(protected $data = null)
public function __construct($data = null)
{ {
$this->data = $data;
} }
public function getData() public function getData()

View file

@ -11,14 +11,11 @@ use Symfony\Contracts\EventDispatcher\Event;
*/ */
class SettingEvent extends Event class SettingEvent extends Event
{ {
const INIT_EVENT = 'setting_event.init'; public const INIT_EVENT = 'setting_event.init';
const FORM_INIT_EVENT = 'setting_event.form_init'; public const FORM_INIT_EVENT = 'setting_event.form_init';
protected $data; public function __construct(protected $data = null)
public function __construct($data = null)
{ {
$this->data = $data;
} }
public function getData() public function getData()

View file

@ -11,7 +11,7 @@ use Symfony\Contracts\EventDispatcher\Event;
*/ */
class TaskInitEvent extends Event class TaskInitEvent extends Event
{ {
const INIT_EVENT = 'task_event.init'; public const INIT_EVENT = 'task_event.init';
protected array $tasks = []; protected array $tasks = [];

View file

@ -14,17 +14,13 @@ use Symfony\Contracts\EventDispatcher\Event;
*/ */
class TaskRunRequestedEvent extends Event class TaskRunRequestedEvent extends Event
{ {
const RUN_REQUEST_EVENT = 'task_event.run_request'; public const RUN_REQUEST_EVENT = 'task_event.run_request';
protected string $task; public function __construct(
protected InputBag $parameters; protected string $task,
protected BufferedOutput $output; protected InputBag $parameters,
protected BufferedOutput $output
public function __construct(string $task, InputBag $parameters, BufferedOutput $output) ) {
{
$this->task = $task;
$this->parameters = $parameters;
$this->output = $output;
} }
public function getTask(): string public function getTask(): string

View file

@ -6,7 +6,6 @@ use App\Core\Ab\AbContainer;
use App\Core\Ab\AbTest; use App\Core\Ab\AbTest;
use App\Core\Entity\Site\Node; use App\Core\Entity\Site\Node;
use App\Core\Event\Ab\AbTestEvent; use App\Core\Event\Ab\AbTestEvent;
use App\Core\Repository\Site\NodeRepository;
use App\Core\Site\SiteRequest; use App\Core\Site\SiteRequest;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Cookie;
@ -21,19 +20,13 @@ use Symfony\Component\HttpKernel\Event\ResponseEvent;
*/ */
class AbListener class AbListener
{ {
protected EventDispatcherInterface $eventDispatcher;
protected AbContainer $container;
protected SiteRequest $siteRequest;
protected ?Node $node; protected ?Node $node;
public function __construct( public function __construct(
AbContainer $container, protected AbContainer $container,
EventDispatcherInterface $eventDispatcher, protected EventDispatcherInterface $eventDispatcher,
SiteRequest $siteRequest protected SiteRequest $siteRequest
) { ) {
$this->eventDispatcher = $eventDispatcher;
$this->container = $container;
$this->siteRequest = $siteRequest;
} }
public function onKernelRequest(RequestEvent $event) public function onKernelRequest(RequestEvent $event)
@ -70,6 +63,16 @@ class AbListener
} }
} }
public function onKernelResponse(ResponseEvent $event)
{
$cookies = $event->getRequest()->attributes->get('ab_test_cookies', []);
foreach ($cookies as $name => $value) {
$cookie = Cookie::create($name, $value['value'], time() + $value['duration']);
$event->getResponse()->headers->setCookie($cookie);
}
}
protected function getCookieName(): string protected function getCookieName(): string
{ {
return 'ab_test_'.$this->getAbTestCode(); return 'ab_test_'.$this->getAbTestCode();
@ -96,14 +99,4 @@ class AbListener
return true; return true;
} }
public function onKernelResponse(ResponseEvent $event)
{
$cookies = $event->getRequest()->attributes->get('ab_test_cookies', []);
foreach ($cookies as $name => $value) {
$cookie = Cookie::create($name, $value['value'], time() + $value['duration']);
$event->getResponse()->headers->setCookie($cookie);
}
}
} }

View file

@ -23,30 +23,18 @@ use Symfony\Component\HttpKernel\Event\RequestEvent;
*/ */
class AnalyticListener class AnalyticListener
{ {
protected NodeRepository $nodeRepository;
protected ViewRepositoryQuery $viewRepositoryQuery;
protected ViewFactory $viewFactory;
protected RefererRepositoryQuery $refererRepositoryQuery;
protected RefererFactory $refererFactory;
protected EntityManager $manager;
protected DeviceDetector $deviceDetector; protected DeviceDetector $deviceDetector;
protected Request $request; protected Request $request;
protected Node $node; protected Node $node;
public function __construct( public function __construct(
NodeRepository $nodeRepository, protected NodeRepository $nodeRepository,
ViewRepositoryQuery $viewRepositoryQuery, protected ViewRepositoryQuery $viewRepositoryQuery,
ViewFactory $viewFactory, protected ViewFactory $viewFactory,
RefererRepositoryQuery $refererRepositoryQuery, protected RefererRepositoryQuery $refererRepositoryQuery,
RefererFactory $refererFactory, protected RefererFactory $refererFactory,
EntityManager $manager protected EntityManager $manager
) { ) {
$this->nodeRepository = $nodeRepository;
$this->viewRepositoryQuery = $viewRepositoryQuery;
$this->viewFactory = $viewFactory;
$this->refererRepositoryQuery = $refererRepositoryQuery;
$this->refererFactory = $refererFactory;
$this->manager = $manager;
$this->createDeviceDetector(); $this->createDeviceDetector();
} }

View file

@ -3,11 +3,10 @@
namespace App\Core\EventListener; namespace App\Core\EventListener;
use App\Core\Repository\RedirectRepositoryQuery; use App\Core\Repository\RedirectRepositoryQuery;
use App\Core\Router\RedirectBuilder;
use App\Core\Router\RedirectMatcher; use App\Core\Router\RedirectMatcher;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use App\Core\Router\RedirectBuilder;
/** /**
* class RedirectListener. * class RedirectListener.
@ -16,15 +15,11 @@ use App\Core\Router\RedirectBuilder;
*/ */
class RedirectListener class RedirectListener
{ {
protected RedirectMatcher $matcher; public function __construct(
protected RedirectBuilder $builder; protected RedirectMatcher $matcher,
protected RedirectRepositoryQuery $repository; protected RedirectBuilder $builder,
protected RedirectRepositoryQuery $repository
public function __construct(RedirectMatcher $matcher, RedirectBuilder $builder, RedirectRepositoryQuery $repository) ) {
{
$this->matcher = $matcher;
$this->builder = $builder;
$this->repository = $repository;
} }
public function onKernelException(ExceptionEvent $event) public function onKernelException(ExceptionEvent $event)

View file

@ -17,24 +17,13 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*/ */
class PasswordRequestEventSubscriber implements EventSubscriberInterface class PasswordRequestEventSubscriber implements EventSubscriberInterface
{ {
protected MailNotifier $notifier;
protected UrlGeneratorInterface $urlGenerator;
protected EntityManager $entityManager;
protected TokenGeneratorInterface $tokenGenerator;
protected TranslatorInterface $translator;
public function __construct( public function __construct(
MailNotifier $notifier, protected MailNotifier $notifier,
UrlGeneratorInterface $urlGenerator, protected UrlGeneratorInterface $urlGenerator,
EntityManager $entityManager, protected EntityManager $entityManager,
TokenGeneratorInterface $tokenGenerator, protected TokenGeneratorInterface $tokenGenerator,
TranslatorInterface $translator protected TranslatorInterface $translator
) { ) {
$this->notifier = $notifier;
$this->urlGenerator = $urlGenerator;
$this->entityManager = $entityManager;
$this->tokenGenerator = $tokenGenerator;
$this->translator = $translator;
} }
public static function getSubscribedEvents() public static function getSubscribedEvents()

View file

@ -16,12 +16,12 @@ use Symfony\Component\Security\Core\Exception\AccessDeniedException;
*/ */
class RequestSecurityEventSubscriber implements EventSubscriberInterface class RequestSecurityEventSubscriber implements EventSubscriberInterface
{ {
protected NodeRepository $nodeRepository;
protected AuthorizationChecker $authorizationChecker; protected AuthorizationChecker $authorizationChecker;
public function __construct(NodeRepository $nodeRepository, ContainerInterface $container) public function __construct(
{ protected NodeRepository $nodeRepository,
$this->nodeRepository = $nodeRepository; ContainerInterface $container
) {
$this->authorizationChecker = $container->get('security.authorization_checker'); $this->authorizationChecker = $container->get('security.authorization_checker');
} }

View file

@ -4,18 +4,15 @@ namespace App\Core\EventSubscriber\Site;
use App\Core\Site\SiteRequest; use App\Core\Site\SiteRequest;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use function Symfony\Component\String\u; use function Symfony\Component\String\u;
class ForcedDomainEventSubscriber implements EventSubscriberInterface class ForcedDomainEventSubscriber implements EventSubscriberInterface
{ {
protected SiteRequest $siteRequest; public function __construct(protected SiteRequest $siteRequest)
public function __construct(SiteRequest $siteRequest)
{ {
$this->siteRequest = $siteRequest;
} }
public function onKernelResponse(ResponseEvent $event) public function onKernelResponse(ResponseEvent $event)
@ -38,7 +35,8 @@ class ForcedDomainEventSubscriber implements EventSubscriberInterface
->replace( ->replace(
'://'.$this->siteRequest->getDomain(), '://'.$this->siteRequest->getDomain(),
'://'.$navigation->getDomain() '://'.$navigation->getDomain()
); )
;
$event->getResponse()->headers->set('Location', $uri); $event->getResponse()->headers->set('Location', $uri);
$event->getResponse()->setStatusCode(Response::HTTP_MOVED_PERMANENTLY); $event->getResponse()->setStatusCode(Response::HTTP_MOVED_PERMANENTLY);

View file

@ -20,27 +20,14 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*/ */
class MenuEventSubscriber extends EntityManagerEventSubscriber class MenuEventSubscriber extends EntityManagerEventSubscriber
{ {
protected NodeFactory $nodeFactory;
protected NodeRepository $nodeRepository;
protected EntityManager $entityManager;
protected CodeSlugify $slugify;
protected SymfonyCacheManager $cacheManager;
protected TranslatorInterface $translation;
public function __construct( public function __construct(
NodeFactory $nodeFactory, protected NodeFactory $nodeFactory,
NodeRepository $nodeRepository, protected NodeRepository $nodeRepository,
EntityManager $entityManager, protected EntityManager $entityManager,
CodeSlugify $slugify, protected CodeSlugify $slugify,
SymfonyCacheManager $cacheManager, protected SymfonyCacheManager $cacheManager,
TranslatorInterface $translator protected TranslatorInterface $translator
) { ) {
$this->nodeFactory = $nodeFactory;
$this->nodeRepository = $nodeRepository;
$this->entityManager = $entityManager;
$this->slugify = $slugify;
$this->cacheManager = $cacheManager;
$this->translator = $translator;
} }
public function supports(EntityInterface $entity): bool public function supports(EntityInterface $entity): bool

View file

@ -17,11 +17,9 @@ use App\Core\Slugify\CodeSlugify;
class NavigationEventSubscriber extends EntityManagerEventSubscriber class NavigationEventSubscriber extends EntityManagerEventSubscriber
{ {
public function __construct( public function __construct(
EntityManager $entityManager, protected EntityManager $entityManager,
CodeSlugify $slugify protected CodeSlugify $slugify
) { ) {
$this->entityManager = $entityManager;
$this->slugify = $slugify;
} }
public function supports(EntityInterface $entity): bool public function supports(EntityInterface $entity): bool

View file

@ -12,7 +12,6 @@ use App\Core\Repository\Site\NodeRepository;
use App\Core\Slugify\CodeSlugify; use App\Core\Slugify\CodeSlugify;
use App\Core\Slugify\RouteParameterSlugify; use App\Core\Slugify\RouteParameterSlugify;
use App\Core\Slugify\Slugify; use App\Core\Slugify\Slugify;
use Symfony\Component\HttpKernel\KernelInterface;
use function Symfony\Component\String\u; use function Symfony\Component\String\u;
/** /**
@ -22,27 +21,14 @@ use function Symfony\Component\String\u;
*/ */
class NodeEventSubscriber extends EntityManagerEventSubscriber class NodeEventSubscriber extends EntityManagerEventSubscriber
{ {
protected NodeFactory $nodeFactory;
protected EntityManager $entityManager;
protected KernelInterface $kernel;
protected Slugify $slugify;
protected CodeSlugify $codeSlugify;
protected RouteParameterSlugify $routeParameterSlugify;
public function __construct( public function __construct(
NodeFactory $nodeFactory, protected NodeFactory $nodeFactory,
NodeRepository $nodeRepository, protected NodeRepository $nodeRepository,
EntityManager $entityManager, protected EntityManager $entityManager,
Slugify $slugify, protected Slugify $slugify,
CodeSlugify $codeSlugify, protected CodeSlugify $codeSlugify,
RouteParameterSlugify $routeParameterSlugify protected RouteParameterSlugify $routeParameterSlugify
) { ) {
$this->nodeFactory = $nodeFactory;
$this->nodeRepository = $nodeRepository;
$this->entityManager = $entityManager;
$this->slugify = $slugify;
$this->codeSlugify = $codeSlugify;
$this->routeParameterSlugify = $routeParameterSlugify;
} }
public function supports(EntityInterface $entity): bool public function supports(EntityInterface $entity): bool
@ -63,7 +49,7 @@ class NodeEventSubscriber extends EntityManagerEventSubscriber
$node = $event->getEntity(); $node = $event->getEntity();
$node->setCode($this->codeSlugify->slugify($node->getCode())); $node->setCode($this->codeSlugify->slugify($node->getCode() ?? ''));
if ($node->getDisableUrl()) { if ($node->getDisableUrl()) {
$node->setUrl(null); $node->setUrl(null);

View file

@ -17,11 +17,8 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
*/ */
class BlockEventSubscriber extends EntityManagerEventSubscriber class BlockEventSubscriber extends EntityManagerEventSubscriber
{ {
protected FileUploadHandler $fileUpload; public function __construct(protected FileUploadHandler $fileUpload)
public function __construct(FileUploadHandler $fileUpload)
{ {
$this->fileUpload = $fileUpload;
} }
public function supports(EntityInterface $entity): bool public function supports(EntityInterface $entity): bool

View file

@ -16,11 +16,8 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
*/ */
class PageEventSubscriber extends EntityManagerEventSubscriber class PageEventSubscriber extends EntityManagerEventSubscriber
{ {
protected FileUploadHandler $fileUpload; public function __construct(protected FileUploadHandler $fileUpload)
public function __construct(FileUploadHandler $fileUpload)
{ {
$this->fileUpload = $fileUpload;
} }
public function supports(EntityInterface $entity): bool public function supports(EntityInterface $entity): bool

View file

@ -18,13 +18,10 @@ use Symfony\Component\HttpKernel\KernelInterface;
*/ */
class SiteEventSubscriber extends EntityManagerEventSubscriber class SiteEventSubscriber extends EntityManagerEventSubscriber
{ {
protected KernelInterface $kernel; public function __construct(
protected SymfonyCacheManager $cacheManager; protected KernelInterface $kernel,
protected SymfonyCacheManager $cacheManager
public function __construct(KernelInterface $kernel, SymfonyCacheManager $cacheManager) ) {
{
$this->kernel = $kernel;
$this->cacheManager = $cacheManager;
} }
public function supports(EntityInterface $entity): bool public function supports(EntityInterface $entity): bool

View file

@ -13,11 +13,8 @@ use App\Core\Event\Task\TaskRunRequestedEvent;
*/ */
class CacheCleanTaskEventSubscriber extends TaskEventSubscriber class CacheCleanTaskEventSubscriber extends TaskEventSubscriber
{ {
protected SymfonyCacheManager $cacheManager; public function __construct(protected SymfonyCacheManager $cacheManager)
public function __construct(SymfonyCacheManager $cacheManager)
{ {
$this->cacheManager = $cacheManager;
} }
public function onInit(TaskInitEvent $event) public function onInit(TaskInitEvent $event)

View file

@ -2,7 +2,6 @@
namespace App\Core\Factory; namespace App\Core\Factory;
use App\Core\Factory\FactoryInterface;
use App\Core\Entity\Redirect as Entity; use App\Core\Entity\Redirect as Entity;
class RedirectFactory implements FactoryInterface class RedirectFactory implements FactoryInterface

View file

@ -23,22 +23,15 @@ class FsFileManager
protected string $path; protected string $path;
protected string $pathUri; protected string $pathUri;
protected array $pathLocked; protected array $pathLocked;
protected FileUploadHandler $uploadHandler;
protected FileInformationFactory $fileInformationFactory;
protected FileInformationRepositoryQuery $fileInformationRepositoryQuery;
public function __construct( public function __construct(
ParameterBagInterface $params, ParameterBagInterface $params,
FileUploadHandler $uploadHandler, protected FileUploadHandler $uploadHandler,
FileInformationFactory $fileInformationFactory, protected FileInformationFactory $fileInformationFactory,
FileInformationRepositoryQuery $fileInformationRepositoryQuery protected FileInformationRepositoryQuery $fileInformationRepositoryQuery
) { ) {
$config = $params->get('core')['file_manager']; $config = $params->get('core')['file_manager'];
$this->uploadHandler = $uploadHandler;
$this->fileInformationFactory = $fileInformationFactory;
$this->fileInformationRepositoryQuery = $fileInformationRepositoryQuery;
$this->mimes = $config['mimes']; $this->mimes = $config['mimes'];
$this->path = $config['path']; $this->path = $config['path'];
$this->pathUri = $this->normalizePath($config['path_uri']); $this->pathUri = $this->normalizePath($config['path_uri']);

View file

@ -6,7 +6,6 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView; use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
class FilePickerType extends AbstractType class FilePickerType extends AbstractType
{ {

View file

@ -2,13 +2,11 @@
namespace App\Core\Form\Filter; namespace App\Core\Form\Filter;
use App\Core\Entity\Redirect;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
class RedirectFilterType extends AbstractType class RedirectFilterType extends AbstractType
{ {

View file

@ -4,12 +4,12 @@ namespace App\Core\Form;
use App\Core\Entity\Redirect; use App\Core\Entity\Redirect;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
class RedirectType extends AbstractType class RedirectType extends AbstractType
{ {

View file

@ -7,6 +7,7 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotBlank;
class MenuType extends AbstractType class MenuType extends AbstractType
@ -23,6 +24,7 @@ class MenuType extends AbstractType
], ],
'constraints' => [ 'constraints' => [
new NotBlank(), new NotBlank(),
new Length(max: 255),
], ],
] ]
); );
@ -37,6 +39,7 @@ class MenuType extends AbstractType
], ],
'constraints' => [ 'constraints' => [
new NotBlank(), new NotBlank(),
new Length(max: 255),
], ],
] ]
); );

View file

@ -7,6 +7,7 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotBlank;
class NavigationAdditionalDomainType extends AbstractType class NavigationAdditionalDomainType extends AbstractType
@ -24,6 +25,7 @@ class NavigationAdditionalDomainType extends AbstractType
], ],
'constraints' => [ 'constraints' => [
new NotBlank(), new NotBlank(),
new Length(max: 255),
], ],
] ]
); );

View file

@ -6,12 +6,12 @@ use App\Core\Entity\Site\Navigation;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType; use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\ColorType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Form\Extension\Core\Type\ColorType;
class NavigationType extends AbstractType class NavigationType extends AbstractType
{ {
@ -27,6 +27,7 @@ class NavigationType extends AbstractType
], ],
'constraints' => [ 'constraints' => [
new NotBlank(), new NotBlank(),
new Length(max: 255),
], ],
] ]
); );
@ -41,6 +42,7 @@ class NavigationType extends AbstractType
], ],
'constraints' => [ 'constraints' => [
new NotBlank(), new NotBlank(),
new Length(max: 255),
], ],
] ]
); );
@ -69,6 +71,7 @@ class NavigationType extends AbstractType
], ],
'constraints' => [ 'constraints' => [
new NotBlank(), new NotBlank(),
new Length(max: 255),
], ],
] ]
); );
@ -109,7 +112,7 @@ class NavigationType extends AbstractType
], ],
'constraints' => [ 'constraints' => [
new NotBlank(), new NotBlank(),
new Length(['min' => 2, 'max' => 10]), new Length(min: 2, max: 10),
], ],
] ]
); );

View file

@ -13,6 +13,7 @@ use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotBlank;
class NodeType extends AbstractType class NodeType extends AbstractType
@ -29,6 +30,7 @@ class NodeType extends AbstractType
], ],
'constraints' => [ 'constraints' => [
new NotBlank(), new NotBlank(),
new Length(max: 255),
], ],
] ]
); );
@ -43,6 +45,7 @@ class NodeType extends AbstractType
'attr' => [ 'attr' => [
], ],
'constraints' => [ 'constraints' => [
new Length(max: 255),
], ],
] ]
); );
@ -82,6 +85,7 @@ class NodeType extends AbstractType
'attr' => [ 'attr' => [
], ],
'constraints' => [ 'constraints' => [
new Length(max: 255),
], ],
] ]
); );
@ -116,6 +120,9 @@ class NodeType extends AbstractType
return $choices; return $choices;
}), }),
'constraints' => [
new Length(max: 255),
],
] ]
); );

View file

@ -0,0 +1,32 @@
<?php
namespace App\Core\Form\Site\Page;
use App\Core\Entity\Site\Page\BuilderBlock;
use App\Core\Form\Type\BuilderType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class BuilderBlockType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'value',
BuilderType::class,
array_merge([
'required' => false,
'label' => false,
], $options['options']),
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => BuilderBlock::class,
'options' => [],
]);
}
}

View file

@ -2,11 +2,11 @@
namespace App\Core\Form\Site\Page; namespace App\Core\Form\Site\Page;
use App\Core\Entity\Site\Page\Block;
use App\Core\Form\FileManager\FilePickerType; use App\Core\Form\FileManager\FilePickerType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\AbstractType;
use App\Core\Entity\Site\Page\Block;
class FilePickerBlockType extends AbstractType class FilePickerBlockType extends AbstractType
{ {

View file

@ -6,10 +6,10 @@ use App\Core\Entity\Site\Navigation;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
class PageFilterType extends AbstractType class PageFilterType extends AbstractType
{ {

View file

@ -10,6 +10,7 @@ use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Image; use Symfony\Component\Validator\Constraints\Image;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotBlank;
class PageType extends AbstractType class PageType extends AbstractType
@ -26,6 +27,7 @@ class PageType extends AbstractType
], ],
'constraints' => [ 'constraints' => [
new NotBlank(), new NotBlank(),
new Length(max: 255),
], ],
] ]
); );
@ -39,6 +41,7 @@ class PageType extends AbstractType
'attr' => [ 'attr' => [
], ],
'constraints' => [ 'constraints' => [
new Length(max: 255),
], ],
] ]
); );
@ -52,6 +55,7 @@ class PageType extends AbstractType
'attr' => [ 'attr' => [
], ],
'constraints' => [ 'constraints' => [
new Length(max: 255),
], ],
] ]
); );
@ -65,6 +69,7 @@ class PageType extends AbstractType
'attr' => [ 'attr' => [
], ],
'constraints' => [ 'constraints' => [
new Length(max: 255),
], ],
] ]
); );
@ -78,6 +83,7 @@ class PageType extends AbstractType
'attr' => [ 'attr' => [
], ],
'constraints' => [ 'constraints' => [
new Length(max: 255),
], ],
] ]
); );

View file

@ -0,0 +1,38 @@
<?php
namespace App\Core\Form\Type;
use Symfony\Component\Form\Extension\Core\Type\CollectionType as BaseCollectionType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
class BuilderType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
parent::buildView($view, $form, $options);
$view->vars = array_replace($view->vars, [
]);
}
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'compound' => false,
]);
}
public function getBlockPrefix()
{
return 'builder';
}
}

View file

@ -22,6 +22,8 @@ class CollectionType extends BaseCollectionType
'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'],
'template_before_item' => $options['template_before_item'],
'template_after_item' => $options['template_after_item'],
]); ]);
} }
@ -33,6 +35,8 @@ class CollectionType extends BaseCollectionType
'collection_name' => '', 'collection_name' => '',
'label_add' => 'Add', 'label_add' => 'Add',
'label_delete' => 'Delete', 'label_delete' => 'Delete',
'template_before_item' => null,
'template_after_item' => null,
]); ]);
} }

View 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)
{
}
}

View file

@ -11,8 +11,8 @@ use Symfony\Bundle\MakerBundle\Maker\AbstractMaker;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use function Symfony\Component\String\u;
use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Filesystem\Filesystem;
use function Symfony\Component\String\u;
class MakeCrudController extends AbstractMaker class MakeCrudController extends AbstractMaker
{ {

View file

@ -11,7 +11,6 @@ use Symfony\Bundle\MakerBundle\Maker\AbstractMaker;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use function Symfony\Component\String\u;
class MakeFactory extends AbstractMaker class MakeFactory extends AbstractMaker
{ {

View file

@ -8,11 +8,11 @@ use Symfony\Bundle\MakerBundle\DependencyBuilder;
use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\Generator;
use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\InputConfiguration;
use Symfony\Bundle\MakerBundle\Maker\AbstractMaker; use Symfony\Bundle\MakerBundle\Maker\AbstractMaker;
use Symfony\Bundle\MakerBundle\Str;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Question\Question;
use Symfony\Bundle\MakerBundle\Str;
use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Filesystem\Filesystem;
class MakePage extends AbstractMaker class MakePage extends AbstractMaker
@ -92,7 +92,8 @@ class MakePage extends AbstractMaker
$this->writeSuccessMessage($io); $this->writeSuccessMessage($io);
$io->text('Register the page in <comment>config/packages/app.yaml</comment>: '); $io->text('Register the page in <comment>config/packages/app.yaml</comment>: ');
$io->text(<<< EOF $io->text(
<<< EOF
core: core:
site: site:
@ -100,10 +101,18 @@ core:
{$pageClassNameDetails->getFullName()}: {$pageClassNameDetails->getFullName()}:
name: {$pageClassNameDetails->getShortName()} name: {$pageClassNameDetails->getShortName()}
templates: templates:
- {name: "Default", file: "${templatePath}"} - {name: "Default", file: "{$templatePath}"}
EOF EOF
); );
}
public function configureDependencies(DependencyBuilder $dependencies)
{
$dependencies->addClassDependency(
Annotation::class,
'doctrine/annotations'
);
} }
private function askForNextBlock(ConsoleStyle $io, array $blocks, bool $isFirstField) private function askForNextBlock(ConsoleStyle $io, array $blocks, bool $isFirstField)
@ -140,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,
@ -183,12 +193,4 @@ EOF
$io->writeln(sprintf(' * <comment>%s</comment>', $type)); $io->writeln(sprintf(' * <comment>%s</comment>', $type));
} }
} }
public function configureDependencies(DependencyBuilder $dependencies)
{
$dependencies->addClassDependency(
Annotation::class,
'doctrine/annotations'
);
}
} }

View file

@ -4,7 +4,6 @@ namespace App\Core\Manager;
use App\Core\Entity\EntityInterface; use App\Core\Entity\EntityInterface;
use App\Core\Event\EntityManager\EntityManagerEvent; use App\Core\Event\EntityManager\EntityManagerEvent;
use Doctrine\ORM\EntityManager as DoctrineEntityManager;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@ -15,14 +14,10 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface;
*/ */
class EntityManager class EntityManager
{ {
protected EventDispatcherInterface $eventDispatcher; public function __construct(
protected EventDispatcherInterface $eventDispatcher,
protected DoctrineEntityManager $entityManager; protected EntityManagerInterface $entityManager
) {
public function __construct(EventDispatcherInterface $eventDispatcher, EntityManagerInterface $entityManager)
{
$this->eventDispatcher = $eventDispatcher;
$this->entityManager = $entityManager;
} }
public function create(EntityInterface $entity, bool $dispatchEvent = true, bool $flush = true): self public function create(EntityInterface $entity, bool $dispatchEvent = true, bool $flush = true): self

View file

@ -3,7 +3,7 @@
namespace App\Core; namespace App\Core;
if (!defined('MURPH_VERSION')) { if (!defined('MURPH_VERSION')) {
define('MURPH_VERSION', 'v1.22.0'); define('MURPH_VERSION', 'v1.25.1');
} }
/** /**

View file

@ -2,10 +2,10 @@
namespace App\Core\Notification; namespace App\Core\Notification;
use App\Entity\User;
use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mailer\MailerInterface;
use Twig\Environment as TwigEnvironment; use Twig\Environment as TwigEnvironment;
use App\Entity\User;
/** /**
* class MailNotifier. * class MailNotifier.
@ -14,7 +14,6 @@ use App\Entity\User;
*/ */
class MailNotifier class MailNotifier
{ {
protected MailerInterface $mailer;
protected array $attachments = []; protected array $attachments = [];
protected array $recipients = []; protected array $recipients = [];
protected array $bccRecipients = []; protected array $bccRecipients = [];
@ -22,10 +21,8 @@ class MailNotifier
protected ?string $from = null; protected ?string $from = null;
protected ?string $replyTo = null; protected ?string $replyTo = null;
public function __construct(TwigEnvironment $twig, MailerInterface $mailer) public function __construct(protected MailerInterface $mailer)
{ {
$this->mailer = $mailer;
$this->twig = $twig;
} }
public function setMailer(Swift_Mailer $mailer): self public function setMailer(Swift_Mailer $mailer): self

View file

@ -7,8 +7,8 @@ use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
/** /**
* @method Referer|null find($id, $lockMode = null, $lockVersion = null) * @method null|Referer find($id, $lockMode = null, $lockVersion = null)
* @method Referer|null findOneBy(array $criteria, array $orderBy = null) * @method null|Referer findOneBy(array $criteria, array $orderBy = null)
* @method Referer[] findAll() * @method Referer[] findAll()
* @method Referer[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) * @method Referer[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/ */

View file

@ -3,9 +3,9 @@
namespace App\Core\Repository\Analytic; namespace App\Core\Repository\Analytic;
use App\Core\Repository\Analytic\RefererRepository as Repository; use App\Core\Repository\Analytic\RefererRepository as Repository;
use App\Core\Repository\RepositoryQuery;
use Knp\Component\Pager\PaginatorInterface; use Knp\Component\Pager\PaginatorInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use App\Core\Repository\RepositoryQuery;
class RefererRepositoryQuery extends RepositoryQuery class RefererRepositoryQuery extends RepositoryQuery
{ {

View file

@ -7,8 +7,8 @@ use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
/** /**
* @method View|null find($id, $lockMode = null, $lockVersion = null) * @method null|View find($id, $lockMode = null, $lockVersion = null)
* @method View|null findOneBy(array $criteria, array $orderBy = null) * @method null|View findOneBy(array $criteria, array $orderBy = null)
* @method View[] findAll() * @method View[] findAll()
* @method View[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) * @method View[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/ */

View file

@ -3,9 +3,9 @@
namespace App\Core\Repository\Analytic; namespace App\Core\Repository\Analytic;
use App\Core\Repository\Analytic\ViewRepository as Repository; use App\Core\Repository\Analytic\ViewRepository as Repository;
use App\Core\Repository\RepositoryQuery;
use Knp\Component\Pager\PaginatorInterface; use Knp\Component\Pager\PaginatorInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use App\Core\Repository\RepositoryQuery;
class ViewRepositoryQuery extends RepositoryQuery class ViewRepositoryQuery extends RepositoryQuery
{ {

View file

@ -7,8 +7,8 @@ use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
/** /**
* @method FileInformation|null find($id, $lockMode = null, $lockVersion = null) * @method null|FileInformation find($id, $lockMode = null, $lockVersion = null)
* @method FileInformation|null findOneBy(array $criteria, array $orderBy = null) * @method null|FileInformation findOneBy(array $criteria, array $orderBy = null)
* @method FileInformation[] findAll() * @method FileInformation[] findAll()
* @method FileInformation[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) * @method FileInformation[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/ */

View file

@ -7,8 +7,8 @@ use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
/** /**
* @method NavigationSetting|null find($id, $lockMode = null, $lockVersion = null) * @method null|NavigationSetting find($id, $lockMode = null, $lockVersion = null)
* @method NavigationSetting|null findOneBy(array $criteria, array $orderBy = null) * @method null|NavigationSetting findOneBy(array $criteria, array $orderBy = null)
* @method NavigationSetting[] findAll() * @method NavigationSetting[] findAll()
* @method NavigationSetting[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) * @method NavigationSetting[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/ */

View file

@ -7,8 +7,8 @@ use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
/** /**
* @method Redirect|null find($id, $lockMode = null, $lockVersion = null) * @method null|Redirect find($id, $lockMode = null, $lockVersion = null)
* @method Redirect|null findOneBy(array $criteria, array $orderBy = null) * @method null|Redirect findOneBy(array $criteria, array $orderBy = null)
* @method Redirect[] findAll() * @method Redirect[] findAll()
* @method Redirect[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) * @method Redirect[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/ */

View file

@ -2,9 +2,8 @@
namespace App\Core\Repository; namespace App\Core\Repository;
use App\Core\Repository\RepositoryQuery;
use Knp\Component\Pager\PaginatorInterface;
use App\Core\Repository\RedirectRepository as Repository; use App\Core\Repository\RedirectRepository as Repository;
use Knp\Component\Pager\PaginatorInterface;
class RedirectRepositoryQuery extends RepositoryQuery class RedirectRepositoryQuery extends RepositoryQuery
{ {

View file

@ -98,9 +98,20 @@ abstract class RepositoryQuery
return $this; return $this;
} }
public function count()
{
return $this
->select(sprintf('COUNT(%s.id) as total', $this->id))
->query
->getQuery()
->setMaxResults(1)
->getOneOrNullResult()['total']
;
}
protected function addForcedFilterHandler(string $name): self protected function addForcedFilterHandler(string $name): self
{ {
if (in_array($name, $this->forcedFilterHandlers)) { if (!in_array($name, $this->forcedFilterHandlers)) {
$this->forcedFilterHandlers[] = $name; $this->forcedFilterHandlers[] = $name;
} }
@ -131,15 +142,4 @@ abstract class RepositoryQuery
protected function filterHandler(string $name, $value) protected function filterHandler(string $name, $value)
{ {
} }
public function count()
{
return $this
->select(sprintf('COUNT(%s.id) as total', $this->id))
->query
->getQuery()
->setMaxResults(1)
->getOneOrNullResult()['total']
;
}
} }

View file

@ -7,8 +7,8 @@ use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
/** /**
* @method Setting|null find($id, $lockMode = null, $lockVersion = null) * @method null|Setting find($id, $lockMode = null, $lockVersion = null)
* @method Setting|null findOneBy(array $criteria, array $orderBy = null) * @method null|Setting findOneBy(array $criteria, array $orderBy = null)
* @method Setting[] findAll() * @method Setting[] findAll()
* @method Setting[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) * @method Setting[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/ */

View file

@ -32,9 +32,9 @@ class NodeRepository extends NestedTreeRepository
; ;
} }
return $query->getQuery() return null !== $query->getQuery()
->setMaxResults(1) ->setMaxResults(1)
->getOneOrNullResult() !== null ->getOneOrNullResult()
; ;
} }
} }

View file

@ -12,6 +12,17 @@ $pagination-active-bg: #343a40 !default;
$sidebar-width: 260px !default; $sidebar-width: 260px !default;
$input-border-color: map-get($theme-colors, 'dark-blue');
$input-btn-focus-color: $input-border-color;
$component-active-color: map-get($theme-colors, 'dark-blue');
$nav-tabs-link-active-bg: map-get($theme-colors, 'dark-blue');
$nav-pills-link-active-bg: map-get($theme-colors, 'dark-blue');
$nav-tabs-link-active-color: lighten(map-get($theme-colors, 'dark-blue'), 100%);
$nav-pills-link-active-color: lighten(map-get($theme-colors, 'dark-blue'), 100%);
$input-focus-border-color: lighten(map-get($theme-colors, 'dark-blue'), 80%);
@import "~choices.js/src/styles/choices.scss"; @import "~choices.js/src/styles/choices.scss";
@import "~bootstrap/scss/bootstrap.scss"; @import "~bootstrap/scss/bootstrap.scss";
@import "~@fortawesome/fontawesome-free/css/all.css"; @import "~@fortawesome/fontawesome-free/css/all.css";
@ -19,9 +30,29 @@ $sidebar-width: 260px !default;
@import '~grapesjs/dist/css/grapes.min.css'; @import '~grapesjs/dist/css/grapes.min.css';
@import '~grapesjs-component-code-editor/dist/grapesjs-component-code-editor.min.css'; @import '~grapesjs-component-code-editor/dist/grapesjs-component-code-editor.min.css';
@for $i from 1 through 100 { @for $i from 1 through 400 {
.miw-#{$i*5} { .miw-#{$i}, .min-width-#{$i} {
min-width: $i * 5px; min-width: #{$i}px;
}
.min-width-#{$i}p {
min-width: #{$i}#{"%"};
}
.maw-#{$i}, .max-width-#{$i} {
max-width: #{$i}px;
}
.max-width-#{$i}p {
max-width: #{$i}#{"%"};
}
.width-#{$i} {
width: #{$i}px;
}
.width-#{$i}p {
width: #{$i}#{"%"};
} }
} }
@ -51,6 +82,11 @@ body {
display: block; display: block;
} }
.choices__inner, .is-focused .choices__inner, .is-open .choices__inner {
border: 1px solid map-get($theme-colors, 'dark-blue');
background: #fff;
}
.dropdown-toggle-hide-after { .dropdown-toggle-hide-after {
&::after { &::after {
display: none; display: none;
@ -110,6 +146,7 @@ body {
.table .thead-light { .table .thead-light {
a, th { a, th {
color: map-get($theme-colors, 'dark-blue'); color: map-get($theme-colors, 'dark-blue');
background: lighten(map-get($theme-colors, 'dark-blue'), 80%);
} }
} }
@ -582,7 +619,11 @@ fieldset.form-group {
&-filter { &-filter {
padding-right: 20px; padding-right: 20px;
padding-bottom: 20px; padding-bottom: 15px;
.pagination {
margin-bottom: 0;
}
@media screen and (max-width: 769px) { @media screen and (max-width: 769px) {
padding-right: 10px; padding-right: 10px;
@ -613,8 +654,20 @@ fieldset.form-group {
} }
} }
.table .crud-batch-column { .table {
width: 1%; .crud-batch-column {
width: 1%;
}
.crud-action-column {
text-align: right;
white-space: nowrap;
width: 1px;
}
}
.no-wrap {
white-space: nowrap;
} }
form { form {
@ -672,6 +725,16 @@ form {
} }
} }
label.required::after {
content: '*';
margin-left: 3px;
color: #b41215;
}
.invalid-feedback {
margin-top: -3px;
}
.gjs-editor-cont { .gjs-editor-cont {
border-radius: 10px; border-radius: 10px;
overflow: hidden !important; overflow: hidden !important;
@ -681,11 +744,127 @@ form {
background: map-get($theme-colors, 'dark-blue'); background: map-get($theme-colors, 'dark-blue');
} }
.tox.tox-silver-sink.tox-tinymce-aux { .tox {
z-index: 3000 !important; &.tox-silver-sink.tox-tinymce-aux {
z-index: 3000 !important;
}
&.tox-tinymce {
border-color: $input-border-color;
border-radius: 5px;
}
} }
.field-boolean { .field-boolean {
color: #49555b; color: #49555b;
font-size: 20px; font-size: 20px;
} }
.builder-widget {
.block {
border: 1px solid rgba(map-get($theme-colors, 'dark-blue'), 0.3);
padding: 10px;
border-radius: 4px;
margin-bottom: 10px;
background: rgba(map-get($theme-colors, 'dark-blue'), 0.02);
}
> .block {
border: 1px solid map-get($theme-colors, 'dark-blue');
}
.block-header {
.block-header-item {
font-size: 12px;
display: inline-block;
padding: 2px 6px;
border-radius: 4px;
margin-right: 2px;
cursor: pointer;
}
}
$block-colors: #E183F5 #E3F7C6 #82DDF5 #F5BA82 #A088A6;
$block-colors-length: length($block-colors);
.block .block-icon {
> * {
display: inline-block;
margin-right: 3px;
}
}
@for $i from 1 through 100 {
$block-color-index: ($block-colors-length + $i) % $block-colors-length + 1;
.block-depth-#{$i} {
.block-label {
background: nth($block-colors, $block-color-index);
border: 1px solid darken(nth($block-colors, $block-color-index), 50%);
color: darken(nth($block-colors, $block-color-index), 50%);
}
.builder-add .btn {
background: nth($block-colors, $block-color-index);
border: 1px solid darken(nth($block-colors, $block-color-index), 50%);
color: darken(nth($block-colors, $block-color-index), 50%);
}
}
}
.builder-add {
width: 100%;
&-top {
margin-top: 7px;
}
.btn {
font-size: 12px;
line-height: 14px;
padding: 3px 5px;
}
}
.block-root > .container .builder-add {
margin-top: 0;
}
.block-settings {
padding: 4px;
margin-top: 10px;
margin-bottom: 5px;
.form-control {
margin-top: 0.5rem !important;
}
}
.block .block {
margin-top: 10px;
}
.block-id {
font-size: 12px;
margin-right: 5px;
}
.block-show-dropzone {
.block-dropzone {
min-height: 40px;
}
}
.block-preview {
white-space: nowrap;
max-width: 30%;
text-overflow: ellipsis;
overflow: hidden;
}
dialog {
border: 0;
padding: 0;
background: none;
}
}

View file

@ -1,6 +1,7 @@
import '../../../../../../../../assets/css/admin.scss'; import '../../../../../../../../assets/css/admin.scss';
require('../../../../../../../../node_modules/bootstrap/dist/js/bootstrap.min.js') require('../../../../../../../../node_modules/bootstrap/dist/js/bootstrap.min.js')
require('./modules/sidebar.js')()
require('./modules/table-fixed.js')() require('./modules/table-fixed.js')()
require('./modules/form-confirm.js')() require('./modules/form-confirm.js')()
require('./modules/form-file.js')() require('./modules/form-file.js')()
@ -27,5 +28,5 @@ require('./modules/file-manager.js')()
require('./modules/file-picker.js')() require('./modules/file-picker.js')()
require('./modules/analytics.js')() require('./modules/analytics.js')()
require('./modules/page.js')() require('./modules/page.js')()
require('./modules/sidebar.js')()
require('./modules/node.js')() require('./modules/node.js')()
require('./modules/builder-block.js')()

View file

@ -0,0 +1,295 @@
<template>
<div
class="block block-root"
v-if="Object.keys(widgets).length && value !== null"
>
<div class="container">
<BuilderBlockCreate
:container="value"
:widgets="widgets"
:openedBlocks="openedBlocks"
:allowedWidgets="[]"
v-if="value.length > 0"
position="top"
/>
</div>
<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"
/>
<div class="container">
<div class="d-flex justify-content-between">
<BuilderBlockCreate
:container="value"
:widgets="widgets"
:openedBlocks="openedBlocks"
:allowedWidgets="[]"
position="bottom"
/>
<div>
<button
type="button"
class="btn btn-sm"
v-on:click="openCodeEditor"
>
<i class="fas fa-code"></i>
</button>
</div>
</div>
</div>
<textarea :name="name" class="d-none">{{ toJson(value) }}</textarea>
<dialog ref="dialog" class="modal-dialog modal-dialog-large">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Code editor</h5>
<button
type="button"
class="close"
v-on:click="closeCodeEditor"
>
<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="closeCodeEditor"
>
Close
</button>
<button
type="button"
class="btn btn-primary"
v-on:click="checkAndSaveNextValue"
>
Save
</button>
</div>
</div>
</dialog>
</Draggable>
</div>
</template>
<script>
import BuilderBlockItem from './BuilderBlockItem'
import BuilderBlockCreate from './BuilderBlockCreate'
import Routing from '../../../../../../../../../friendsofsymfony/jsrouting-bundle/Resources/public/js/router.min.js'
import Draggable from 'vuedraggable'
const axios = require('axios').default
const routes = require('../../../../../../../../../../public/js/fos_js_routes.json')
Routing.setRoutingData(routes)
export default {
name: 'BuilderBlock',
props: {
id: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
initialValue: {
type: Array,
required: false,
}
},
data() {
return {
value: null,
nextValue: null,
widgets: {},
openedBlocks: {},
blockKey: 0,
showDragDrop: false,
}
},
methods: {
toJson(value) {
return JSON.stringify(value)
},
triggerBuilderBlockEvent() {
document.querySelector('body').dispatchEvent(new Event('builder_block.update'))
},
openCodeEditor() {
this.nextValue = this.toJson(this.value)
this.$refs.dialog.showModal()
},
closeCodeEditor() {
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.value = this.updateNextValueRecursiveIds(data)
++this.blockKey
}
} catch (e) {
hasError = true
}
return this.$refs.codeEditor.classList.toggle('is-invalid', hasError)
},
removeBlock(key) {
let newValue = []
this.value.forEach((v, k) => {
if (k !== key) {
newValue.push(v)
}
})
this.value = newValue
++this.blockKey
},
dragStart() {
this.showDragDrop = true
},
dragEnd() {
this.showDragDrop = false
++this.blockKey
},
fixSettings(data) {
if (Array.isArray(data)) {
data.forEach((value, key) => {
data[key] = this.fixSettings(value)
})
} 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
},
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: {
BuilderBlockItem,
BuilderBlockCreate,
Draggable,
},
mounted() {
const that = this
axios.get(Routing.generate('admin_editor_builder_block_widgets'))
.then((response) => {
that.widgets = response.data
that.value = that.fixSettings(that.initialValue)
})
},
created() {
this.triggerBuilderBlockEvent()
},
updated() {
this.triggerBuilderBlockEvent()
}
}
</script>

View file

@ -0,0 +1,205 @@
<style scoped>
.builder-block-picker {
padding: 8px;
border: 1px solid #333;
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;
width: 100%;
}
.widget-icon {
margin-right: 3px;
}
.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 {
background: #eee;
border: 1px solid #1e2430;
}
.search {
max-width: 300px;
}
</style>
<template>
<div class="builder-add" :class="{'builder-add-top': position === 'top'}">
<button type="button" class="btn btn-secondary" v-on:click="togglePicker">
<span class="fa fa-plus"></span>
</button>
<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
v-for="(category, key) in categories()"
v-if="Object.keys(category.widgets).length"
class="col-auto builder-block-picker-widgets"
:class="{'d-none': activeCategory !== key}"
>
<div class="row">
<div class="col-12 mb-4">
<input v-model="search" placeholder="Type to search..." class="form-control search">
</div>
<div
v-for="(widget, name) in category.widgets"
v-on:click="add(name, widget)"
v-if="matchSearch(widget.label) || matchSearch(name)"
class="widget col-auto"
>
<span class="widget-icon" v-if="widget.icon" v-html="widget.icon"></span>
{{ widget.label }}
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'BuilderBlockCreate',
props: {
container: {
type: Array,
required: true
},
widgets: {
type: Object,
required: true
},
allowedWidgets: {
type: Array,
required: true
},
openedBlocks: {
type: Object,
required: true
},
position: {
type: String,
required: true
},
},
data() {
return {
showPicker: false,
activeCategory: 'all',
search: '',
}
},
methods: {
add(name, widget) {
let settings = {}
for (let i in widget.settings) {
settings[i] = widget.settings[i].default
}
const block = {
id: this.makeId(),
widget: name,
settings,
children: [],
}
if (this.position === 'bottom') {
this.container.push(block)
} else {
this.container.unshift(block)
}
this.$emit('updateContainer', this.container)
this.openedBlocks[block.id] = true
this.togglePicker()
},
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}`
},
matchSearch(name) {
if (!this.search.trim().length) {
return true
}
return name.toLowerCase().includes(this.search.toLowerCase())
},
togglePicker() {
this.showPicker = !this.showPicker
},
categories() {
let items = {
all: {label: 'All', widgets: {}},
}
for (let widgetName in this.widgets) {
let value = this.widgets[widgetName]
if (!value.category) {
value.category = 'all'
}
if (typeof items[value.category] === 'undefined') {
items[value.category] = {
label: value.category,
widgets: {},
}
}
if (!this.allowedWidgets.length || this.allowedWidgets.includes(widgetName)) {
items[value.category].widgets[widgetName] = value
items['all'].widgets[widgetName] = value
}
}
return items
}
},
}
</script>

View file

@ -0,0 +1,199 @@
<template>
<div
class="block"
:class="'block-depth-' + depth"
v-if="widget"
:key="blockKey"
>
<div class="block-header d-flex justify-content-between">
<div>
<div
class="block-header-item block-label"
:title="item.widget"
>
<span
class="block-icon"
v-if="widget.icon"
v-html="widget.icon"
>
</span>
{{ widget.label }}
</div>
<button
type="button"
class="block-header-item btn btn-sm btn-outline-secondary"
v-on:click="toggleSettings"
v-if="Object.keys(widget.settings).length"
>
<span class="fa fa-cog"></span>
</button>
<button
type="button"
class="block-header-item btn btn-sm btn-outline-secondary dragger"
>
<span class="fa fa-arrows-alt dragger"></span>
</button>
</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 dragger"></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>
<Draggable
v-if="widget.isContainer"
v-model="item.children"
ghost-class="ghost"
group="children"
@start="dragStart"
@end="dragEnd"
:animation="200"
handle=".dragger"
class="block-dropzone"
>
<BuilderBlockItem
v-if="item.children !== null && item.children.length > 0"
v-for="(child, key) in item.children"
:key="child.id"
:item="child"
:widgets="widgets"
:openedBlocks="openedBlocks"
:depth="depth + 1"
@remove-item="removeBlock(key)"
@drag-start="dragStart"
@drag-end="dragEnd"
/>
</Draggable>
<div v-if="widget.isContainer" class="container">
<BuilderBlockCreate
:container="item.children"
:widgets="widgets"
:openedBlocks="openedBlocks"
:allowedWidgets="widget.widgets"
position="bottom"
/>
</div>
</div>
</template>
<script>
import BuilderBlockCreate from './BuilderBlockCreate'
import BuilderBlockSetting from './BuilderBlockSetting'
import Draggable from 'vuedraggable'
export default {
name: 'BuilderBlockItem',
props: {
widgets: {
type: Object,
required: true
},
item: {
type: Object,
required: true
},
openedBlocks: {
type: Object,
required: true
},
depth: {
type: Number,
required: true
}
},
data() {
return {
widget: null,
showSettings: this.openedBlocks[this.item.id] === true,
blockKey: 0,
}
},
methods: {
toggleSettings() {
this.openedBlocks[this.item.id] = !this.openedBlocks[this.item.id]
this.showSettings = !this.showSettings
},
truncate(value) {
return value.replace(/(<([^>]+)>)/ig, '').trim()
},
removeMe() {
this.$emit('remove-item')
},
removeBlock(key) {
let children = []
this.item.children.forEach((v, k) => {
if (k !== key) {
children.push(v)
}
})
this.item.children = children
++this.blockKey
},
dragStart() {
this.$emit('drag-start')
},
dragEnd() {
this.$emit('drag-end')
++this.blockKey
},
},
components: {
BuilderBlockCreate,
BuilderBlockSetting,
Draggable,
},
mounted() {
this.widget = this.widgets[this.item.widget]
},
updated() {
document.querySelector('body').dispatchEvent(new Event('builder_block.update'))
}
}
</script>

View file

@ -0,0 +1,59 @@
<template>
<label class="form-group mb-2">
<span v-if="params.label && params.type !== 'checkbox'" v-text="params.label"></span>
<input
v-if="['number', 'checkbox', 'text'].includes(params.type)"
v-model="item.settings[setting]"
v-bind="params.attr"
:type="params.type"
:class="{'form-control': params.type !== 'checkbox'}"
/>
<span v-if="params.label && params.type == 'checkbox'" v-text="params.label"></span>
<textarea
v-if="params.type == 'textarea'"
v-model="item.settings[setting]"
v-bind="params.attr"
class="form-control"
></textarea>
<select
v-if="params.type == 'select'"
v-model="item.settings[setting]"
v-bind="params.attr"
class="form-control"
>
<option :value="v.value" v-for="(v, k) in params.options" :key="k">
{{ v.text }}
</option>
</select>
</label>
</template>
<script>
export default {
name: 'BuilderBlockSetting',
props: {
item: {
type: Object,
required: true,
},
params: {
type: Object,
required: true,
},
setting: {
type: String,
required: true,
}
},
}
</script>
<style scoped>
label > span {
margin-bottom: 3px;
}
</style>

Some files were not shown because too many files have changed in this diff Show more