Merge branch 'develop' into v2

This commit is contained in:
Simon Vieille 2023-01-25 20:53:13 +01:00
commit 8518b05b8d
112 changed files with 2206 additions and 1177 deletions

View file

@ -1,10 +1,85 @@
## [Unreleased]
## [1.18.0] - 2023-01-13
### Added
* add page maker
* feat(dep): add symfony/runtime
* feat(dep): add symfony/flex
### Fixed
* fix(crud): allow POST in delete actions
* fix(crud): remove default page value in abstract crud controller
* fix(admin): test site_logo before using it
* fix(ui): update z-index of choices__list--dropdown
## [1.17.1] - 2022-12-03
### Fixed
* add mising attribute on timestampable (doctrine)
## [1.17.0] - 2022-11-19
### Fixed
* fix tinymce modal z-index in tox
### Changed
* replace annotation with attributes
## [1.16.0] - 2022-09-06
### Added
* add A/B testing feature
* add cleanup of html string extracted from grapesjs content
### Fixed
* fix file block type
### Changed
* remove dashboard action from the core
## [1.15.0] - 2022-05-09
### Added
* CrudConfiguration::setAction can receive a callable instead of a boolean in 'enabled' param
* add grapesjs-component-code-editor and grapesjs-parser-postcss
* hide the backoffice site name when small resolution
* add entity_to_array twig function
* add default field to show in crud configuration
### Fixed
* fix the mail notifier
* fix sitemap: navigation with several domains
* fix regression with editorjs: content not loaded
### Changed
* change default template to show an entity using `entity_to_array`
## [1.14.1] - 2022-04-30
### Added
* add allowed chars in RouteParameterSlugify and CodeSlugify
* improve sidebar in mobile view
### Fixed
* fix creation of new element when a menu is edited
* fix editorjs error when the textarea is empty
## [1.14.0] - 2022-04-20
### Added
* add grapesjs modes
* add tinymce block type
* add editor types in page maker
* add the page template when the page is generated with the maker
### Changed
* replace flag-icon-css with flag-icons
## [1.13.0] - 2022-04-17
### Added
* add editorjs hyperlink block
* add button to show and hide metas (admin)
* add grapesjs editor
* add editorjs type
### Fixed
* fix editorjs inline tools (bold and italic)
### Changed
* update editorjs quote block template
## [1.12.0] - 2022-03-26
### Added
* add page maker command (`make:page`)
* add CrudConfiguration::getViewData in complement of CrudConfiguration::getViewDatas
* add editorjs link block endpoint
### Fixed
* fix issue with empty user-agent in AnalyticListener
### Changed
* update editorjs image block view
## [1.11.0] - 2022-03-22
### Added

12
SECURITY.md Normal file
View file

@ -0,0 +1,12 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ---------- | --------- |
| >= 2.0 | :x: |
| >= 1.0 | :white_check_mark: |
## Reporting a Vulnerability
If you discover a security vulnerability within Murph, send an email to security [at] murph-project.org.

View file

@ -1,3 +1,14 @@
## General process
Upgrade dependencies:
* `composer update`
* `yarn upgrade`
Build:
* `make build`
## [Unreleased]
## Upgrade to v1.10.0

View file

@ -16,6 +16,7 @@
"doctrine/doctrine-bundle": "^2.5",
"doctrine/doctrine-migrations-bundle": "^3.2",
"doctrine/orm": "^2.11",
"fusonic/opengraph": "^2.2",
"friendsofsymfony/jsrouting-bundle": "^2.8",
"jaybizzle/crawler-detect": "^1.2",
"knplabs/doctrine-behaviors": "^2.6",
@ -30,19 +31,19 @@
"spe/filesize-extension-bundle": "~2.0.0",
"stof/doctrine-extensions-bundle": "^1.7",
"symfony/apache-pack": "^1.0",
"symfony/asset": "6.0.*",
"symfony/console": "6.0.*",
"symfony/dotenv": "6.0.*",
"symfony/event-dispatcher": "6.0.*",
"symfony/expression-language": "6.0.*",
"symfony/finder": "6.0.*",
"symfony/flex": "^1.3.1",
"symfony/form": "6.0.*",
"symfony/framework-bundle": "6.0.*",
"symfony/http-client": "6.0.*",
"symfony/intl": "6.0.*",
"symfony/mailer": "6.0.*",
"symfony/mime": "6.0.*",
"symfony/asset": "6.2.*",
"symfony/console": "6.2.*",
"symfony/dotenv": "6.2.*",
"symfony/event-dispatcher": "6.2.*",
"symfony/expression-language": "6.2.*",
"symfony/finder": "6.2.*",
"symfony/form": "6.2.*",
"symfony/framework-bundle": "6.2.*",
"symfony/http-client": "6.2.*",
"symfony/intl": "6.2.*",
"symfony/mailer": "6.2.*",
"symfony/mime": "6.2.*",
"symfony/flex": "^2.2",
"symfony/monolog-bundle": "^3.1",
"symfony/notifier": "6.0.*",
"symfony/process": "6.0.*",
@ -59,7 +60,8 @@
"symfony/webpack-encore-bundle": "^1.11",
"symfony/yaml": "6.0.*",
"twig/extra-bundle": "^2.12|^3.3",
"twig/twig": "^2.12|^3.3"
"twig/twig": "^2.12|^3.3",
"symfony/runtime": "^5.4"
},
"autoload": {
"psr-4": {

View file

@ -0,0 +1,30 @@
<?php
namespace App\Core\Ab;
/**
* class AbContainer.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class AbContainer implements AbContainerInterface
{
protected array $tests = [];
public function add(AbTestInterface $test): self
{
$this->tests[$test->getName()] = $test;
return $this;
}
public function has(string $name): bool
{
return isset($this->tests[$name]);
}
public function get(string $name): AbTestInterface
{
return $this->tests[$name];
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace App\Core\Ab;
/**
* interface AbContainerInterface.
*
* @author Simon Vieille <simon@deblan.fr>
*/
interface AbContainerInterface
{
public function add(AbTestInterface $test): self;
public function has(string $name): bool;
public function get(string $name): AbTestInterface;
}

125
src/core/Ab/AbTest.php Normal file
View file

@ -0,0 +1,125 @@
<?php
namespace App\Core\Ab;
/**
* class AbTest.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class AbTest implements AbTestInterface
{
protected $results;
protected string $name;
protected array $variations = [];
protected array $probabilities = [];
protected int $duration = 3600 * 24;
public function __construct(string $name)
{
$this->name = $name;
}
public function getName(): string
{
return $this->name;
}
public function getResult()
{
return $this->result;
}
public function setResult(string $result): self
{
$this->result = $result;
return $this;
}
public function isValidVariation($variation): bool
{
return array_key_exists($variation, $this->variations);
}
public function addVariation(string $name, $value, int $probability = null): self
{
$this->variations[$name] = $value;
$this->probabilities[$name] = $probability;
return $this;
}
public function getVariation($variation)
{
return $this->variations[$variation];
}
public function getResultValue()
{
return $this->getVariation($this->getResult());
}
public function setDuration(int $duration): self
{
$this->duration = $duration;
return $this;
}
public function getDuration(): int
{
return $this->duration;
}
public function run(): self
{
$this->result = $this->chance();
return $this;
}
protected function chance(): string
{
$sum = 0;
$empty = 0;
foreach ($this->probabilities as $name => $value) {
$sum += $value;
if (empty($value)) {
++$empty;
}
}
if ($sum > 100) {
throw new \LogicException('Test Error: Total variation probabilities is bigger than 100%');
}
if ($sum < 100) {
foreach ($this->probabilities as $name => $value) {
if (empty($value)) {
$this->probabilities[$name] = (100 - $sum) / $empty;
}
}
}
krsort($this->probabilities);
$number = mt_rand(0, (int) array_sum($this->probabilities) * 10);
$starter = 0;
$return = '';
foreach ($this->probabilities as $key => $val) {
$starter += $val * 10;
if ($number <= $starter) {
$return = $key;
break;
}
}
return $return;
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace App\Core\Ab;
/**
* interface AbTestInterface.
*
* @author Simon Vieille <simon@deblan.fr>
*/
interface AbTestInterface
{
public function getName(): string;
public function getResult();
public function setResult(string $result): self;
public function isValidVariation($variation): bool;
public function addVariation(string $name, $value, int $probability = null): self;
public function getVariation($variation);
public function getResultValue();
public function setDuration(int $duration): self;
public function getDuration(): int;
public function run(): self;
}

View file

@ -2,14 +2,12 @@
namespace App\Core\Annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* class UrlGenerator.
*
* @author Simon Vieille <simon@deblan.fr>
* @Annotation
*/
#[\Attribute]
class UrlGenerator
{
public string $service;
@ -17,4 +15,11 @@ class UrlGenerator
public string $method;
public array $options = [];
public function __construct(string $service, string $method, array $options = [])
{
$this->service = $service;
$this->method = $method;
$this->options = $options;
}
}

View file

@ -14,14 +14,10 @@ use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface;
use ZxcvbnPhp\Zxcvbn;
/**
* @Route("/admin/account")
*/
#[Route(path: '/admin/account')]
class AccountAdminController extends AdminController
{
/**
* @Route("/", name="admin_account")
*/
#[Route(path: '/', name: 'admin_account')]
public function account(Request $request, TotpAuthenticatorInterface $totpAuthenticatorService): Response
{
$account = $this->getUser();
@ -31,9 +27,7 @@ class AccountAdminController extends AdminController
]);
}
/**
* @Route("/2fa", name="admin_account_2fa")
*/
#[Route(path: '/2fa', name: 'admin_account_2fa')]
public function twoFactorAuthentication(
Request $request,
GoogleAuthenticatorInterface $totpAuthenticatorService,
@ -93,9 +87,7 @@ class AccountAdminController extends AdminController
]);
}
/**
* @Route("/password", name="admin_account_password", methods={"POST"})
*/
#[Route(path: '/password', name: 'admin_account_password', methods: ['POST'])]
public function password(
Request $request,
UserRepository $repository,

View file

@ -9,9 +9,7 @@ use Symfony\Component\Routing\Annotation\Route;
abstract class AdminController extends AbstractController
{
/**
* @Route("/_ping", name="_ping")
*/
#[Route(path: '/_ping', name: '_ping')]
public function ping()
{
return $this->json(true);

View file

@ -26,7 +26,7 @@ abstract class CrudController extends AdminController
abstract protected function getConfiguration(): CrudConfiguration;
protected function doIndex(int $page = 1, RepositoryQuery $query, Request $request, Session $session): Response
protected function doIndex(int $page, RepositoryQuery $query, Request $request, Session $session): Response
{
$configuration = $this->getConfiguration();
@ -127,7 +127,7 @@ abstract class CrudController extends AdminController
]);
}
protected function doSort(int $page = 1, RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session): Response
protected function doSort(int $page, RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session): Response
{
$configuration = $this->getConfiguration();
$context = $request->query->get('context', 'index');
@ -165,7 +165,7 @@ abstract class CrudController extends AdminController
return $this->json([]);
}
protected function doBatch(int $page = 1, RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session): Response
protected function doBatch(int $page, RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session): Response
{
$configuration = $this->getConfiguration();
$datas = $request->request->get('batch', []);

View file

@ -8,14 +8,10 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/admin/analytic")
*/
#[Route(path: '/admin/analytic')]
class AnalyticController extends AbstractController
{
/**
* @Route("/stats/{node}/{range}", name="admin_analytic_stats")
*/
#[Route(path: '/stats/{node}/{range}', name: 'admin_analytic_stats')]
public function stats(Node $node, DateRangeAnalytic $analytic, string $range = '7days'): Response
{
if (!in_array($range, ['7days', '30days', '90days', '1year'])) {

View file

@ -25,9 +25,7 @@ class AuthController extends AbstractController
$this->coreParameters = $parameters->get('core');
}
/**
* @Route("/login", name="auth_login")
*/
#[Route(path: '/login', name: 'auth_login')]
public function login(AuthenticationUtils $authenticationUtils): Response
{
if ($this->getUser()) {
@ -45,9 +43,7 @@ class AuthController extends AbstractController
]);
}
/**
* @Route("/resetting/request", name="auth_resetting_request")
*/
#[Route(path: '/resetting/request', name: 'auth_resetting_request')]
public function requestResetting(Request $request, UserRepository $repository, EventDispatcherInterface $eventDispatcher): Response
{
if ($this->getUser()) {
@ -85,9 +81,7 @@ class AuthController extends AbstractController
]);
}
/**
* @Route("/resetting/update/{token}", name="auth_resetting_update")
*/
#[Route(path: '/resetting/update/{token}', name: 'auth_resetting_update')]
public function requestUpdate(
string $token,
Request $request,
@ -145,9 +139,7 @@ class AuthController extends AbstractController
]);
}
/**
* @Route("/logout", name="auth_logout")
*/
#[Route(path: '/logout', name: 'auth_logout')]
public function logout()
{
throw new \Exception('This method can be blank - it will be intercepted by the logout key on your firewall');

View file

@ -6,20 +6,8 @@ use App\Core\Controller\Admin\AdminController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/admin")
*/
class DashboardAdminController extends AdminController
{
/**
* @Route("/", name="admin_dashboard_index")
*/
public function index(): Response
{
return $this->render('@Core/dashboard/index.html.twig', [
]);
}
protected function getSection(): string
{
return 'dashboard';

View file

@ -0,0 +1,48 @@
<?php
namespace App\Core\Controller\Editor;
use Fusonic\OpenGraph\Consumer;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\HttpClient\HttpClientInterface;
#[Route(path: '/admin/editor/editorjs')]
class EditorJsController extends AbstractController
{
#[Route(path: '/fetch_url', name: 'admin_editor_editorjs_fetch_url', options: ['expose' => true])]
public function fetchUrl(Request $request, HttpClientInterface $client): JsonResponse
{
$url = filter_var($request->query->get('url'), FILTER_VALIDATE_URL);
$datas = [];
if (!$url) {
$data['success'] = 0;
} else {
try {
$consumer = new Consumer();
$response = $client->request('GET', $url);
$openGraph = $consumer->loadHtml($response->getContent());
$data = [
'success' => 1,
'link' => $openGraph->url,
'meta' => [
'title' => $openGraph->title,
'description' => $openGraph->description,
],
];
if (isset($openGraph->images[0])) {
$data['meta']['image']['url'] = $openGraph->images[0]->url;
}
} catch (\Exception $e) {
$data['success'] = 0;
}
}
return $this->json($data);
}
}

View file

@ -15,22 +15,16 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @Route("/admin/file_manager")
*/
#[Route(path: '/admin/file_manager')]
class FileManagerAdminController extends AdminController
{
/**
* @Route("/", name="admin_file_manager_index")
*/
#[Route(path: '/', name: 'admin_file_manager_index')]
public function index(): Response
{
return $this->render('@Core/file_manager/index.html.twig');
}
/**
* @Route("/api/directory", name="admin_file_manager_api_directory", options={"expose"=true})
*/
#[Route(path: '/api/directory', name: 'admin_file_manager_api_directory', options: ['expose' => true])]
public function directory(FsFileManager $manager, Request $request): Response
{
$options = [
@ -43,9 +37,7 @@ class FileManagerAdminController extends AdminController
return $this->json($files);
}
/**
* @Route("/info/{tab}/{context}/{ajax}", name="admin_file_manager_info", options={"expose"=true})
*/
#[Route(path: '/info/{tab}/{context}/{ajax}', name: 'admin_file_manager_info', options: ['expose' => true])]
public function info(
FsFileManager $manager,
Request $request,
@ -115,9 +107,7 @@ class FileManagerAdminController extends AdminController
]);
}
/**
* @Route("/directory/new/{ajax}", name="admin_file_manager_directory_new", options={"expose"=true}, methods={"GET", "POST"})
*/
#[Route(path: '/directory/new/{ajax}', name: 'admin_file_manager_directory_new', options: ['expose' => true], methods: ['GET', 'POST'])]
public function directoryNew(FsFileManager $manager, Request $request, TranslatorInterface $translator, bool $ajax = false): Response
{
$splInfo = $manager->getSplInfo($request->query->get('file'));
@ -184,9 +174,7 @@ class FileManagerAdminController extends AdminController
]);
}
/**
* @Route("/directory/rename/{ajax}", name="admin_file_manager_directory_rename", methods={"GET", "POST"})
*/
#[Route(path: '/directory/rename/{ajax}', name: 'admin_file_manager_directory_rename', methods: ['GET', 'POST'])]
public function directoryRename(FsFileManager $manager, Request $request, TranslatorInterface $translator, bool $ajax = false): Response
{
$splInfo = $manager->getSplInfo($request->query->get('file'));
@ -255,9 +243,7 @@ class FileManagerAdminController extends AdminController
]);
}
/**
* @Route("/file/rename/{ajax}", name="admin_file_manager_file_rename", methods={"GET", "POST"})
*/
#[Route(path: '/file/rename/{ajax}', name: 'admin_file_manager_file_rename', methods: ['GET', 'POST'])]
public function fileRename(FsFileManager $manager, Request $request, TranslatorInterface $translator, bool $ajax = false): Response
{
$splInfo = $manager->getSplInfo($request->query->get('file'));
@ -327,9 +313,7 @@ class FileManagerAdminController extends AdminController
]);
}
/**
* @Route("/upload/{ajax}", name="admin_file_manager_upload", options={"expose"=true}, methods={"GET", "POST"})
*/
#[Route(path: '/upload/{ajax}', name: 'admin_file_manager_upload', options: ['expose' => true], methods: ['GET', 'POST'])]
public function upload(FsFileManager $manager, Request $request, TranslatorInterface $translator, bool $ajax = false): Response
{
$splInfo = $manager->getSplInfo($request->query->get('file'));
@ -406,9 +390,7 @@ class FileManagerAdminController extends AdminController
]);
}
/**
* @Route("/delete", name="admin_file_manager_delete", methods={"DELETE"})
*/
#[Route(path: '/delete', name: 'admin_file_manager_delete', methods: ['DELETE', 'POST'])]
public function delete(FsFileManager $manager, Request $request): Response
{
$path = $request->request->get('file');

View file

@ -19,65 +19,49 @@ use Symfony\Component\Routing\Annotation\Route;
class RedirectAdminController extends CrudController
{
/**
* @Route("/admin/redirect/{page}", name="admin_redirect_index", methods={"GET"}, requirements={"page":"\d+"})
*/
#[Route(path: '/admin/redirect/{page}', name: 'admin_redirect_index', methods: ['GET'], requirements: ['page' => '\d+'])]
public function index(RepositoryQuery $query, Request $request, Session $session, int $page = 1): Response
{
return $this->doIndex($page, $query, $request, $session);
}
/**
* @Route("/admin/redirect/new", name="admin_redirect_new", methods={"GET", "POST"})
*/
#[Route(path: '/admin/redirect/new', name: 'admin_redirect_new', methods: ['GET', 'POST'])]
public function new(Factory $factory, EntityManager $entityManager, Request $request): Response
{
return $this->doNew($factory->create(), $entityManager, $request);
}
/**
* @Route("/admin/redirect/show/{entity}", name="admin_redirect_show", methods={"GET"})
*/
#[Route(path: '/admin/redirect/show/{entity}', name: 'admin_redirect_show', methods: ['GET'])]
public function show(Entity $entity): Response
{
return $this->doShow($entity);
}
/**
* @Route("/admin/redirect/filter", name="admin_redirect_filter", methods={"GET"})
*/
#[Route(path: '/admin/redirect/filter', name: 'admin_redirect_filter', methods: ['GET'])]
public function filter(Session $session): Response
{
return $this->doFilter($session);
}
/**
* @Route("/admin/redirect/edit/{entity}", name="admin_redirect_edit", methods={"GET", "POST"})
*/
#[Route(path: '/admin/redirect/edit/{entity}', name: 'admin_redirect_edit', methods: ['GET', 'POST'])]
public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return $this->doEdit($entity, $entityManager, $request);
}
/**
* @Route("/admin/redirect/sort/{page}", name="admin_redirect_sort", methods={"POST"}, requirements={"page":"\d+"})
*/
#[Route(path: '/admin/redirect/sort/{page}', name: 'admin_redirect_sort', methods: ['POST'], requirements: ['page' => '\d+'])]
public function sort(RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session, int $page = 1): Response
{
return $this->doSort($page, $query, $entityManager, $request, $session);
}
/**
* @Route("/admin/redirect/batch/{page}", name="admin_redirect_batch", methods={"POST"}, requirements={"page":"\d+"})
*/
#[Route(path: '/admin/redirect/batch/{page}', name: 'admin_redirect_batch', methods: ['POST'], requirements: ['page' => '\d+'])]
public function batch(RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session, int $page = 1): Response
{
return $this->doBatch($page, $query, $entityManager, $request, $session);
}
/**
* @Route("/admin/redirect/delete/{entity}", name="admin_redirect_delete", methods={"DELETE"})
*/
#[Route(path: '/admin/redirect/delete/{entity}', name: 'admin_redirect_delete', methods: ['DELETE', 'POST'])]
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return $this->doDelete($entity, $entityManager, $request);

View file

@ -11,14 +11,10 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/admin/navigation_setting")
*/
#[Route(path: '/admin/navigation_setting')]
class NavigationSettingAdminController extends AdminController
{
/**
* @Route("/edit/{entity}", name="admin_navigation_setting_edit")
*/
#[Route(path: '/edit/{entity}', name: 'admin_navigation_setting_edit')]
public function edit(
Entity $entity,
EntityManager $entityManager,
@ -58,9 +54,7 @@ class NavigationSettingAdminController extends AdminController
]);
}
/**
* @Route("/delete/{entity}", name="admin_navigation_setting_delete", methods={"DELETE"})
*/
#[Route(path: '/delete/{entity}', name: 'admin_navigation_setting_delete', methods: ['DELETE', 'POST'])]
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
{
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {

View file

@ -12,14 +12,10 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/admin/site/menu")
*/
#[Route(path: '/admin/site/menu')]
class MenuAdminController extends AdminController
{
/**
* @Route("/new/{navigation}", name="admin_site_menu_new", methods={"POST"})
*/
#[Route(path: '/new/{navigation}', name: 'admin_site_menu_new', methods: ['POST'])]
public function new(Navigation $navigation, EntityFactory $factory, EntityManager $entityManager, Request $request): Response
{
$entity = $factory->create($navigation);
@ -39,9 +35,7 @@ class MenuAdminController extends AdminController
]);
}
/**
* @Route("/edit/{entity}", name="admin_site_menu_edit", methods={"POST"})
*/
#[Route(path: '/edit/{entity}', name: 'admin_site_menu_edit', methods: ['POST'])]
public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response
{
$form = $this->createForm(EntityType::class, $entity);
@ -59,9 +53,7 @@ class MenuAdminController extends AdminController
]);
}
/**
* @Route("/delete/{entity}", name="admin_site_menu_delete", methods={"DELETE"})
*/
#[Route(path: '/delete/{entity}', name: 'admin_site_menu_delete', methods: ['DELETE', 'POST'])]
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
{
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {

View file

@ -20,25 +20,19 @@ use Symfony\Component\Routing\Annotation\Route;
class NavigationAdminController extends CrudController
{
/**
* @Route("/admin/site/navigation/{page}", name="admin_site_navigation_index", methods={"GET"}, requirements={"page":"\d+"})
*/
#[Route(path: '/admin/site/navigation/{page}', name: 'admin_site_navigation_index', methods: ['GET'], requirements: ['page' => '\d+'])]
public function index(RepositoryQuery $query, Request $request, Session $session, int $page = 1): Response
{
return $this->doIndex($page, $query, $request, $session);
}
/**
* @Route("/admin/site/navigation/new", name="admin_site_navigation_new", methods={"GET", "POST"})
*/
#[Route(path: '/admin/site/navigation/new', name: 'admin_site_navigation_new', methods: ['GET', 'POST'])]
public function new(Factory $factory, EntityManager $entityManager, Request $request): Response
{
return $this->doNew($factory->create(), $entityManager, $request);
}
/**
* @Route("/admin/site/navigation/show/{entity}", name="admin_site_navigation_show", methods={"GET"})
*/
#[Route(path: '/admin/site/navigation/show/{entity}', name: 'admin_site_navigation_show', methods: ['GET'])]
public function show(
Entity $entity,
EventDispatcherInterface $eventDispatcher,
@ -60,33 +54,25 @@ class NavigationAdminController extends CrudController
return $this->doShow($entity);
}
/**
* @Route("/admin/site/navigation/filter", name="admin_site_navigation_filter", methods={"GET"})
*/
#[Route(path: '/admin/site/navigation/filter', name: 'admin_site_navigation_filter', methods: ['GET'])]
public function filter(Session $session): Response
{
return $this->doFilter($session);
}
/**
* @Route("/admin/site/navigation/edit/{entity}", name="admin_site_navigation_edit", methods={"GET", "POST"})
*/
#[Route(path: '/admin/site/navigation/edit/{entity}', name: 'admin_site_navigation_edit', methods: ['GET', 'POST'])]
public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return $this->doEdit($entity, $entityManager, $request);
}
/**
* @Route("/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
{
return $this->doSort($page, $query, $entityManager, $request, $session);
}
/**
* @Route("/admin/site/navigation/delete/{entity}", name="admin_site_navigation_delete", methods={"DELETE"})
*/
#[Route(path: '/admin/site/navigation/delete/{entity}', name: 'admin_site_navigation_delete', methods: ['DELETE', 'POST'])]
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return $this->doDelete($entity, $entityManager, $request);

View file

@ -24,14 +24,10 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
/**
* @Route("/admin/site/node")
*/
#[Route(path: '/admin/site/node')]
class NodeAdminController extends AbstractController
{
/**
* @Route("/new/{node}", name="admin_site_node_new")
*/
#[Route(path: '/new/{node}', name: 'admin_site_node_new')]
public function new(
Node $node,
EntityFactory $factory,
@ -103,9 +99,7 @@ class NodeAdminController extends AbstractController
]);
}
/**
* @Route("/edit/{entity}/{tab}", name="admin_site_node_edit")
*/
#[Route(path: '/edit/{entity}/{tab}', name: 'admin_site_node_edit')]
public function edit(
Entity $entity,
EntityManager $entityManager,
@ -166,9 +160,7 @@ class NodeAdminController extends AbstractController
]);
}
/**
* @Route("/urls/{entity}", name="admin_site_node_urls")
*/
#[Route(path: '/urls/{entity}', name: 'admin_site_node_urls')]
public function urls(Entity $entity, SitemapBuilder $builder): Response
{
return $this->render('@Core/site/node_admin/urls.html.twig', [
@ -177,9 +169,7 @@ class NodeAdminController extends AbstractController
]);
}
/**
* @Route("/move/{entity}", name="admin_site_node_move")
*/
#[Route(path: '/move/{entity}', name: 'admin_site_node_move')]
public function move(
Entity $entity,
EntityManager $entityManager,
@ -233,9 +223,7 @@ class NodeAdminController extends AbstractController
]);
}
/**
* @Route("/toggle/visibility/{entity}", name="admin_site_node_toggle_visibility", methods={"POST"})
*/
#[Route(path: '/toggle/visibility/{entity}', name: 'admin_site_node_toggle_visibility', methods: ['POST'])]
public function toggleVisibility(Entity $entity, EntityManager $entityManager, Request $request): Response
{
if ($this->isCsrfTokenValid('toggle_visibility'.$entity->getId(), $request->request->get('_token'))) {
@ -251,9 +239,7 @@ class NodeAdminController extends AbstractController
]).sprintf('#node-%d', $entity->getId()));
}
/**
* @Route("/delete/{entity}", name="admin_site_node_delete", methods={"DELETE"})
*/
#[Route(path: '/delete/{entity}', name: 'admin_site_node_delete', methods: ['DELETE', 'POST'])]
public function delete(
Entity $entity,
NodeRepository $nodeRepository,

View file

@ -21,33 +21,25 @@ use App\Core\Entity\EntityInterface;
class PageAdminController extends CrudController
{
/**
* @Route("/admin/site/page/{page}", name="admin_site_page_index", methods={"GET"}, requirements={"page":"\d+"})
*/
#[Route(path: '/admin/site/page/{page}', name: 'admin_site_page_index', methods: ['GET'], requirements: ['page' => '\d+'])]
public function index(RepositoryQuery $query, Request $request, Session $session, int $page = 1): Response
{
return $this->doIndex($page, $query, $request, $session);
}
/**
* @Route("/admin/site/page/show/{entity}", name="admin_site_page_show", methods={"GET"})
*/
#[Route(path: '/admin/site/page/show/{entity}', name: 'admin_site_page_show', methods: ['GET'])]
public function show(Entity $entity): Response
{
return $this->doShow($entity);
}
/**
* @Route("/admin/site/page/filter", name="admin_site_page_filter", methods={"GET"})
*/
#[Route(path: '/admin/site/page/filter', name: 'admin_site_page_filter', methods: ['GET'])]
public function filter(Session $session): Response
{
return $this->doFilter($session);
}
/**
* @Route("/admin/site/page/edit/{entity}", name="admin_site_page_edit", methods={"GET", "POST"})
*/
#[Route(path: '/admin/site/page/edit/{entity}', name: 'admin_site_page_edit', methods: ['GET', 'POST'])]
public function edit(
int $entity,
EntityManager $entityManager,
@ -69,17 +61,13 @@ class PageAdminController extends CrudController
return $this->doEdit($entity, $entityManager, $request);
}
/**
* @Route("/admin/site/page/delete/{entity}", name="admin_site_page_delete", methods={"DELETE"})
*/
#[Route(path: '/admin/site/page/delete/{entity}', name: 'admin_site_page_delete', methods: ['DELETE', 'POST'])]
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return $this->doDelete($entity, $entityManager, $request);
}
/**
* @Route("/admin/site/page/batch/{page}", name="admin_site_page_batch", methods={"POST"}, requirements={"page":"\d+"})
*/
#[Route(path: '/admin/site/page/batch/{page}', name: 'admin_site_page_batch', methods: ['POST'], requirements: ['page' => '\d+'])]
public function batch(RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session, int $page = 1): Response
{
return $this->doBatch($page, $query, $entityManager, $request, $session);
@ -101,6 +89,7 @@ class PageAdminController extends CrudController
->setForm('edit', Type::class, [])
->setForm('filter', FilterType::class)
->setView('form', '@Core/site/page_admin/_form.html.twig')
->setView('edit', '@Core/site/page_admin/edit.html.twig')
->setAction('index', 'new', false)
->setAction('index', 'show', false)

View file

@ -11,22 +11,21 @@ use Symfony\Component\Routing\Annotation\Route;
class SitemapController extends AbstractController
{
/**
* @Route("/sitemap.xml", name="sitemap")
*/
public function sitemap(Request $request, NavigationRepositoryQuery $navigationRepositoryQuery, SitemapBuilder $builder): Response
#[Route(path: '/sitemap.xml', name: 'sitemap')]
public function sitemap(Request $request, NavigationRepositoryQuery $query, SitemapBuilder $builder): Response
{
$navigations = $navigationRepositoryQuery
->whereDomain($request->getHost())
->find()
;
$navigations = $query->create()->find();
$items = [];
foreach ($navigations as $navigation) {
if (!$navigation->matchDomain($request->getHost())) {
continue;
}
$items = array_merge(
$items,
$builder->build($navigation)
$builder->build($navigation, $request->getHost())
);
}

View file

@ -11,14 +11,10 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/admin/site/tree")
*/
#[Route(path: '/admin/site/tree')]
class TreeAdminController extends AdminController
{
/**
* @Route("/", name="admin_site_tree_index")
*/
#[Route(path: '/', name: 'admin_site_tree_index')]
public function index(NavigationRepositoryQuery $navigationQuery, Session $session): Response
{
$navigation = null;
@ -48,9 +44,7 @@ class TreeAdminController extends AdminController
]);
}
/**
* @Route("/navigation/{navigation}", name="admin_site_tree_navigation")
*/
#[Route(path: '/navigation/{navigation}', name: 'admin_site_tree_navigation')]
public function navigation(
Navigation $navigation,
NavigationRepositoryQuery $navigationQuery,

View file

@ -13,14 +13,10 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/admin/task")
*/
#[Route(path: '/admin/task')]
class TaskAdminController extends AdminController
{
/**
* @Route("/", name="admin_task_index")
*/
#[Route(path: '/', name: 'admin_task_index')]
public function index(EventDispatcherInterface $eventDispatcher): Response
{
$event = new TaskInitEvent();
@ -31,9 +27,7 @@ class TaskAdminController extends AdminController
]);
}
/**
* @Route("/run/{task}", name="admin_task_run", methods={"GET"})
*/
#[Route(path: '/run/{task}', name: 'admin_task_run', methods: ['GET'])]
public function run(
string $task,
Request $request,

View file

@ -20,57 +20,43 @@ use App\Core\Security\TokenGenerator;
class UserAdminController extends CrudController
{
/**
* @Route("/admin/user/{page}", name="admin_user_index", methods={"GET"}, requirements={"page":"\d+"})
*/
#[Route(path: '/admin/user/{page}', name: 'admin_user_index', methods: ['GET'], requirements: ['page' => '\d+'])]
public function index(RepositoryQuery $query, Request $request, Session $session, int $page = 1): Response
{
return $this->doIndex($page, $query, $request, $session);
}
/**
* @Route("/admin/user/new", name="admin_user_new", methods={"GET", "POST"})
*/
#[Route(path: '/admin/user/new', name: 'admin_user_new', methods: ['GET', 'POST'])]
public function new(Factory $factory, EntityManager $entityManager, Request $request, TokenGenerator $tokenGenerator): Response
{
return $this->doNew($factory->create(null, $tokenGenerator->generateToken()), $entityManager, $request);
}
/**
* @Route("/admin/user/show/{entity}", name="admin_user_show", methods={"GET"})
*/
#[Route(path: '/admin/user/show/{entity}', name: 'admin_user_show', methods: ['GET'])]
public function show(Entity $entity): Response
{
return $this->doShow($entity);
}
/**
* @Route("/admin/user/filter", name="admin_user_filter", methods={"GET"})
*/
#[Route(path: '/admin/user/filter', name: 'admin_user_filter', methods: ['GET'])]
public function filter(Session $session): Response
{
return $this->doFilter($session);
}
/**
* @Route("/admin/user/edit/{entity}", name="admin_user_edit", methods={"GET", "POST"})
*/
#[Route(path: '/admin/user/edit/{entity}', name: 'admin_user_edit', methods: ['GET', 'POST'])]
public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return $this->doEdit($entity, $entityManager, $request);
}
/**
* @Route("/admin/user/delete/{entity}", name="admin_user_delete", methods={"DELETE"})
*/
#[Route(path: '/admin/user/delete/{entity}', name: 'admin_user_delete', methods: ['DELETE', 'POST'])]
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return $this->doDelete($entity, $entityManager, $request);
}
/**
* @Route("/admin/user/resetting_request/{entity}", name="admin_user_resetting_request", methods={"POST"})
*/
#[Route(path: '/admin/user/resetting_request/{entity}', name: 'admin_user_resetting_request', methods: ['POST'])]
public function requestResetting(Entity $entity, EventDispatcherInterface $eventDispatcher, Request $request): Response
{
if ($this->isCsrfTokenValid('resetting_request'.$entity->getId(), $request->request->get('_token'))) {

View file

@ -2,8 +2,6 @@
namespace App\Core\Crud;
use App\Core\Crud\Exception\CrudConfigurationException;
/**
* class CrudConfiguration.
*
@ -109,7 +107,7 @@ class CrudConfiguration
/* -- */
public function setAction(string $page, string $action, bool $enabled): self
public function setAction(string $page, string $action, bool|callable $enabled): self
{
if (!isset($this->actions[$page])) {
$this->actions[$page] = [];
@ -120,9 +118,20 @@ class CrudConfiguration
return $this;
}
public function getAction(string $page, string $action, bool $default = true)
public function getAction(string $page, string $action, bool $default = true, array $callableParamaters = [])
{
return $this->actions[$page][$action] ?? $default;
if (!isset($this->actions[$page][$action])) {
return $default;
}
if (is_bool($this->actions[$page][$action])) {
return $this->actions[$page][$action];
}
return call_user_func_array(
$this->actions[$page][$action],
$callableParamaters
);
}
public function setBatchAction(string $page, string $action, string $label, callable $callback): self

View file

@ -6,28 +6,20 @@ use Doctrine\ORM\Mapping as ORM;
trait Timestampable
{
/**
* @ORM\Column(name="created_at", type="datetime")
*/
#[ORM\Column(name: 'created_at', type: 'datetime')]
protected $createdAt;
/**
* @ORM\Column(name="updated_at", type="datetime")
*/
#[ORM\Column(name: 'updated_at', type: 'datetime')]
protected $updatedAt;
/**
* @ORM\PrePersist
*/
#[ORM\PrePersist]
public function onPrePersist(): void
{
$this->createdAt = new \DateTime();
$this->updatedAt = new \DateTime();
}
/**
* @ORM\PreUpdate
*/
#[ORM\PreUpdate]
public function onPreUpdate(): void
{
$this->updatedAt = new \DateTime();

View file

@ -7,38 +7,26 @@ use App\Repository\Entity\Analytic\NodeViewRepository;
use Doctrine\ORM\Mapping as ORM;
use App\Core\Entity\EntityInterface;
/**
* @ORM\Entity(repositoryClass=ViewRepository::class)
* @ORM\Table(name="analytic_referer")
*/
#[ORM\Table(name: 'analytic_referer')]
#[ORM\Entity(repositoryClass: ViewRepository::class)]
class Referer implements EntityInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
protected $id;
/**
* @ORM\ManyToOne(targetEntity=Node::class, inversedBy="analyticReferers")
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
*/
#[ORM\ManyToOne(targetEntity: Node::class, inversedBy: 'analyticReferers')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
protected $node;
/**
* @ORM\Column(type="string", length=255)
*/
#[ORM\Column(type: 'string', length: 255)]
protected $uri;
/**
* @ORM\Column(type="integer", options={"default"=0})
*/
#[ORM\Column(type: 'integer', options: ['default' => 0])]
protected $views = 0;
/**
* @ORM\Column(type="date")
*/
#[ORM\Column(type: 'date')]
protected $date;
public function getId(): ?int

View file

@ -7,48 +7,32 @@ use App\Repository\Entity\Analytic\NodeViewRepository;
use Doctrine\ORM\Mapping as ORM;
use App\Core\Entity\EntityInterface;
/**
* @ORM\Entity(repositoryClass=ViewRepository::class)
* @ORM\Table(name="analytic_view")
*/
#[ORM\Table(name: 'analytic_view')]
#[ORM\Entity(repositoryClass: ViewRepository::class)]
class View implements EntityInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
protected $id;
/**
* @ORM\ManyToOne(targetEntity=Node::class, inversedBy="analyticViews")
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
*/
#[ORM\ManyToOne(targetEntity: Node::class, inversedBy: 'analyticViews')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
protected $node;
/**
* @ORM\Column(type="string", length=255)
*/
#[ORM\Column(type: 'string', length: 255)]
protected $path;
/**
* @ORM\Column(type="integer", options={"default"=0})
*/
#[ORM\Column(type: 'integer', options: ['default' => 0])]
protected $views = 0;
/**
* @ORM\Column(type="integer", options={"default"=0})
*/
#[ORM\Column(type: 'integer', options: ['default' => 0])]
protected $desktopViews = 0;
/**
* @ORM\Column(type="integer", options={"default"=0})
*/
#[ORM\Column(type: 'integer', options: ['default' => 0])]
protected $mobileViews = 0;
/**
* @ORM\Column(type="date")
*/
#[ORM\Column(type: 'date')]
protected $date;
public function getId(): ?int

View file

@ -5,21 +5,15 @@ namespace App\Core\Entity;
use App\Repository\Entity\FileInformationRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=FileInformationRepository::class)
*/
#[ORM\Entity(repositoryClass: FileInformationRepository::class)]
class FileInformation implements EntityInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="NONE")
* @ORM\Column(type="string", length=96, unique=true)
*/
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'NONE')]
#[ORM\Column(type: 'string', length: 96, unique: true)]
protected $id;
/**
* @ORM\Column(type="text", nullable=true)
*/
#[ORM\Column(type: 'text', nullable: true)]
protected $attributes;
public function getId(): ?string

View file

@ -6,42 +6,28 @@ use App\Core\Entity\Site\Navigation;
use App\Core\Repository\NavigationSettingRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=NavigationSettingRepository::class)
*/
#[ORM\Entity(repositoryClass: NavigationSettingRepository::class)]
class NavigationSetting implements EntityInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
protected $id;
/**
* @ORM\Column(type="string", length=255)
*/
#[ORM\Column(type: 'string', length: 255)]
protected $section;
/**
* @ORM\Column(type="string", length=255)
*/
#[ORM\Column(type: 'string', length: 255)]
protected $label;
/**
* @ORM\Column(type="string", length=255)
*/
#[ORM\Column(type: 'string', length: 255)]
protected $code;
/**
* @ORM\Column(type="text", nullable=true)
*/
#[ORM\Column(type: 'text', nullable: true)]
protected $value;
/**
* @ORM\ManyToOne(targetEntity=Navigation::class, inversedBy="navigationSettings")
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
*/
#[ORM\ManyToOne(targetEntity: Navigation::class, inversedBy: 'navigationSettings')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
protected $navigation;
public function getId(): ?int

View file

@ -5,71 +5,45 @@ namespace App\Core\Entity;
use App\Core\Repository\RedirectRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=RedirectRepository::class)
*/
#[ORM\Entity(repositoryClass: RedirectRepository::class)]
class Redirect implements EntityInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
protected $id;
/**
* @ORM\Column(type="string", length=5)
*/
#[ORM\Column(type: 'string', length: 5)]
protected $scheme;
/**
* @ORM\Column(type="string", length=255)
*/
#[ORM\Column(type: 'string', length: 255)]
protected $domain;
/**
* @ORM\Column(type="string", length=6)
*/
#[ORM\Column(type: 'string', length: 6)]
protected $domainType;
/**
* @ORM\Column(type="string", length=255)
*/
#[ORM\Column(type: 'string', length: 255)]
protected $rule;
/**
* @ORM\Column(type="string", length=6)
*/
#[ORM\Column(type: 'string', length: 6)]
protected $ruleType;
/**
* @ORM\Column(type="string", length=255)
*/
#[ORM\Column(type: 'string', length: 255)]
protected $location;
/**
* @ORM\Column(type="integer")
*/
#[ORM\Column(type: 'integer')]
protected $redirectCode;
/**
* @ORM\Column(type="string", length=255)
*/
#[ORM\Column(type: 'string', length: 255)]
protected $label;
/**
* @ORM\Column(type="integer", nullable=true)
*/
#[ORM\Column(type: 'integer', nullable: true)]
protected $sortOrder;
/**
* @ORM\Column(type="boolean")
*/
#[ORM\Column(type: 'boolean')]
protected $isEnabled;
/**
* @ORM\Column(type="boolean")
*/
#[ORM\Column(type: 'boolean')]
protected $reuseQueryString;
public function getId(): ?int

View file

@ -5,36 +5,24 @@ namespace App\Core\Entity;
use App\Core\Repository\SettingRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=SettingRepository::class)
*/
#[ORM\Entity(repositoryClass: SettingRepository::class)]
class Setting implements EntityInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
protected $id;
/**
* @ORM\Column(type="string", length=255)
*/
#[ORM\Column(type: 'string', length: 255)]
protected $section;
/**
* @ORM\Column(type="string", length=255)
*/
#[ORM\Column(type: 'string', length: 255)]
protected $label;
/**
* @ORM\Column(type="string", length=255)
*/
#[ORM\Column(type: 'string', length: 255)]
protected $code;
/**
* @ORM\Column(type="text", nullable=true)
*/
#[ORM\Column(type: 'text', nullable: true)]
protected $value;
public function getId(): ?int

View file

@ -9,46 +9,32 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=MenuRepository::class)
* @ORM\HasLifecycleCallbacks
*/
#[ORM\Entity(repositoryClass: MenuRepository::class)]
#[ORM\HasLifecycleCallbacks]
class Menu implements EntityInterface
{
use Timestampable;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
protected $id;
/**
* @ORM\Column(type="string", length=255)
*/
#[ORM\Column(type: 'string', length: 255)]
protected $label;
/**
* @ORM\Column(type="string", length=255)
*/
#[ORM\Column(type: 'string', length: 255)]
protected $code;
/**
* @ORM\ManyToOne(targetEntity=Navigation::class, inversedBy="menus")
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
*/
#[ORM\ManyToOne(targetEntity: Navigation::class, inversedBy: 'menus')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
protected $navigation;
/**
* @ORM\OneToMany(targetEntity=Node::class, mappedBy="menu", orphanRemoval=true, cascade={"remove", "persist"})
*/
#[ORM\OneToMany(targetEntity: Node::class, mappedBy: 'menu', orphanRemoval: true, cascade: ['remove', 'persist'])]
protected $nodes;
/**
* @ORM\OneToOne(targetEntity=Node::class, cascade={"persist"})
* @ORM\JoinColumn(onDelete="CASCADE")
*/
#[ORM\OneToOne(targetEntity: Node::class, cascade: ['persist'])]
#[ORM\JoinColumn(onDelete: 'CASCADE')]
protected $rootNode;
public function __construct()

View file

@ -10,64 +10,42 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=NavigationRepository::class)
* @ORM\HasLifecycleCallbacks
*/
#[ORM\Entity(repositoryClass: NavigationRepository::class)]
#[ORM\HasLifecycleCallbacks]
class Navigation implements EntityInterface
{
use Timestampable;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
protected $id;
/**
* @ORM\Column(type="string", length=255)
*/
#[ORM\Column(type: 'string', length: 255)]
protected $label;
/**
* @ORM\Column(type="string", length=255)
*/
#[ORM\Column(type: 'string', length: 255)]
protected $code;
/**
* @ORM\Column(type="string", length=255)
*/
#[ORM\Column(type: 'string', length: 255)]
protected $domain;
/**
* @ORM\Column(type="boolean", options={"default"=0})
*/
#[ORM\Column(type: 'boolean', options: ['default' => 0])]
protected $forceDomain = false;
/**
* @ORM\Column(type="text", nullable=true)
*/
#[ORM\Column(type: 'text', nullable: true)]
protected $additionalDomains = '[]';
/**
* @ORM\OneToMany(targetEntity=Menu::class, mappedBy="navigation")
*/
#[ORM\OneToMany(targetEntity: Menu::class, mappedBy: 'navigation')]
protected $menus;
/**
* @ORM\Column(type="string", length=10)
*/
#[ORM\Column(type: 'string', length: 10)]
protected $locale = 'en';
/**
* @ORM\Column(type="integer", nullable=true)
*/
#[ORM\Column(type: 'integer', nullable: true)]
protected $sortOrder;
/**
* @ORM\OneToMany(targetEntity=NavigationSetting::class, mappedBy="navigation", orphanRemoval=true)
*/
#[ORM\OneToMany(targetEntity: NavigationSetting::class, mappedBy: 'navigation', orphanRemoval: true)]
protected $navigationSettings;
public function __construct()
@ -240,4 +218,23 @@ class Navigation implements EntityInterface
return $this;
}
public function matchDomain(string $domain): bool
{
if ($domain === $this->getDomain()) {
return true;
}
foreach ($this->getAdditionalDomains() as $additionalDomain) {
if ('domain' === $additionalDomain['type'] && $additionalDomain['domain'] === $domain) {
return true;
}
if ('regexp' === $additionalDomain['type'] && preg_match('#'.$additionalDomain['domain'].'#', $domain) > 0) {
return true;
}
}
return false;
}
}

View file

@ -16,156 +16,120 @@ use function Symfony\Component\String\u;
/**
* @Gedmo\Tree(type="nested")
* @ORM\HasLifecycleCallbacks
* @ORM\Entity(repositoryClass=NodeRepository::class)
*/
#[ORM\HasLifecycleCallbacks]
#[ORM\Entity(repositoryClass: NodeRepository::class)]
class Node implements EntityInterface
{
use Timestampable;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
protected $id;
/**
* @ORM\ManyToOne(targetEntity=Menu::class, inversedBy="nodes", cascade={"persist", "remove"})
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
*/
#[ORM\ManyToOne(targetEntity: Menu::class, inversedBy: 'nodes', cascade: ['persist', 'remove'])]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
protected $menu;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
#[ORM\Column(type: 'string', length: 255, nullable: true)]
protected $label;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
#[ORM\Column(type: 'string', length: 255, nullable: true)]
protected $url;
/**
* @ORM\Column(type="boolean", options={"default"=0})
*/
#[ORM\Column(type: 'boolean', options: ['default' => 0])]
protected $disableUrl = false;
/**
* @ORM\Column(type="boolean", options={"default"=0})
*/
#[ORM\Column(type: 'boolean', options: ['default' => 0])]
protected $isVisible = false;
/**
* @Gedmo\TreeLeft
* @ORM\Column(type="integer")
*/
#[ORM\Column(type: 'integer')]
protected $treeLeft;
/**
* @Gedmo\TreeLevel
* @ORM\Column(type="integer")
*/
#[ORM\Column(type: 'integer')]
protected $treeLevel;
/**
* @Gedmo\TreeRight
* @ORM\Column(type="integer")
*/
#[ORM\Column(type: 'integer')]
protected $treeRight;
/**
* @Gedmo\TreeRoot
* @ORM\ManyToOne(targetEntity="Node")
* @ORM\JoinColumn(referencedColumnName="id", onDelete="CASCADE")
*/
#[ORM\ManyToOne(targetEntity: 'Node')]
#[ORM\JoinColumn(referencedColumnName: 'id', onDelete: 'CASCADE')]
protected $treeRoot;
/**
* @Gedmo\TreeParent
* @ORM\ManyToOne(targetEntity="Node", inversedBy="children")
* @ORM\JoinColumn(referencedColumnName="id", onDelete="CASCADE")
*/
#[ORM\ManyToOne(targetEntity: 'Node', inversedBy: 'children')]
#[ORM\JoinColumn(referencedColumnName: 'id', onDelete: 'CASCADE')]
protected $parent;
/**
* @ORM\OneToMany(targetEntity="Node", mappedBy="parent")
* @ORM\OrderBy({"treeLeft"="ASC"})
*/
#[ORM\OneToMany(targetEntity: 'Node', mappedBy: 'parent')]
#[ORM\OrderBy(['treeLeft' => 'ASC'])]
protected $children;
/**
* @ORM\ManyToOne(targetEntity=Page::class, inversedBy="nodes", cascade={"persist"})
* @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
*/
#[ORM\ManyToOne(targetEntity: Page::class, inversedBy: 'nodes', cascade: ['persist'])]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
protected $page;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
#[ORM\Column(type: 'string', length: 255, nullable: true)]
protected $code;
/**
* @ORM\Column(type="array", nullable=true)
*/
#[ORM\Column(type: 'array', nullable: true)]
protected $parameters = [];
/**
* @ORM\Column(type="array", nullable=true)
*/
#[ORM\Column(type: 'array', nullable: true)]
protected $attributes = [];
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
#[ORM\Column(type: 'string', length: 255, nullable: true)]
protected $controller;
/**
* @ORM\Column(type="array", nullable=true)
*/
#[ORM\Column(type: 'array', nullable: true)]
protected $sitemapParameters = [];
/**
* @ORM\ManyToOne(targetEntity=Node::class, inversedBy="aliasNodes")
* @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
*/
#[ORM\ManyToOne(targetEntity: Node::class, inversedBy: 'aliasNodes')]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
protected $aliasNode;
/**
* @ORM\OneToMany(targetEntity=Node::class, mappedBy="aliasNode")
*/
#[ORM\OneToMany(targetEntity: Node::class, mappedBy: 'aliasNode')]
protected $aliasNodes;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
#[ORM\Column(type: 'string', length: 255, nullable: true)]
protected $contentType;
/**
* @ORM\Column(type="boolean", options={"default"=0})
*/
#[ORM\Column(type: 'boolean', options: ['default' => 0])]
protected $enableAnalytics = false;
/**
* @ORM\OneToMany(targetEntity=View::class, mappedBy="node")
*/
#[ORM\OneToMany(targetEntity: View::class, mappedBy: 'node')]
protected $analyticViews;
/**
* @ORM\OneToMany(targetEntity=Referer::class, mappedBy="node")
*/
#[ORM\OneToMany(targetEntity: Referer::class, mappedBy: 'node')]
protected $analyticReferers;
/**
* @ORM\Column(type="array", nullable=true)
*/
#[ORM\Column(type: 'array', nullable: true)]
private $securityRoles = [];
/**
* @ORM\Column(type="string", length=3, nullable=true)
*/
#[ORM\Column(type: 'string', length: 3, nullable: true)]
private $securityOperator = 'or';
#[ORM\Column(type: 'boolean', options: ['default' => 0])]
private $hasAbTest = false;
#[ORM\Column(type: 'string', length: 255, nullable: true)]
private $abTestCode;
public function __construct()
{
$this->children = new ArrayCollection();
@ -673,4 +637,28 @@ class Node implements EntityInterface
return $this;
}
public function getHasAbTest(): ?bool
{
return $this->hasAbTest;
}
public function setHasAbTest(bool $hasAbTest): self
{
$this->hasAbTest = $hasAbTest;
return $this;
}
public function getAbTestCode(): ?string
{
return $this->abTestCode;
}
public function setAbTestCode(?string $abTestCode): self
{
$this->abTestCode = $abTestCode;
return $this;
}
}

View file

@ -6,37 +6,27 @@ use App\Core\Doctrine\Timestampable;
use App\Core\Repository\Site\Page\BlockRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=BlockRepository::class)
* @ORM\DiscriminatorColumn(name="class_key", type="string")
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\HasLifecycleCallbacks
*/
#[ORM\Entity(repositoryClass: BlockRepository::class)]
#[ORM\DiscriminatorColumn(name: 'class_key', type: 'string')]
#[ORM\InheritanceType('SINGLE_TABLE')]
#[ORM\HasLifecycleCallbacks]
class Block
{
use Timestampable;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
protected $id;
/**
* @ORM\Column(type="string", length=255)
*/
#[ORM\Column(type: 'string', length: 255)]
protected $name;
/**
* @ORM\Column(type="text", nullable=true)
*/
#[ORM\Column(type: 'text', nullable: true)]
protected $value;
/**
* @ORM\ManyToOne(targetEntity=Page::class, inversedBy="blocks")
* @ORM\JoinColumn(onDelete="CASCADE")
*/
#[ORM\ManyToOne(targetEntity: Page::class, inversedBy: 'blocks')]
#[ORM\JoinColumn(onDelete: 'CASCADE')]
protected $page;
public function getId(): ?int

View file

@ -4,9 +4,7 @@ namespace App\Core\Entity\Site\Page;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
#[ORM\Entity]
class ChoiceBlock extends Block
{
public function getValue()

View file

@ -4,9 +4,7 @@ namespace App\Core\Entity\Site\Page;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
#[ORM\Entity]
class CollectionBlock extends Block
{
public function getValue()

View file

@ -6,9 +6,7 @@ use App\Core\File\FileAttribute;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
/**
* @ORM\Entity
*/
#[ORM\Entity]
class FileBlock extends Block
{
public function getValue()

View file

@ -13,66 +13,44 @@ use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\File\File;
use App\Core\File\FileAttribute;
/**
* @ORM\Entity(repositoryClass=PageRepository::class)
* @ORM\DiscriminatorColumn(name="class_key", type="string")
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\HasLifecycleCallbacks
*/
#[ORM\Entity(repositoryClass: PageRepository::class)]
#[ORM\DiscriminatorColumn(name: 'class_key', type: 'string')]
#[ORM\InheritanceType('SINGLE_TABLE')]
#[ORM\HasLifecycleCallbacks]
class Page implements EntityInterface
{
use Timestampable;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
protected $id;
/**
* @ORM\Column(type="string", length=255)
*/
#[ORM\Column(type: 'string', length: 255)]
protected $name;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
#[ORM\Column(type: 'string', length: 255, nullable: true)]
protected $template;
/**
* @ORM\OneToMany(targetEntity=Block::class, mappedBy="page", cascade={"persist"})
*/
#[ORM\OneToMany(targetEntity: Block::class, mappedBy: 'page', cascade: ['persist'])]
protected $blocks;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
#[ORM\Column(type: 'string', length: 255, nullable: true)]
protected $metaTitle;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
#[ORM\Column(type: 'string', length: 255, nullable: true)]
protected $metaDescription;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
#[ORM\Column(type: 'string', length: 255, nullable: true)]
protected $ogTitle;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
#[ORM\Column(type: 'string', length: 255, nullable: true)]
protected $ogDescription;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
#[ORM\Column(type: 'string', length: 255, nullable: true)]
protected $ogImage;
/**
* @ORM\OneToMany(targetEntity=Node::class, mappedBy="page")
*/
#[ORM\OneToMany(targetEntity: Node::class, mappedBy: 'page')]
protected $nodes;
public function __construct()

View file

@ -4,9 +4,7 @@ namespace App\Core\Entity\Site\Page;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
#[ORM\Entity]
class TextBlock extends Block
{
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Core\Event\Ab;
use App\Core\Ab\AbTest;
use Symfony\Contracts\EventDispatcher\Event;
/**
* class AbTestEvent.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class AbTestEvent extends Event
{
public const INIT_EVENT = 'ab_test.init';
public const RUN_EVENT = 'ab_test.run';
protected AbTest $test;
public function __construct(AbTest $test)
{
$this->test = $test;
}
public function getTest(): AbTest
{
return $this->test;
}
}

View file

@ -21,7 +21,7 @@ class PasswordRequestEvent extends Event
$this->user = $user;
}
public function getUser(): USer
public function getUser(): User
{
return $this->user;
}

View file

@ -0,0 +1,109 @@
<?php
namespace App\Core\EventListener;
use App\Core\Ab\AbContainer;
use App\Core\Ab\AbTest;
use App\Core\Entity\Site\Node;
use App\Core\Event\Ab\AbTestEvent;
use App\Core\Repository\Site\NodeRepository;
use App\Core\Site\SiteRequest;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
/**
* class AbListener.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class AbListener
{
protected EventDispatcherInterface $eventDispatcher;
protected AbContainer $container;
protected SiteRequest $siteRequest;
protected ?Node $node;
public function __construct(
AbContainer $container,
EventDispatcherInterface $eventDispatcher,
SiteRequest $siteRequest
) {
$this->eventDispatcher = $eventDispatcher;
$this->container = $container;
$this->siteRequest = $siteRequest;
}
public function onKernelRequest(RequestEvent $event)
{
$this->node = $this->siteRequest->getNode();
if (!$this->supports($event->getRequest())) {
return;
}
$request = $event->getRequest();
$cookieName = md5($this->getCookieName());
$cookieValue = $event->getRequest()->cookies->get($cookieName);
$abTest = new AbTest($this->getAbTestCode());
$event = new AbTestEvent($abTest);
$this->container->add($abTest);
$this->eventDispatcher->dispatch($event, AbTestEvent::INIT_EVENT);
if (!$abTest->isValidVariation($cookieValue)) {
$abTest->run();
$result = $abTest->getResult();
$attributes = array_merge($request->attributes->get('ab_test_cookies', []), [
$cookieName => ['value' => $result, 'duration' => $abTest->getDuration()],
]);
$request->attributes->set('ab_test_cookies', $attributes);
$this->eventDispatcher->dispatch($event, AbTestEvent::RUN_EVENT);
} else {
$abTest->setResult($cookieValue);
}
}
protected function getCookieName(): string
{
return 'ab_test_'.$this->getAbTestCode();
}
protected function getAbTestCode(): string
{
return $this->node->getAbTestCode();
}
protected function supports(Request $request): bool
{
if (!$this->node) {
return false;
}
if (!$this->node->getHasAbTest()) {
return false;
}
if (!$this->node->getAbTestCode()) {
return false;
}
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

@ -0,0 +1,32 @@
<?php
namespace App\Core\EventSubscriber;
use App\Core\Event\Ab\AbTestEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* class AbEventSubscriber.
*
* @author Simon Vieille <simon@deblan.fr>
*/
abstract class AbEventSubscriber implements EventSubscriberInterface
{
protected static int $priority = 0;
public static function getSubscribedEvents()
{
return [
AbTestEvent::INIT_EVENT => ['onInit', self::$priority],
AbTestEvent::RUN_EVENT => ['onRun', self::$priority],
];
}
public function onInit(AbTestEvent $event)
{
}
public function onRun(AbTestEvent $event)
{
}
}

View file

@ -43,14 +43,14 @@ class MenuEventSubscriber extends EntityManagerEventSubscriber
$this->translator = $translator;
}
public function support(EntityInterface $entity)
public function supports(EntityInterface $entity): bool
{
return $entity instanceof Menu;
}
public function onPreUpdate(EntityManagerEvent $event)
{
if (!$this->support($event->getEntity())) {
if (!$this->supports($event->getEntity())) {
return;
}
@ -60,13 +60,13 @@ class MenuEventSubscriber extends EntityManagerEventSubscriber
public function onCreate(EntityManagerEvent $event)
{
if (!$this->support($event->getEntity())) {
if (!$this->supports($event->getEntity())) {
return;
}
$menu = $event->getEntity();
if (count($menu->getNodes()) > 2) {
if (count($menu->getNodes()) >= 2) {
return;
}
@ -84,10 +84,10 @@ class MenuEventSubscriber extends EntityManagerEventSubscriber
$menu->setRootNode($rootNode);
$this->entityManager->getEntityManager()->persist($rootNode);
$this->entityManager->getEntityManager()->persist($childNode);
foreach ([$rootNode, $childNode, $menu] as $entity) {
$this->entityManager->getEntityManager()->persist($entity);
}
$this->entityManager->getEntityManager()->persist($menu);
$this->entityManager->flush();
$this->nodeRepository->persistAsFirstChild($childNode, $rootNode);

View file

@ -24,14 +24,14 @@ class NavigationEventSubscriber extends EntityManagerEventSubscriber
$this->slugify = $slugify;
}
public function support(EntityInterface $entity)
public function supports(EntityInterface $entity): bool
{
return $entity instanceof Navigation;
}
public function onPreUpdate(EntityManagerEvent $event)
{
if (!$this->support($event->getEntity())) {
if (!$this->supports($event->getEntity())) {
return;
}

View file

@ -45,7 +45,7 @@ class NodeEventSubscriber extends EntityManagerEventSubscriber
$this->routeParameterSlugify = $routeParameterSlugify;
}
public function support(EntityInterface $entity)
public function supports(EntityInterface $entity): bool
{
return $entity instanceof Node;
}
@ -57,7 +57,7 @@ class NodeEventSubscriber extends EntityManagerEventSubscriber
public function onPreUpdate(EntityManagerEvent $event)
{
if (!$this->support($event->getEntity())) {
if (!$this->supports($event->getEntity())) {
return;
}
@ -159,7 +159,7 @@ class NodeEventSubscriber extends EntityManagerEventSubscriber
public function onDelete(EntityManagerEvent $event)
{
if (!$this->support($event->getEntity())) {
if (!$this->supports($event->getEntity())) {
return;
}

View file

@ -24,14 +24,14 @@ class BlockEventSubscriber extends EntityManagerEventSubscriber
$this->fileUpload = $fileUpload;
}
public function support(EntityInterface $entity)
public function supports(EntityInterface $entity): bool
{
return $entity instanceof Page;
}
public function onPreUpdate(EntityManagerEvent $event)
{
if (!$this->support($event->getEntity())) {
if (!$this->supports($event->getEntity())) {
return;
}

View file

@ -23,14 +23,14 @@ class PageEventSubscriber extends EntityManagerEventSubscriber
$this->fileUpload = $fileUpload;
}
public function support(EntityInterface $entity)
public function supports(EntityInterface $entity): bool
{
return $entity instanceof Page;
}
public function onPreUpdate(EntityManagerEvent $event)
{
if (!$this->support($event->getEntity())) {
if (!$this->supports($event->getEntity())) {
return;
}

View file

@ -27,14 +27,14 @@ class SiteEventSubscriber extends EntityManagerEventSubscriber
$this->cacheManager = $cacheManager;
}
public function support(EntityInterface $entity)
public function supports(EntityInterface $entity): bool
{
return $entity instanceof Node || $entity instanceof Menu || $entity instanceof Navigation;
}
public function onUpdate(EntityManagerEvent $event)
{
if (!$this->support($event->getEntity())) {
if (!$this->supports($event->getEntity())) {
return;
}

View file

@ -154,6 +154,37 @@ class NodeType extends AbstractType
);
}
$builder->add(
'hasAbTest',
CheckboxType::class,
[
'label' => 'Enable A/B Testing',
'required' => false,
]
);
$builder->add(
'abTestCode',
TextType::class,
[
'label' => 'Code',
'required' => $builder->getData()->getHasAbTest(),
]
);
$builder->add(
'securityOperator',
ChoiceType::class,
[
'label' => 'Condition',
'required' => true,
'choices' => [
'At least one role' => 'or',
'All roles' => 'and',
],
]
);
$actions = [
'New page' => 'new',
'Use an existing page' => 'existing',

View file

@ -0,0 +1,21 @@
<?php
namespace App\Core\Form\Site\Page;
use App\Core\Form\Type\EditorJsTextareaType;
use Symfony\Component\Form\FormBuilderInterface;
class EditorJsTextareaBlockType extends TextareaBlockType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'value',
EditorJsTextareaType::class,
array_merge([
'required' => false,
'label' => false,
], $options['options']),
);
}
}

View file

@ -5,6 +5,8 @@ namespace App\Core\Form\Site\Page;
use App\Core\Entity\Site\Page\FileBlock;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class FileBlockType extends TextBlockType
@ -21,11 +23,21 @@ class FileBlockType extends TextBlockType
);
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars = array_replace($view->vars, [
'file_type' => $options['file_type'],
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => FileBlock::class,
'block_prefix' => 'file_block',
'file_type' => 'auto',
'options' => [],
]);
}

View file

@ -0,0 +1,21 @@
<?php
namespace App\Core\Form\Site\Page;
use App\Core\Form\Type\GrapesJsType;
use Symfony\Component\Form\FormBuilderInterface;
class GrapesJsBlockType extends TextareaBlockType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'value',
GrapesJsType::class,
array_merge([
'required' => false,
'label' => false,
], $options['options']),
);
}
}

View file

@ -4,6 +4,7 @@ namespace App\Core\Form\Site\Page;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Image;
class ImageBlockType extends FileBlockType
@ -22,4 +23,11 @@ class ImageBlockType extends FileBlockType
], $options['options']),
);
}
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefault('is_image', true);
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace App\Core\Form\Site\Page;
use App\Core\Form\Type\TinymceTextareaType;
use Symfony\Component\Form\FormBuilderInterface;
class TinymceTextareaBlockType extends TextareaBlockType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'value',
TinymceTextareaType::class,
array_merge([
'required' => false,
'label' => false,
], $options['options']),
);
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace App\Core\Form\Type;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
class GrapesJsType extends TextareaType
{
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
if (!isset($view->vars['attr']['data-grapesjs'])) {
$view->vars['attr']['data-grapesjs'] = '';
}
return parent::buildView($view, $form, $options);
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'grapesjs';
}
}

View file

@ -13,6 +13,7 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Bundle\MakerBundle\Str;
use Symfony\Component\Filesystem\Filesystem;
class MakePage extends AbstractMaker
{
@ -75,17 +76,31 @@ class MakePage extends AbstractMaker
$generator->writeChanges();
$templatePath = sprintf(
'page/%s/default.html.twig',
Str::asSnakeCase(preg_replace('/Page$/', '', $pageClassNameDetails->getShortName()))
);
$realTemplatePath = 'templates/'.$templatePath;
$filesystem = new Filesystem();
if (!$filesystem->exists($templatePath)) {
$filesystem->mkdir(dirname($realTemplatePath));
$filesystem->dumpFile($realTemplatePath, "{% extends 'base.html.twig' %}\n\n{% block page %}\n\n{% endblock %}\n");
}
$this->writeSuccessMessage($io);
$io->text('Register the page in <comment>config/packages/app.yaml</comment>: ');
$io->text(<<< EOF
core:
site:
pages:
{$pageClassNameDetails->getFullName()}:
name: {$pageClassNameDetails->getShortName()}
templates:
- {name: "Default", file: "path/to/template.html.twig"}
core:
site:
pages:
{$pageClassNameDetails->getFullName()}:
name: {$pageClassNameDetails->getShortName()}
templates:
- {name: "Default", file: "${templatePath}"}
EOF
);
@ -125,9 +140,12 @@ EOF
'textarea' => null,
'choice' => null,
'collection' => 'BlockEntity\\CollectionBlock::class',
'editor_js_textarea' => null,
'file' => 'BlockEntity\\FileBlock::class',
'file_picker' => null,
'grapes_js' => null,
'image' => 'BlockEntity\\FileBlock::class',
'tinymce_textarea' => null,
];
while (null === $type) {

View file

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

View file

@ -5,6 +5,7 @@ namespace App\Core\Notification;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mailer\MailerInterface;
use Twig\Environment as TwigEnvironment;
use App\Entity\User;
/**
* class MailNotifier.
@ -139,19 +140,19 @@ class MailNotifier
return $this;
}
public function addRecipientByAccount(Account $account, bool $isBcc = false): self
public function addRecipientByUser(User $user, bool $isBcc = false): self
{
return $this->addRecipient($account->getEmail(), $isBcc);
return $this->addRecipient($user->getEmail(), $isBcc);
}
public function addRecipientsByAccounts($accounts, bool $isBcc = false)
public function addRecipientsByUsers($users, bool $isBcc = false)
{
if (!is_array($accounts)) {
if (!is_array($users)) {
throw new InvalidArgumentException('The "accounts" parameter must be an array or an instance of ObjectCollection');
}
foreach ($accounts as $account) {
$this->addRecipientByAccount($account, $isBcc);
foreach ($users as $user) {
$this->addRecipientByUser($user, $isBcc);
}
return $this;

View file

@ -17,7 +17,7 @@ abstract class RepositoryQuery
protected QueryBuilder $query;
protected PaginatorInterface $paginator;
protected string $id;
protected array $forcedFilterHandlers;
protected array $forcedFilterHandlers = [];
public function __construct(ServiceEntityRepository $repository, string $id, PaginatorInterface $paginator = null)
{
@ -25,7 +25,6 @@ abstract class RepositoryQuery
$this->query = $repository->createQueryBuilder($id);
$this->paginator = $paginator;
$this->id = $id;
$this->forcedFilterHandlers = [];
}
public function __call(string $name, $params): self

View file

@ -13,565 +13,657 @@ $pagination-active-bg: #343a40 !default;
@import "~choices.js/src/styles/choices.scss";
@import "~bootstrap/scss/bootstrap.scss";
@import "~@fortawesome/fontawesome-free/css/all.css";
@import "~flag-icon-css/sass/flag-icon.scss";
@import "~flag-icon-css/sass/flag-icon.scss";
@import "~flag-icons/sass/flag-icons.scss";
@import '~grapesjs/dist/css/grapes.min.css';
@import '~grapesjs-component-code-editor/dist/grapesjs-component-code-editor.min.css';
@for $i from 1 through 100 {
.miw-#{$i*5} {
min-width: $i * 5px;
}
.miw-#{$i*5} {
min-width: $i * 5px;
}
}
.flag-icon-en {
background-image: url(~flag-icon-css/flags/4x3/gb.svg);
.fi-en {
background-image: url(~flag-icons/flags/4x3/gb.svg);
}
body {
overflow-x: hidden;
overflow-x: hidden;
}
.body.has-form {
min-height: 100vh;
background: lighten(map-get($theme-colors, 'dark-blue'), 75%);
}
#logo {
width: 30px;
width: 30px;
}
.choices__list--dropdown {
display: none;
display: none;
z-index: 3;
}
.choices__list--dropdown.is-active {
display: block;
display: block;
}
.dropdown-toggle-hide-after {
&::after {
display: none;
}
&::after {
display: none;
}
}
.login {
&-container {
margin-top: 5%;
margin-bottom: 5%;
}
&-container {
margin-top: 5%;
margin-bottom: 5%;
}
&-form {
padding: 5%;
}
&-form {
padding: 5%;
}
&-image {
width: 100%;
max-width: 80%;
}
&-image {
width: 100%;
max-width: 80%;
}
}
.sidebar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 100;
padding: 71px 0 0;
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 100;
padding: 71px 0 0;
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
}
.sidebar-sticky {
position: relative;
top: 0;
height: calc(100vh - 71px);
padding-top: .5rem;
overflow-x: hidden;
overflow-y: hidden;
position: relative;
top: 0;
height: calc(100vh - 71px);
padding-top: .5rem;
overflow-x: hidden;
overflow-y: hidden;
&:hover {
overflow-y: auto;
}
&:hover {
overflow-y: auto;
}
}
@supports ((position: -webkit-sticky) or (position: sticky)) {
.sidebar-sticky {
position: -webkit-sticky;
position: sticky;
}
.sidebar-sticky {
position: -webkit-sticky;
position: sticky;
}
}
.actions-container {
padding-right: 25px;
padding-right: 25px;
}
.table .thead-light {
a, th {
color: map-get($theme-colors, 'dark-blue');
}
a, th {
color: map-get($theme-colors, 'dark-blue');
}
}
tr.table-primary-light {
background-color: #ecf5fa;
background-color: #ecf5fa;
}
.td-nowrap {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wrap {
word-wrap: anywhere;
word-wrap: anywhere;
}
.analytic .analytic-referer::before {
content: '';
padding-right: 10px;
content: '';
padding-right: 10px;
}
.analytic[open] .analytic-referer::before {
content: '';
content: '';
}
.analytic-referer {
max-width: calc(100% - 60px);
max-width: calc(100% - 60px);
}
.table tr {
td {
transition: border 500ms ease-out;
border-bottom: 1px solid #dee2e6;
}
td {
transition: border 500ms ease-out;
border-bottom: 1px solid #dee2e6;
}
&:hover {
td {
border-bottom: 1px solid #a8aaac;
}
&:hover {
td {
border-bottom: 1px solid #a8aaac;
}
}
}
.bg-dark-blue {
background: map-get($theme-colors, 'dark-blue');
color: #fff;
background: map-get($theme-colors, 'dark-blue');
color: #fff;
.nav-item-label {
color: #fff;
}
.nav-item-label {
color: #fff;
}
}
.nav-pills {
.nav-item {
margin-right: 3px;
}
.nav-item {
margin-right: 3px;
}
.nav-link:not(.active) {
color: #333;
background: #eee;
}
.nav-link:not(.active) {
color: #333;
background: #eee;
}
}
.sidebar {
width: 260px;
display: inline-block;
width: 260px;
display: inline-block;
.nav-link {
font-weight: 500;
color: #333;
border-left: 4px solid map-get($theme-colors, 'dark-blue');
padding-top: 14px;
padding-bottom: 14px;
.sidebar-toggler {
display: none;
text-align: right;
padding-right: 5px;
.fa {
font-size: 1.2rem;
margin-right: 5px;
color: #fff;
width: 35px;
display: inline-block;
}
.fa {
color: #fff;
}
}
.nav-item-label {
display: inline-block;
}
.nav-link {
font-weight: 500;
color: #333;
border-left: 4px solid map-get($theme-colors, 'dark-blue');
padding-top: 14px;
padding-bottom: 14px;
&.active {
font-weight: bold;
border-left: 4px solid map-get($theme-colors, 'primary');
background: map-get($theme-colors, 'dark-blue');
}
.fa {
font-size: 1.2rem;
margin-right: 5px;
color: #fff;
width: 35px;
display: inline-block;
}
&-heading {
font-size: .75rem;
text-transform: uppercase;
display: flex;
.nav-item-label {
display: inline-block;
}
&.active {
font-weight: bold;
border-left: 4px solid map-get($theme-colors, 'primary');
background: map-get($theme-colors, 'dark-blue');
}
}
&-heading {
font-size: .75rem;
text-transform: uppercase;
display: flex;
}
}
*[data-selectable-selector] {
-moz-user-select: none;
-webkit-user-select: none;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
user-select: none;
}
*[data-selectable-selector] {
&:hover {
cursor: pointer;
}
&:hover {
cursor: pointer;
}
}
*[data-sortable-item] {
&:hover {
cursor: pointer;
}
&:hover {
cursor: pointer;
}
&.sortable-chosen {
background: map-get($theme-colors, 'primary-light');
}
&.sortable-chosen {
background: map-get($theme-colors, 'primary-light');
}
}
.footer {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
z-index: 1000;
height: 35px;
background: #f8f9fa;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
z-index: 1000;
height: 35px;
background: #f8f9fa;
}
.body {
padding-top: 60px;
width: calc(100% - 260px);
margin-left: 260px;
display: inline-block;
padding-top: 60px;
width: calc(100% - 260px);
margin-left: 260px;
display: inline-block;
.nav {
padding-left: 10px;
}
.nav {
padding-left: 10px;
}
}
@media screen and (max-width: 1080px) {
.sidebar-sticky {
overflow-y: auto !important;
}
.sidebar-sticky {
overflow-y: auto !important;
}
}
@media screen and (max-width: 770px) {
.body {
margin-left: 50px;
width: calc(100vw - 50px);
.body {
margin-left: 50px;
width: calc(100vw - 50px);
}
.sidebar {
.sidebar-toggler {
display: block;
}
}
.sidebar:not(.is-open) {
width: 50px;
max-width: 100% !important;
.sidebar-sticky {
width: 50px;
max-width: 100% !important;
width: 50px;
max-width: 100% !important;
}
.sidebar {
width: 50px;
max-width: 100% !important;
.nav {
padding-left: 0;
}
.nav-link {
padding-left: 10px;
.nav-item-label {
display: none;
}
}
.sidebar-heading {
display: none;
}
.nav {
padding-left: 0;
}
.nav-link {
padding-left: 10px;
.nav-item-label {
display: none;
}
}
.sidebar-heading {
display: none;
}
}
}
th {
&.sorted {
&::before {
content: '\f0dc';
font-family: 'FontAwesome';
color: #aaa;
margin-right: 3px;
}
&.sorted {
&::before {
content: '\f0dc';
font-family: 'FontAwesome';
color: #aaa;
margin-right: 3px;
}
}
}
.table-responsive {
max-width: 100%;
overflow-y: hidden;
max-width: 100%;
overflow-y: hidden;
}
.toast-container {
display: flex;
position: relative;
z-index: 4000;
display: flex;
position: relative;
z-index: 4000;
.toast-wrapper {
position: fixed;
top: 20px;
right: 20px;
z-index: 1060;
width: 300px;
}
.toast-wrapper {
position: fixed;
top: 20px;
right: 20px;
z-index: 1060;
width: 300px;
}
}
.bg-tiles {
background-color: #c1c1c1;
background-image: linear-gradient(45deg, #646464 25%, transparent 25%, transparent 75%, #646464 75%), linear-gradient(45deg, #646464 25%, transparent 25%, transparent 75%, #646464 75%);
background-size: 20px 20px;
background-position: 0 0, 10px 10px;
background-color: #c1c1c1;
background-image: linear-gradient(45deg, #646464 25%, transparent 25%, transparent 75%, #646464 75%), linear-gradient(45deg, #646464 25%, transparent 25%, transparent 75%, #646464 75%);
background-size: 20px 20px;
background-position: 0 0, 10px 10px;
}
.tab-form {
padding: 15px;
padding: 15px;
}
.icon-margin {
margin-right: 4px;
margin-right: 4px;
}
.file-icon {
font-size: 2em;
font-size: 2em;
}
.d-ib {
display: inline-block;
display: inline-block;
}
.list-checkbox {
vertical-align: middle;
margin-right: 10px;
margin-top: -2px;
vertical-align: middle;
margin-right: 10px;
margin-top: -2px;
}
.password-strenth {
padding: 0 0 5px 0;
margin-top: -4px;
padding: 0 0 5px 0;
margin-top: -4px;
.col-sm {
height: 8px;
border: 2px solid #fff;
}
.col-sm {
height: 8px;
border: 2px solid #fff;
}
&-info {
font-size: 13px;
height: 22px;
}
&-info {
font-size: 13px;
height: 22px;
}
}
.notification-bell:not([disabled]) {
[data-counter]:after {
display: block;
color: #fff;
background: red;
width: 9px;
height: 9px;
position: absolute;
content: ' ';
top: 4px;
right: 10px;
border-radius: 4px;
}
[data-counter]:after {
display: block;
color: #fff;
background: red;
width: 9px;
height: 9px;
position: absolute;
content: ' ';
top: 4px;
right: 10px;
border-radius: 4px;
}
}
.form-error-icon {
margin-right: 4px;
margin-right: 4px;
}
#form-main {
> .tab-content {
@media screen and (min-width: 500px) {
padding: 30px;
}
background: lighten(map-get($theme-colors, 'dark-blue'), 75%);
.tab-form {
@media screen and (min-width: 500px) {
padding: 30px;
}
background: #fff;
border: 1px solid #ccc;
}
}
}
legend.col-form-label, label {
font-weight: bold;
}
*[data-collection] label, .form-check-label {
font-weight: normal;
}
.custom-file-label::after {
content: "Parcourir";
content: "Parcourir";
}
#lease_template_html {
height: calc(100vh - 270px);
height: calc(100vh - 270px);
}
.panel {
&-toggler {
&:hover {
cursor: pointer;
}
&-toggler {
&:hover {
cursor: pointer;
}
}
&-content {
display: block;
&-content {
display: block;
&:not(.active) {
display: none;
}
&:not(.active) {
display: none;
}
}
}
*[data-collection-delete-container] {
cursor: pointer;
cursor: pointer;
}
*[data-collection-add] {
cursor: pointer;
cursor: pointer;
}
.login-image {
width: 50%;
width: 50%;
}
.tree {
position: relative;
background: white;
color: #212529;
position: relative;
background: white;
color: #212529;
span {
font-style: italic;
letter-spacing: .4px;
color: #a8a8a8;
span {
font-style: italic;
letter-spacing: .4px;
color: #a8a8a8;
}
.fa-folder-open, .fa-folder {
color: #007bff;
}
.fa-html5 {
color: #f21f10;
}
ul {
padding-left: 5px;
list-style: none;
margin: 0;
padding-bottom: 0;
li {
position: relative;
padding-top: 5px;
padding-bottom: 5px;
padding-left: 15px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
&:before {
position: absolute;
top: 15px;
left: 0;
width: 10px;
height: 1px;
margin: auto;
content: '';
background-color: #666;
}
&:after {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 1px;
height: 100%;
content: '';
background-color: #666;
}
&:last-child:after {
height: 15px;
}
}
.fa-folder-open, .fa-folder {
color: #007bff;
}
.fa-html5 {
color: #f21f10;
}
ul {
padding-left: 5px;
list-style: none;
margin: 0;
padding-bottom: 0;
li {
position: relative;
padding-top: 5px;
padding-bottom: 5px;
padding-left: 15px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
&:before {
position: absolute;
top: 15px;
left: 0;
width: 10px;
height: 1px;
margin: auto;
content: '';
background-color: #666;
}
&:after {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 1px;
height: 100%;
content: '';
background-color: #666;
}
&:last-child:after {
height: 15px;
}
}
a {
cursor: pointer;
&:hover {
text-decoration: none;
}
}
a {
cursor: pointer;
&:hover {
text-decoration: none;
}
}
}
}
fieldset.form-group {
margin-bottom: 0;
margin-bottom: 0;
}
.crud-header {
padding: 20px;
.btn-group:empty {
display: none;
}
@media screen and (max-width: 769px) {
padding: 10px;
.btn {
@include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-line-height-sm, $btn-border-radius-sm);
}
}
&-title {
font-size: 2em;
@media screen and (max-width: 769px) {
font-size: 1.5em;
text-align: center;
margin-bottom: 30px;
}
}
&-filter {
padding-right: 20px;
padding-bottom: 20px;
@media screen and (max-width: 769px) {
padding-right: 10px;
padding-bottom: 10px;
}
}
&-actions {
text-align: right;
}
@media screen and (min-width: 770px) {
&-title {
font-size: 2em;
float: left;
font-size: 2em;
}
&-actions {
text-align: right;
margin-bottom: 10px;
float: right;
}
@media screen and (min-width: 770px) {
&-title {
float: left;
font-size: 2em;
}
}
&-actions {
float: right;
}
}
&::after {
display: block;
content: "";
clear: both;
}
&::after {
display: block;
content: "";
clear: both;
}
}
.table .crud-batch-column {
width: 1%;
width: 1%;
}
form {
.loader {
display: none;
}
.loader {
display: none;
}
&.is-loading .loader {
display: inline-block;
}
&.is-loading .loader {
display: inline-block;
}
}
.modal {
z-index: 3000;
z-index: 3000;
}
.modal-dialog-large {
max-width: 80%;
margin-left: auto;
margin-right: auto;
max-width: 80%;
margin-left: auto;
margin-right: auto;
}
.modal.show.blur {
filter: blur(4px);
filter: blur(4px);
}
.output {
&-console {
background: #073642;
line-height: normal;
}
&-console {
background: #073642;
line-height: normal;
}
}
#murph-version {
display: inline;
font-size: 14px;
display: inline;
font-size: 14px;
a {
color: #333;
}
a {
color: #333;
}
}
.editorjs {
@extend .form-control;
@extend .form-control;
height: auto;
padding-left: 50px;
padding-right: 50px;
height: auto;
padding-left: 50px;
padding-right: 50px;
&-block-image {
img {
max-width: 100%;
margin-bottom: 10px;
}
&-block-image {
img {
max-width: 100%;
margin-bottom: 10px;
}
}
}
.gjs-editor-cont {
border-radius: 10px;
overflow: hidden !important;
}
.gjs-one-bg {
background: map-get($theme-colors, 'dark-blue');
}
.tox.tox-silver-sink.tox-tinymce-aux {
z-index: 3000 !important;
}

View file

@ -14,6 +14,7 @@ require('./modules/password.js')()
require('./modules/tooltip.js')()
require('./modules/tinymce.js')()
require('./modules/editorjs.js')()
require('./modules/grapesjs.js')()
require('./modules/panel.js')()
require('./modules/choices.js')()
require('./modules/checkbox-checker.js')()
@ -25,3 +26,6 @@ require('./modules/batch.js')()
require('./modules/file-manager.js')()
require('./modules/file-picker.js')()
require('./modules/analytics.js')()
require('./modules/page.js')()
require('./modules/sidebar.js')()
require('./modules/node.js')()

View file

@ -1,6 +1,6 @@
<template>
<div>
<nav aria-label="breadcrumb" class="d-flex justify-content-between">
<nav aria-label="breadcrumb" class="d-block d-md-flex justify-content-between">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item" v-for="item in breadcrumb">
<a class="btn btn-sm" href="#" v-on:click="setDirectory(item.path)" v-html="item.label"></a>
@ -12,7 +12,7 @@
</li>
</ol>
<div class="d-flex">
<div class="d-block d-md-flex">
<div class="breadcrumb mb-0 file-manager-actions">
<span class="btn btn-sm btn-primary ml-1" v-bind:data-modal="generateUploadLink(directory)" data-modal-create>
<span class="fa fa-upload" v-bind:data-modal="generateUploadLink(directory)" data-modal-create></span>
@ -173,6 +173,12 @@ tr {
background: #e9ecef;
}
@media screen and (max-width: 770px) {
.breadcrumb {
padding: 2px;
}
}
.file-manager-actions .fa {
padding: 3px;
cursor: pointer;

View file

@ -1,10 +1,15 @@
const $ = require('jquery')
const EditorJS = require('@editorjs/editorjs')
const InlineTools = require('editorjs-inline-tool')
const Routing = require('../../../../../../../../friendsofsymfony/jsrouting-bundle/Resources/public/js/router.min.js')
const routes = require('../../../../../../../../../public/js/fos_js_routes.json')
const UnderlineInlineTool = InlineTools.UnderlineInlineTool
const StrongInlineTool = InlineTools.StrongInlineTool
const ItalicInlineTool = InlineTools.ItalicInlineTool
const createGenericInlineTool = require('editorjs-inline-tool/es/tool').default
Routing.setRoutingData(routes)
const tools = {
header: {
@ -39,9 +44,15 @@ const tools = {
class: require('@editorjs/checklist'),
inlineToolbar: true
},
hyperLink: {
class: require('editorjs-hyperlink'),
inlineToolbar: true
},
link: {
class: require('@editorjs/link'),
inlineToolbar: true
config: {
endpoint: Routing.generate('admin_editor_editorjs_fetch_url')
}
},
table: {
class: require('@editorjs/table'),
@ -67,13 +78,32 @@ const tools = {
class: require('@editorjs/underline'),
inlineToolbar: true
},
linkAutocomplete: {
class: require('@editorjs/link-autocomplete'),
inlineToolbar: true
},
image: {
class: require('../components/editorjs/image-tool.js')
}
},
bold: {
class: createGenericInlineTool({
sanitize: {
strong: {}
},
shortcut: 'CMD+B',
tagName: 'STRONG',
toolboxIcon:
'<svg class="icon icon--bold" width="12px" height="14px"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#bold"></use></svg>'
})
},
italic: {
class: createGenericInlineTool({
sanitize: {
em: {}
},
shortcut: 'CMD+I',
tagName: 'EM',
toolboxIcon:
'<svg class="icon icon--italic" width="12px" height="14px"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#italic"></use></svg>'
})
},
underline: UnderlineInlineTool
}
const makeId = () => {
@ -89,10 +119,7 @@ const makeId = () => {
}
const configurationBase = {
tools,
bold: StrongInlineTool,
italic: ItalicInlineTool,
underline: UnderlineInlineTool
tools
}
const buildConfiguration = (conf) => {
@ -120,9 +147,20 @@ const doInitEditor = () => {
editorContainer.attr('id', id)
element.hide()
let data = { time: null, blocks: [] }
try {
const value = JSON.parse(element.val())
if (value.time && Array.isArray(value.blocks)) {
data = value
}
} catch (e) {
}
const editor = new EditorJS(buildConfiguration({
holder: id,
data: JSON.parse(element.val()),
data: data,
onReady: () => {
ready = true
}
@ -143,6 +181,7 @@ const doInitEditor = () => {
const value = JSON.stringify(data)
element.val(value)
} catch (e) {
element.val('[]')
}
})
}, 500)

View file

@ -71,7 +71,7 @@ const FormCollection = () => {
const item = collectionContainer.children('*[data-collection-item]:last-child')
const deleteBtn = $('<span data-collection-delete="__name__" class="fa fa-trash"></span>')
item.find('*[data-collection-delete-container]').first().append(deleteBtn)
item.find('*[data-collection-delete-container]').first().prepend(deleteBtn)
item.html(item.html().replace(/__name__/g, name))
item.attr('data-collection-item', name)

View file

@ -0,0 +1,157 @@
const $ = require('jquery')
const GrapesJs = require('grapesjs')
require('grapesjs-blocks-bootstrap4').default
require('grapesjs-preset-webpage').default
require('grapesjs-preset-newsletter').default
require('grapesjs-plugin-header').default
const makeId = () => {
let result = ''
const characters = 'abcdefghijklmnopqrstuvwxyz0123456789'
const charactersLength = characters.length
for (let i = 0; i < 20; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength))
}
return 'grapesjs-' + result
}
const modes = {
bootstrap4: {
id: 'grapesjs-blocks-bootstrap4',
options: {
blocks: {},
blockCategories: {},
labels: {},
gridDevicesPanel: true,
formPredefinedActions: [
]
},
canvas: {
styles: [
'https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css'
],
scripts: [
'https://code.jquery.com/jquery-3.5.1.slim.min.js',
'https://unpkg.com/@popperjs/core@2',
'https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js'
]
}
},
presetWebpage: {
id: 'gjs-preset-webpage',
options: {
},
canvas: {
}
},
presetNewsletter: {
id: 'gjs-preset-newsletter',
options: {
},
canvas: {
}
}
}
const doInitEditor = () => {
$('textarea[data-grapesjs]').each((i, v) => {
const textarea = $(v)
const element = textarea.parent().prev()
const id = element.attr('id') ? element.attr('id') : makeId()
let mode = textarea.attr('data-grapesjs')
const pluginsOpts = {}
if (!mode || typeof modes[mode] === 'undefined') {
mode = 'bootstrap4'
}
pluginsOpts[modes[mode].id] = modes[mode].options
element.attr('id', id)
const editor = GrapesJs.init({
container: '#' + id,
fromElement: false,
height: '900px',
width: 'auto',
storageManager: {
autoload: false
},
noticeOnUnload: 0,
showOffsets: 1,
showDevices: false,
plugins: [
modes[mode].id,
'grapesjs-plugin-header',
'grapesjs-component-code-editor',
'grapesjs-parser-postcss'
],
colorPicker: {
appendTo: 'parent',
offset: {
top: 26,
left: -166
}
},
pluginsOpts: pluginsOpts,
canvas: modes[mode].canvas
})
const deviceManager = editor.DeviceManager
const devices = [
'Extra Small',
'Small',
'Medium',
'Large',
'Extra Large',
'Desktop',
'Tablet',
'mobileLandscape',
'mobilePortrait'
]
for (const device of devices) {
deviceManager.remove(device)
}
deviceManager.add('All', '100%')
editor.Panels.getPanels().remove('devices-buttons')
editor.Panels.addButton('views', {
id: 'open-code',
attributes: {
title: 'Open Code'
},
className: 'fa fa-edit',
command: 'open-code'
})
editor.on('update', () => {
textarea.val(JSON.stringify(editor.storeData()))
})
try {
editor.loadData(JSON.parse(textarea.val()))
} catch (e) {
editor.loadData({ html: '' })
}
$(element).parents('form').keypress((e) => {
if ($(e.target).parents('#' + id).length === 0) {
return true
}
return (e.keyCode || e.which || e.charCode || 0) !== 13
})
})
}
module.exports = () => {
$(() => {
doInitEditor()
})
}

View file

@ -18,7 +18,7 @@ const openModal = function (url, createModal) {
body.append(container)
}
const loader = $('<div style="position: absolute; top: 25vh; left: 50vw; z-index: 2000">')
const loader = $('<div style="position: absolute; top: 25vh; left: 50vw; z-index: 5000">')
loader.html('<div class="spinner-border text-primary" role="status"><span class="sr-only">Loading...</span></div>')
body.append(loader)
@ -56,7 +56,11 @@ module.exports = function () {
body.on('shown.bs.modal', '.modal', onShownAndHide)
body.on('hidden.bs.modal', '.modal', (e) => {
$(e.target).remove()
const modal = $(e.target)
if (!modal.hasClass('modal-static')) {
modal.remove()
}
if ($('.modal.show').length) {
$('body').addClass('modal-open')

View file

@ -0,0 +1,20 @@
const $ = require('jquery')
const abTestChecker = () => {
$('body').on('change', '#node_hasAbTest', () => {
const checkbox = document.querySelector('#node_hasAbTest')
const code = document.querySelector('#node_abTestCode')
code.parentNode.parentNode.classList.toggle('d-none', !checkbox.checked)
if (checkbox.checked) {
code.setAttribute('required', 'required')
} else {
code.removeAttribute('required')
}
})
}
module.exports = () => {
$(abTestChecker)
}

View file

@ -0,0 +1,41 @@
const $ = require('jquery')
const doExpandCollapse = (stmt) => {
stmt = (stmt == 1)
const button = $('#page-form-expand')
const mainForm = $('#page-main-form')
const metasForm = $('#page-metas-form')
mainForm
.toggleClass('col-md-8', !stmt)
.toggleClass('col-md-12', stmt)
metasForm
.toggleClass('d-none', stmt)
button
.children()
.toggleClass('fa-expand-arrows-alt', !stmt)
.toggleClass('fa-compress-arrows-alt', stmt)
localStorage.setItem('pageFormExpandStmt', stmt ? 1 : null)
}
const initExpander = () => {
const button = $('#page-form-expand')
if (button.length) {
doExpandCollapse(localStorage.getItem('pageFormExpandStmt'))
button.click(() => {
doExpandCollapse(button.children().hasClass('fa-expand-arrows-alt'))
})
}
}
module.exports = () => {
$(() => {
initExpander()
})
}

View file

@ -0,0 +1,9 @@
const $ = require('jquery')
const SidebarOpener = () => {
$('.sidebar-toggler .btn').click(() => {
$('.sidebar').toggleClass('is-open')
})
}
module.exports = SidebarOpener

View file

@ -18,65 +18,51 @@ use Symfony\Component\Routing\Annotation\Route;
class <?= $class_name; ?> extends CrudController
{
/**
* @Route("/admin/<?= $route; ?>/{page}", name="admin_<?= $route; ?>_index", methods={"GET"}, requirements={"page":"\d+"})
*/
#[Route(path: '/admin/user/edit/{entity}', name: 'admin_user_edit', methods: ['GET', 'POST'])]
#[Route(path: "/admin/<?= $route; ?>/{page}", name: "admin_<?= $route; ?>_index", methods: ['GET'], requirements: ['page' => '\d+'])]
public function index(RepositoryQuery $query, Request $request, Session $session, int $page = 1): Response
{
return $this->doIndex($page, $query, $request, $session);
}
/**
* @Route("/admin/<?= $route; ?>/new", name="admin_<?= $route; ?>_new", methods={"GET", "POST"})
*/
#[Route(path: "/admin/<?= $route; ?>/new", name: "admin_<?= $route; ?>_new", methods: ['GET', 'POST'])]
public function new(Factory $factory, EntityManager $entityManager, Request $request): Response
{
return $this->doNew($factory->create(), $entityManager, $request);
}
/**
* @Route("/admin/<?= $route; ?>/show/{entity}", name="admin_<?= $route; ?>_show", methods={"GET"})
*/
#[Route(path: "/admin/<?= $route; ?>/show/{entity}", name: "admin_<?= $route; ?>_show", methods: ['GET'])]
public function show(Entity $entity): Response
{
return $this->doShow($entity);
}
/**
* @Route("/admin/<?= $route; ?>/filter", name="admin_<?= $route; ?>_filter", methods={"GET"})
*/
#[Route(path: "/admin/<?= $route; ?>/filter", name: "admin_<?= $route; ?>_filter", methods: ['GET'])]
public function filter(Session $session): Response
{
return $this->doFilter($session);
}
/**
* @Route("/admin/<?= $route; ?>/edit/{entity}", name="admin_<?= $route; ?>_edit", methods={"GET", "POST"})
*/
#[Route(path: "/admin/<?= $route; ?>/edit/{entity}", name: "admin_<?= $route; ?>_edit", methods: ['GET', 'POST'])]
public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return $this->doEdit($entity, $entityManager, $request);
}
/**
* @Route("/admin/<?= $route; ?>/sort/{page}", name="admin_<?= $route; ?>_sort", methods={"POST"}, requirements={"page":"\d+"})
*/
#[Route(path: "/admin/<?= $route; ?>/sort/{page}", name: "admin_<?= $route; ?>_sort", methods: ['POST'], requirements: ['page' => '\d+'])]
public function sort(RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session, int $page = 1): Response
{
return $this->doSort($page, $query, $entityManager, $request, $session);
}
/**
* @Route("/admin/<?= $route; ?>/batch/{page}", name="admin_<?= $route; ?>_batch", methods={"POST"}, requirements={"page":"\d+"})
*/
#[Route(path: "/admin/<?= $route; ?>/batch/{page}", name: "admin_<?= $route; ?>_batch", methods: ['POST'], requirements: ['page' => '\d+'])]
public function batch(RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session, int $page = 1): Response
{
return $this->doBatch($page, $query, $entityManager, $request, $session);
}
/**
* @Route("/admin/<?= $route; ?>/delete/{entity}", name="admin_<?= $route; ?>_delete", methods={"DELETE"})
*/
#[Route(path: "/admin/<?= $route; ?>/delete/{entity}", name: "admin_<?= $route; ?>_delete", methods: ['DELETE', 'POST'])]
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return $this->doDelete($entity, $entityManager, $request);
@ -120,8 +106,18 @@ class <?= $class_name; ?> extends CrudController
// ->setAction('show', 'back', true)
// ->setAction('show', 'edit', true)
// ->setField('index', 'Label', Field\TextField::class, [
// 'property' => 'label',
->setField('index', 'Entity', Field\TextField::class, [
'property_builder' => function (EntityInterface $entity) {
try {
return (string) $entity;
} catch (\Error $e) {
return $entity->getId();
}
},
])
// ->setField('index', 'Foo', Field\TextField::class, [
// 'property' => 'foo',
// ])
// ->setBatchAction('index', 'delete', 'Delete', function(EntityInterface $entity, EntityManager $manager) {

View file

@ -8,9 +8,7 @@ use App\Core\Form\Site\Page as Block;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @ORM\Entity
*/
#[ORM\Entity]
class <?= $class_name; ?> extends Page
{
public function buildForm(FormBuilderInterface $builder, array $options)

View file

@ -222,3 +222,4 @@
"Security": "Sécurité"
"At least one role": "Au moins un rôle"
"All roles": "Tous les rôles"
"Enable A/B Testing": "Activer le test A/B"

View file

@ -5,7 +5,7 @@
{% block title %}{{ 'My account'|trans }} - {{ parent() }}{% endblock %}
{% block body %}
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
<div class="bg-light">
<div class="crud-header">
<h1 class="crud-header-title">{{ 'My account'|trans }}</h1>
</div>

View file

@ -5,7 +5,7 @@
{% for locale in configuration.locales %}
<li class="nav-item">
<a class="nav-link {% if loop.first %}active{% endif %}" data-toggle="tab" href="#form-locale-{{ loop.index }}">
<span class="flag-icon flag-icon-{{ locale }}"></span>
<span class="fi fi-{{ locale }}"></span>
{{ locale|upper|trans }}
</a>

View file

@ -1 +1,8 @@
<p>{{ '{__toString}'|build_string(entity) }}</p>
<div class="row">
<div class="col-md-12 p-3">
{% for item in entity_to_array(entity) %}
<div class="font-weight-bold">{{ item.name|trans }}</div>
<div class="mb-3">{{ item.value }}</div>
{% endfor %}
</div>
</div>

View file

@ -2,11 +2,13 @@
{% set context = context ?? 'edit' %}
{% block body_class %}has-form{% endblock %}
{% block title %}{{ configuration.pageTitle(context)|trans|build_string(entity) }} - {{ parent() }}{% endblock %}
{% block body %}
{% block header %}
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
<div class="bg-light">
<div class="crud-header">
{% block header_title %}
<h1 class="crud-header-title">{{ configuration.pageTitle(context)|trans|build_string(entity) }}</h1>
@ -20,20 +22,26 @@
{% if configuration.action(context, 'back', true) %}
<a href="{{ path(configuration.pageRoute('index'), configuration.pageRouteParams('index')) }}" class="btn btn-light">
<span class="fa fa-list pr-1"></span>
{{ configuration.actionTitle(context, 'back', 'Back to the list')|trans }}
<span class="d-none d-md-inline">
{{ configuration.actionTitle(context, 'back', 'Back to the list')|trans }}
</span>
</a>
{% endif %}
{% if configuration.action(context, 'show', true) %}
{% if configuration.action(context, 'show', true, [entity]) %}
<a href="{{ path(configuration.pageRoute('show'), {entity: entity.id}|merge(configuration.pageRouteParams('show'))) }}" class="btn btn-secondary">
<span class="fa fa-eye pr-1"></span>
{{ configuration.actionTitle(context, 'show', 'Show')|trans|build_string(entity) }}
<span class="d-none d-md-inline">
{{ configuration.actionTitle(context, 'show', 'Show')|trans|build_string(entity) }}
</span>
</a>
{% endif %}
<button type="submit" form="form-main" class="btn btn-primary">
<span class="fa fa-save pr-1"></span>
{{ configuration.actionTitle(context, 'save', 'Save')|trans|build_string(entity) }}
<span class="d-none d-md-inline">
{{ configuration.actionTitle(context, 'save', 'Save')|trans|build_string(entity) }}
</span>
</button>
{% block header_actions_after %}{% endblock %}
@ -42,7 +50,7 @@
{% set header_actions_dropdown_menu = '' %}
{% endif %}
{% if configuration.action(context, 'delete', true) %}
{% if configuration.action(context, 'delete', true, [entity]) %}
{% set item %}
<button type="submit" form="form-delete" class="dropdown-item">
{{ configuration.actionTitle(context, 'delete', 'Delete')|trans|build_string(entity) }}
@ -86,7 +94,7 @@
</form>
{% endblock %}
{% if configuration.action(context, 'delete', true) %}
{% if configuration.action(context, 'delete', true, [entity]) %}
<form method="post" action="{{ path(configuration.pageRoute('delete'), {entity: entity.id}|merge(configuration.pageRouteParams('delete'))) }}" id="form-delete" data-form-confirm>
<input type="hidden" name="_method" value="DELETE">
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ entity.id) }}">

View file

@ -18,32 +18,34 @@
{% endif %}
{%- endset -%}
<div class="bg-light pl-5 pr-4 pt-5 {{ pb }}">
<div class="bg-light">
<div class="crud-header">
{% block header_title %}
<h1 class="crud-header-title">{{ configuration.pageTitle(context)|trans }}</h1>
{% endblock %}
{% block header_actions %}
{%- block header_actions %}
<div class="crud-header-actions">
<div class="btn-group">
{% block header_actions_before %}{% endblock %}
{%- block header_actions_before %}{% endblock -%}
{% if configuration.action(context, 'new', true) %}
{%- if configuration.action(context, 'new', true) %}
<a href="{{ path(configuration.pageRoute('new'), configuration.pageRouteParams('new')) }}" class="btn btn-primary">
<span class="fa fa-plus pr-1"></span>
{{ configuration.actionTitle(context, 'new', 'New')|trans }}
<span class="d-none d-md-inline">
{{ configuration.actionTitle(context, 'new', 'New')|trans }}
</span>
</a>
{% endif %}
{% endif -%}
{% if header_actions_dropdown_menu is not defined %}
{%- if header_actions_dropdown_menu is not defined %}
{% set header_actions_dropdown_menu = '' %}
{% endif %}
{% endif -%}
{% block header_actions_after %}{% endblock %}
{%- block header_actions_after %}{% endblock -%}
{% block header_actions_dropdown %}
{% if header_actions_dropdown_menu %}
{%- block header_actions_dropdown %}
{%- if header_actions_dropdown_menu %}
<button type="button" class="btn btn-white dropdown-toggle dropdown-toggle-hide-after" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="font-weight-bold">
⋅⋅⋅
@ -53,8 +55,8 @@
<div class="dropdown-menu dropdown-menu-right">
{{ header_actions_dropdown_menu|raw }}
</div>
{% endif %}
{% endblock %}
{% endif -%}
{% endblock -%}
</div>
</div>
{% endblock %}
@ -62,7 +64,7 @@
{% block header_filter_pager %}
{% if filters.show %}
<div class="row pb-3">
<div class="row crud-header-filter">
<div class="col-auto ml-auto {% if pager.getPaginationData.pageCount > 1 %}mr-3{% endif %}">
<button data-modal="{{ path(configuration.pageRoute('filter'), configuration.pageRouteParams('filter')) }}" class="btn btn-sm btn-secondary">
{{ 'Filter'|trans }} {% if not filters.isEmpty %}({{ 'yes'|trans }}){% endif %}
@ -73,7 +75,9 @@
</div>
</div>
{% else %}
{{ knp_pagination_render(pager) }}
<div class="crud-header-filter">
{{ knp_pagination_render(pager) }}
</div>
{% endif %}
{% endblock %}
</div>
@ -174,9 +178,9 @@
{% block list_item %}
{%- set dbClick %}
{% if configuration.action(context, 'show', true) %}
{% if configuration.action(context, 'show', true, [item]) %}
{{ path(configuration.pageRoute('show'), {entity: item.id}|merge(configuration.pageRouteParams('show'))) }}
{% elseif configuration.action(context, 'edit', true) %}
{% elseif configuration.action(context, 'edit', true, [item]) %}
{{ path(configuration.pageRoute('edit'), {entity: item.id}|merge(configuration.pageRouteParams('show'))) }}
{% endif %}
{% endset -%}
@ -211,19 +215,19 @@
<td class="crud-action-column col-2 miw-200 text-right">
{% block list_item_actions_before %}{% endblock %}
{% if configuration.action(context, 'show', true) %}
{% if configuration.action(context, 'show', true, [item]) %}
<a href="{{ path(configuration.pageRoute('show'), {entity: item.id}|merge(configuration.pageRouteParams('show'))) }}" class="btn btn-sm btn-secondary mr-1">
<span class="fa fa-eye"></span>
</a>
{% endif %}
{% if configuration.action(context, 'edit', true) %}
{% if configuration.action(context, 'edit', true, [item]) %}
<a href="{{ path(configuration.pageRoute('edit'), {entity: item.id}|merge(configuration.pageRouteParams('edit'))) }}" class="btn btn-sm btn-primary mr-1">
<span class="fa fa-edit"></span>
</a>
{% endif %}
{% if configuration.action(context, 'delete', true) %}
{% if configuration.action(context, 'delete', true, [item]) %}
<button type="submit" form="form-delete-{{ item.id }}" class="btn btn-sm btn-danger">
<span class="fa fa-trash"></span>
</button>

View file

@ -2,11 +2,13 @@
{% set context = context ?? 'new' %}
{% block body_class %}has-form{% endblock %}
{% block title %}{{ configuration.pageTitle(context)|trans|build_string(entity) }} - {{ parent() }}{% endblock %}
{% block body %}
{% block header %}
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
<div class="bg-light">
<div class="crud-header">
{% block header_title %}
<h1 class="crud-header-title">{{ configuration.pageTitle(context)|trans|build_string(entity) }}</h1>
@ -20,13 +22,17 @@
{% if configuration.action(context, 'back', true) %}
<a href="{{ path(configuration.pageRoute('index'), configuration.pageRouteParams('index')) }}" class="btn btn-light">
<span class="fa fa-list pr-1"></span>
{{ configuration.actionTitle(context, 'back', 'Back to the list')|trans }}
<span class="d-none d-md-inline">
{{ configuration.actionTitle(context, 'back', 'Back to the list')|trans }}
</span>
</a>
{% endif %}
<button type="submit" form="form-main" class="btn btn-primary">
<span class="fa fa-save pr-1"></span>
{{ configuration.actionTitle(context, 'save', 'Save')|trans|build_string(entity) }}
<span class="d-none d-md-inline">
{{ configuration.actionTitle(context, 'save', 'Save')|trans|build_string(entity) }}
</span>
</button>
{% if header_actions_dropdown_menu is not defined %}

View file

@ -6,7 +6,7 @@
{% block body %}
{% block header %}
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
<div class="bg-light">
<div class="crud-header">
{% block header_title %}
<h1 class="crud-header-title">{{ configuration.pageTitle(context)|trans|build_string(entity) }}</h1>
@ -20,15 +20,19 @@
{% if configuration.action(context, 'back', true) %}
<a href="{{ path(configuration.pageRoute('index')) }}" class="btn btn-light">
<span class="fa fa-list pr-1"></span>
{{ configuration.actionTitle(context, 'back', 'Back to the list')|trans }}
<span class="d-none d-md-inline">
{{ configuration.actionTitle(context, 'back', 'Back to the list')|trans }}
</span>
</a>
{% endif %}
{% if configuration.action(context, 'edit', true) %}
{% if configuration.action(context, 'edit', true, [entity]) %}
<a href="{{ path(configuration.pageRoute('edit'), {entity: entity.id}) }}" class="btn btn-primary">
<span class="fa fa-edit pr-1"></span>
{{ configuration.actionTitle(context, 'edit', 'Edit')|trans|build_string(entity) }}
<span class="d-none d-md-inline">
{{ configuration.actionTitle(context, 'edit', 'Edit')|trans|build_string(entity) }}
</span>
</a>
{% endif %}

View file

@ -17,7 +17,7 @@
<a class="navbar-brand" href="{{ path('admin_dashboard_index') }}">
<img id="logo" src="{{ asset(site_logo) }}" alt="{{ site_name }}" title="{{ site_name }}">
{{ site_name }}
<span class="ml-2 d-none d-md-inline">{{ site_name }}</span>
</a>
<div class="ml-auto">
@ -34,11 +34,17 @@
{% block body_container %}
<div class="container-fluid">
<nav class="bg-dark-blue sidebar">
<div class="sidebar-toggler">
<button class="btn">
<span class="fa fa-bars"></span>
</button>
</div>
<div class="sidebar-sticky">
{{ include('@Core/admin/module/menu.html.twig') }}
</div>
</nav>
<div class="body">
<div class="body {% block body_class %}{% endblock %}">
{% block body %}
{% endblock %}
</div>
@ -47,6 +53,8 @@
{% block js %}
<script src="{{ asset('vendor/tinymce/tinymce.min.js') }}"></script>
<script src="{{ asset('vendor/grapesjs-parser-postcss/dist/grapesjs-parser-postcss.min.js') }}"></script>
<script src="{{ asset('vendor/grapesjs-component-code-editor/dist/grapesjs-component-code-editor.min.js') }}"></script>
{{ encore_entry_script_tags('admin') }}
{% endblock %}

View file

@ -1,5 +1,3 @@
{% import "@Core/admin/macros/menu.html.twig" as macros_menu %}
<ul class="nav flex-column">
{{ include('@Core/admin/module/_menu_item.html.twig', {
id: 'dashboard',

View file

@ -1,4 +1,6 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="{{ asset(site_logo) }}">
<link rel="icon" href="{{ asset(site_logo) }}">
{% if site_logo is defined %}
<link rel="shortcut icon" href="{{ asset(site_logo) }}">
<link rel="icon" href="{{ asset(site_logo) }}">
{% endif %}

View file

@ -1,3 +1,3 @@
{%- block render -%}
<img src="{{ source }}" alt="{{ caption }}">
<p><img src="{{ source }}" alt="{{ caption }}"></p>
{%- endblock -%}

View file

@ -0,0 +1,7 @@
{%- block render -%}
<p><a href="{{ link }}">{{- meta.title -}}</a></p>
{%- if meta.description -%}
<p>{{- meta.description -}}</p>
{%- endif -%}
{%- endblock -%}

View file

@ -1,3 +1,3 @@
{%- block render -%}
<p {% block attributes %}{% endblock %}>{{- text -}}</p>
<p {% block attributes %}{% endblock %}>{{- text|raw -}}</p>
{%- endblock -%}

View file

@ -1,6 +1,6 @@
{%- block render -%}
<figure {% block attributes %}{% endblock %}>
{{- text -}}
<blockquote><p>{{- text|nl2br -}}</p></blockquote>
{% if caption %}
<figcaption>

View file

@ -1,13 +1,11 @@
{% extends '@Core/admin/layout.html.twig' %}
{% block body %}
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
<div class="d-flex">
<div class="mr-auto w-50">
<h1 class="display-5">
{{ 'File manager'|trans }}
</h1>
</div>
<div class="bg-light">
<div class="crud-header">
<h1 class="crud-header-title">
{{ 'File manager'|trans }}
</h1>
</div>
</div>

View file

@ -1,12 +1,25 @@
{% extends 'bootstrap_4_layout.html.twig' %}
{% block grapesjs_widget %}
<div class="gjs"></div>
<div class="d-none">
<textarea {{ block('widget_attributes') }}>{{ value }}</textarea>
</div>
{% endblock %}
{% block file_widget -%}
<div class="row">
<div class="col-12">
{% set value = form.vars.data %}
{% if form.parent.vars.file_type is defined %}
{% set fileType = form.parent.vars.file_type %}
{% else %}
{% set fileType = 'auto' %}
{% endif %}
{% if value %}
{% if value and value.extension in ['jpeg', 'jpg', 'gif', 'png', 'svg'] %}
{% if fileType in ['auto', 'image'] and value.extension in ['jpeg', 'jpg', 'gif', 'png', 'svg'] %}
<div class="card">
<div class="card-img-top bg-tiles text-center">
<a href="{{ asset(value.pathname) }}" target="_blank">
@ -48,6 +61,7 @@
<div class="text-right">
<span data-collection-delete-container class="btn btn-sm btn-danger">
<span data-collection-delete="{{ loop.index }}">
<span data-collection-delete="{{ loop.index }}" class="fa fa-trash"></span>
{{ label_delete|trans }}
</span>
</span>
@ -58,6 +72,7 @@
<div data-collection-add="collection-{{ collection_name }}" class="collection-add">
<span class="btn btn-sm btn-primary" data-collection-add="collection-{{ collection_name }}">
<span data-collection-add="collection-{{ collection_name }}" class="fa fa-plus"></span>
{{ label_add|trans }}
</span>
</div>
@ -76,9 +91,11 @@
{% endblock %}
{% block murph_collection_widget %}
<div data-collection="collection-{{ collection_name }}">
{% set attrs = attr|merge({class: 'mb-1 ' ~ (attr.class ?? '')}) %}
<div data-collection="collection-{{ collection_name }}" {% for attr, value in row_attr %}{{ attr }}="{{ value }}"{% endfor %}>
{% for item in form %}
<div data-collection-item="{{ loop.index }}" class="mb-1">
<div data-collection-item="{{ loop.index }}" {% for attr, value in attrs %}{{ attr }}="{{ value }}"{% endfor %}>
{% for child in item %}
{{ form_row(child) }}
{% endfor %}
@ -86,6 +103,7 @@
<div class="text-right">
<span data-collection-delete-container class="btn btn-sm btn-danger">
<span data-collection-delete="{{ loop.index }}">
<span data-collection-delete="{{ loop.index }}" class="fa fa-trash"></span>
{{ label_delete|trans }}
</span>
</span>
@ -96,12 +114,13 @@
<div data-collection-add="collection-{{ collection_name }}" class="collection-add">
<span class="btn btn-sm btn-primary" data-collection-add="collection-{{ collection_name }}">
<span data-collection-add="collection-{{ collection_name }}" class="fa fa-plus"></span>
{{ label_add|trans }}
</span>
</div>
<template type="text/template" id="collection-{{ collection_name }}">
<div data-collection-item="__name__">
<div data-collection-item="__name__" {% for attr, value in attrs %}{{ attr }}="{{ value }}"{% endfor %}>
{{ form_rest(form.vars.prototype) }}
<div class="text-right">

View file

@ -3,7 +3,7 @@
{% set message %}
{% trans %}A password reset request has been made. If you are the source of this request, click on the link below or copy and paste the address if the link does not work.{% endtrans %}
<a href="{{ reseting_update_link }}">{{ reseting_update_link }}</a>
<br><a href="{{ reseting_update_link }}">{{ reseting_update_link }}</a>
{% endset %}
{% block body %}

View file

@ -7,42 +7,45 @@
<meta name="format-detection" content="telephone=no" />
<title>{% block title %}{% endblock %}</title>
{% block css %}
<style>
html, body {
margin: 0;
padding: 0;
background: #fff;
}
#header {
padding: 20px 10px;
background: #f8f9fa;
}
#content {
padding: 10px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
#logo {
width: 230px;
}
#logo svg {
width: 230px;
}
a {
color: #3183aa;
}
</style>
{% endblock %}
</head>
<body>
<div id="content">
{% block body %}{% endblock %}
</div>
<body style="box-sizing: border-box; margin: 0;">
<table class="main-body" style="box-sizing: border-box; padding-top: 5px; padding-right: 5px; padding-bottom: 5px; padding-left: 5px; width: 100%; height: 100%; background-color: rgb(234, 236, 237);" width="100%" height="100%" bgcolor="rgb(234, 236, 237)">
<tbody style="box-sizing: border-box;">
<tr class="row" style="box-sizing: border-box; vertical-align: top;" valign="top">
<td class="main-body-cell" style="box-sizing: border-box;">
<table class="container" style="box-sizing: border-box; font-family: Helvetica, serif; padding-top: 5px; padding-right: 5px; padding-bottom: 5px; padding-left: 5px; margin-top: auto; margin-right: auto; margin-bottom: auto; margin-left: auto; height: 0px; width: 90%; max-width: 550px;" width="90%" height="0">
<tbody style="box-sizing: border-box;">
<tr style="box-sizing: border-box;">
<td class="container-cell" style="box-sizing: border-box; vertical-align: top; font-size: medium; padding-bottom: 50px;" valign="top">
<table class="card" style="box-sizing: border-box; padding-top: 5px; padding-right: 5px; padding-bottom: 5px; padding-left: 5px; margin-bottom: 20px; height: 0px;" height="0" width="100%">
<tbody style="box-sizing: border-box;">
<tr style="box-sizing: border-box;">
<td class="card-cell" style="box-sizing: border-box; background-color: rgb(255, 255, 255); overflow-x: hidden; overflow-y: hidden; border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px;" bgcolor="rgb(255, 255, 255)" align="center">
<table class="table100 c1357" style="box-sizing: border-box; width: 100%; padding-top: 5px; padding-right: 5px; padding-bottom: 5px; padding-left: 5px; height: 0px; margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; border-collapse: collapse;" width="100%" height="0" width="100%">
<tbody id="if8tm" style="box-sizing: border-box;">
<tr id="iu0an" style="box-sizing: border-box;">
<td class="card-content" id="i9wt9" style="box-sizing: border-box; font-size: 13px; line-height: 20px; color: rgb(111, 119, 125); padding-top: 10px; padding-right: 20px; padding-bottom: 0px; padding-left: 20px; vertical-align: top;" valign="top">
<div class="card-text" style="box-sizing: border-box;">
{% block body %}{% endblock %}
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</body>
</html>

View file

@ -1,4 +1,4 @@
<div class="btn-group">
<div class="btn-group mb-1">
<span class="btn btn-sm btn-dark border-secondary disabled">
{%- if entity.scheme == 'all' -%}
http(s)
@ -13,9 +13,7 @@
{{ entity.rule }}
</span>
</div>
<br>
<div class="btn-group">
<span class="btn btn-sm btn-warning border-secondary disabled">
{{ entity.location }}

View file

@ -3,7 +3,7 @@
{% block title %}{{ 'Settings'|trans }} - {{ parent() }}{% endblock %}
{% block body %}
<div class="bg-light pl-5 pr-4 pt-5 {% if pager.paginationData.pageCount < 2 %}pb-5{% endif %}">
<div class="bg-light">
<div class="crud-header">
<h1 class="crud-header-title">{{ 'Settings'|trans }}</h1>
</div>

View file

@ -43,6 +43,7 @@
</div>
<div data-collection-add="collection-{{ collection_name }}" class="collection-add col-12">
<span class="btn btn-sm btn-primary" data-collection-add="collection-{{ collection_name }}">
<span data-collection-add="collection-{{ collection_name }}" class="fa fa-plus"></span>
{{ label_add|trans }}
</span>
</div>

View file

@ -3,7 +3,9 @@
{% block header_actions_before %}
<a href="{{ path(configuration.pageRoute('redirects'), configuration.pageRouteParams('redirects')) }}" class="btn btn-light">
<span class="fa fa-map-signs pr-1"></span>
{{ configuration.actionTitle(context, 'redirects', 'Redirects')|trans }}
<span class="d-none d-md-inline">
{{ configuration.actionTitle(context, 'redirects', 'Redirects')|trans }}
</span>
</a>
{% endblock %}

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