add node ang page edition

This commit is contained in:
Simon Vieille 2021-03-19 12:10:52 +01:00
parent 023e6aec06
commit 237b105289
23 changed files with 583 additions and 25 deletions

View file

@ -17,4 +17,5 @@ return [
Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['all' => true],
Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true],
Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true],
App\Bundle\AppBundle::class => ['all' => true],
];

8
config/packages/app.yaml Normal file
View file

@ -0,0 +1,8 @@
app:
site:
pages:
App\Site\Page\SimplePage:
name: 'Page simple'
templates:
- {name: "Template 1", file: "site/page/simple/page.html.twig"}
- {name: "Template 2", file: "site/page/simple/page2.html.twig"}

View file

@ -16,11 +16,11 @@ doctrine:
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App\Entity
App\Page:
App\Site\Page:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Page'
prefix: 'App\Page'
dir: '%kernel.project_dir%/src/Site/Page'
prefix: 'App\Site\Page'
gedmo_tree:
type: annotation
prefix: Gedmo\Tree\Entity

24
src/Bundle/AppBundle.php Normal file
View file

@ -0,0 +1,24 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use App\DependencyInjection\AppExtension;
class AppBundle extends Bundle
{
public function getContainerExtension()
{
return new AppExtension();
}
}

View file

@ -16,6 +16,9 @@ use Symfony\Component\Form\FormError;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use App\Site\PageLocator;
use App\Factory\Site\Page\PageFactory;
use App\Entity\Site\Page\Page;
/**
* @Route("/admin/site/node")
@ -28,12 +31,16 @@ class NodeAdminController extends AdminController
public function new(
Node $node,
EntityFactory $factory,
PageFactory $pageFactory,
EntityManager $entityManager,
NodeRepository $nodeRepository,
PageLocator $pageLocator,
Request $request
): Response {
$entity = $factory->create($node->getMenu());
$form = $this->createForm(EntityType::class, $entity);
$form = $this->createForm(EntityType::class, $entity, [
'pages' => $pageLocator->getPages(),
]);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
@ -46,17 +53,24 @@ class NodeAdminController extends AdminController
if ('above' === $position) {
$nodeRepository->persistAsLastChild($entity, $node);
$entityManager->flush();
} else {
if ('after' === $position) {
$nodeRepository->persistAsNextSiblingOf($entity, $node);
} elseif ('before' === $position) {
$nodeRepository->persistAsPrevSiblingOf($entity, $node);
}
$entityManager->flush();
}
$this->handlePageAssociation(
$form->get('pageAction')->getData(),
$form->get('pageEntity')->getData(),
$form->get('pageType')->getData(),
$entity,
$pageFactory
);
$entityManager->update($entity);
$this->addFlash('success', 'Donnée enregistrée.');
} else {
$this->addFlash('warning', 'Le formulaire est invalide.');
@ -77,13 +91,29 @@ class NodeAdminController extends AdminController
/**
* @Route("/edit/{entity}", name="admin_site_node_edit")
*/
public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response {
$form = $this->createForm(EntityType::class, $entity);
public function edit(
Entity $entity,
EntityManager $entityManager,
PageFactory $pageFactory,
PageLocator $pageLocator,
Request $request
): Response {
$form = $this->createForm(EntityType::class, $entity, [
'pages' => $pageLocator->getPages(),
]);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
$this->handlePageAssociation(
$form->get('pageAction')->getData(),
$form->get('pageEntity')->getData(),
$form->get('pageType')->getData(),
$entity,
$pageFactory
);
$entityManager->update($entity);
$this->addFlash('success', 'Donnée enregistrée.');
@ -102,6 +132,28 @@ class NodeAdminController extends AdminController
]);
}
protected function handlePageAssociation(
string $pageAction,
?Page $pageEntity,
string $pageType,
Entity $entity,
PageFactory $pageFactory
)
{
if ($pageAction === 'new') {
$page = $pageFactory->create($pageType, $entity->getLabel());
$entity->setPage($page);
} elseif ($pageAction === 'existing') {
if ($pageEntity) {
$entity->setPage($pageEntity);
} else {
$this->addFlash('info', 'Aucun changement de page effectué.');
}
} elseif ($pageAction === 'none') {
$entity->setPage(null);
}
}
/**
* @Route("/move/{entity}", name="admin_site_node_move")
*/

View file

@ -47,11 +47,6 @@ class PageAdminController extends AdminController
EntityRepositoryQuery $repositoryQuery,
Request $request
): Response {
// $page = $factory->create(PageFoo::class);
// $page->setName('Page de test 2');
// $entityManager->update($page);
// die;
$entity = $repositoryQuery->filterById($entity)->findOne();
$form = $this->createForm(EntityType::class, $entity);
@ -77,6 +72,20 @@ class PageAdminController extends AdminController
]);
}
/**
* @Route("/delete/{entity}", name="admin_site_page_delete", methods={"DELETE"})
*/
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
{
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
$entityManager->delete($entity);
$this->addFlash('success', 'Données supprimée..');
}
return $this->redirectToRoute('admin_site_tree_index');
}
public function getSection(): string
{
return '';

View file

@ -0,0 +1,29 @@
<?php
namespace App\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use App\DependencyInjection\Configuration;
class AppExtension extends Extension
{
/**
* {@inheritDoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$configuration = $this->getConfiguration($configs, $container);
$config = $this->processConfiguration($configuration, $configs);
$container->setParameter('app', $config);
}
/**
* {@inheritDoc}
*/
public function getConfiguration(array $configs, ContainerBuilder $container)
{
return new Configuration();
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace App\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('app');
$treeBuilder->getRootNode()
->children()
->arrayNode('site')
->children()
->arrayNode('pages')
->prototype('array')
->children()
->scalarNode('name')
->isRequired()
->cannotBeEmpty()
->end()
->arrayNode('templates')
->prototype('array')
->children()
->scalarNode('name')
->cannotBeEmpty()
->end()
->scalarNode('file')
->cannotBeEmpty()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end();
return $treeBuilder;
}
}

View file

@ -4,6 +4,7 @@ namespace App\Entity\Site;
use App\Doctrine\Timestampable;
use App\Entity\EntityInterface;
use App\Entity\Site\Page\Page;
use App\Repository\Site\NodeRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
@ -85,6 +86,12 @@ class Node implements EntityInterface
*/
private $children;
/**
* @ORM\ManyToOne(targetEntity=Page::class, inversedBy="nodes", cascade={"persist"})
* @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
*/
private $page;
public function __construct()
{
$this->children = new ArrayCollection();
@ -260,4 +267,16 @@ class Node implements EntityInterface
return trim($prefix.' '.$this->getLabel());
}
public function getPage(): ?Page
{
return $this->page;
}
public function setPage(?Page $page): self
{
$this->page = $page;
return $this;
}
}

View file

@ -33,6 +33,7 @@ class Block
/**
* @ORM\ManyToOne(targetEntity=Page::class, inversedBy="blocks")
* @ORM\JoinColumn(onDelete="CASCADE")
*/
private $page;

View file

@ -4,6 +4,7 @@ namespace App\Entity\Site\Page;
use App\Doctrine\Timestampable;
use App\Entity\EntityInterface;
use App\Entity\Site\Node;
use App\Repository\Site\Page\PageRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
@ -62,9 +63,15 @@ class Page implements EntityInterface
*/
private $ogDescription;
/**
* @ORM\OneToMany(targetEntity=Node::class, mappedBy="page")
*/
private $nodes;
public function __construct()
{
$this->blocks = new ArrayCollection();
$this->nodes = new ArrayCollection();
}
public function getId(): ?int
@ -208,4 +215,34 @@ class Page implements EntityInterface
return $this;
}
/**
* @return Collection|Node[]
*/
public function getNodes(): Collection
{
return $this->nodes;
}
public function addNode(Node $node): self
{
if (!$this->nodes->contains($node)) {
$this->nodes[] = $node;
$node->setPage($this);
}
return $this;
}
public function removeNode(Node $node): self
{
if ($this->nodes->removeElement($node)) {
// set the owning side to null (unless already changed)
if ($node->getPage() === $this) {
$node->setPage(null);
}
}
return $this;
}
}

View file

@ -11,8 +11,11 @@ use App\Entity\Site\Page\Page;
*/
class PageFactory
{
public function create(string $className): Page
public function create(string $className, string $name): Page
{
return new $className();
$entity = new $className();
$entity->setName($name);
return $entity;
}
}

View file

@ -9,6 +9,9 @@ use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
use App\Entity\Site\Page\Page;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Doctrine\ORM\EntityRepository;
class NodeType extends AbstractType
{
@ -42,6 +45,73 @@ class NodeType extends AbstractType
]
);
$actions = [
'Nouvelle page' => 'new',
'Associer à une page existante' => 'existing',
'Aucune page' => 'none',
];
if ($builder->getData()->getId()) {
$actions['Garder la configuration actuelle'] = 'keep';
}
$builder->add(
'pageAction',
ChoiceType::class,
[
'label' => false,
'required' => true,
'expanded' => true,
'mapped' => false,
'choices' => $actions,
'constraints' => [
new NotBlank(),
],
]
);
$builder->add(
'pageType',
ChoiceType::class,
[
'label' => false,
'required' => true,
'mapped' => false,
'choices' => call_user_func(function() use ($options) {
$choices = [];
foreach ($options['pages'] as $page) {
$choices[$page->getName()] = $page->getClassName();
}
return $choices;
}),
'constraints' => [
new NotBlank(),
],
]
);
$builder->add(
'pageEntity',
EntityType::class,
[
'label' => false,
'required' => true,
'mapped' => false,
'class' => Page::class,
'choice_label' => 'name',
'query_builder' => function (EntityRepository $repo) {
return $repo->createQueryBuilder('p')
->orderBy('p.name', 'ASC')
;
},
'constraints' => [
new NotBlank(),
],
]
);
if ($builder->getData()->getId() === null) {
$builder->add(
'position',
@ -69,7 +139,7 @@ class NodeType extends AbstractType
{
$resolver->setDefaults([
'data_class' => Node::class,
'pages' => null,
'pages' => [],
]);
}
}

View file

@ -7,11 +7,26 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
class PageType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'name',
TextType::class,
[
'label' => 'Nom',
'required' => true,
'attr' => [
],
'constraints' => [
new NotBlank(),
],
]
);
$builder->add(
'metaTitle',
TextType::class,

View file

@ -1,6 +1,6 @@
<?php
namespace App\Page;
namespace App\Site\Page;
use App\Entity\Site\Page\Block;
use App\Entity\Site\Page\Page;

View file

@ -0,0 +1,51 @@
<?php
namespace App\Site;
/**
* class PageConfiguration.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class PageConfiguration
{
protected string $className;
protected string $name;
protected array $templates;
public function setClassName(string $className): self
{
$this->className = $className;
return $this;
}
public function getClassName(): string
{
return $this->className;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getName(): string
{
return $this->name;
}
public function setTemplates(array $templates): self
{
$this->templates = $templates;
return $this;
}
public function getTemplates(): array
{
return $this->templates;
}
}

48
src/Site/PageLocator.php Normal file
View file

@ -0,0 +1,48 @@
<?php
namespace App\Site;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
/**
* class PageLocator.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class PageLocator
{
protected array $params;
protected array $pages;
public function __construct(ParameterBagInterface $bag)
{
$this->params = $bag->get('app');
$this->loadPages();
}
protected function loadPages(): void
{
$params = $this->params['site']['pages'] ?? [];
foreach ($params as $className => $conf) {
$pageConfiguration = new PageConfiguration();
$pageConfiguration
->setClassName($className)
->setName($conf['name'])
->setTemplates($conf['templates'])
;
$this->pages[$className] = $pageConfiguration;
}
}
public function getPages(): array
{
return $this->pages;
}
public function getPage($className)
{
return $this->pages[$className] ?? null;
}
}

View file

@ -0,0 +1,108 @@
{{ form_row(form.label) }}
{{ form_row(form.url) }}
{% if form.position is defined %}
{{ form_row(form.position) }}
{% endif %}
<div class="accordion" id="node-page-action">
<div class="card">
{% set action = form.pageAction[0] %}
{% set options = not entity.id ? {'attr': {'checked': 'checked'}} : {} %}
<div class="card-header p-0">
<h2 class="mb-0">
<label class="btn btn-link btn-block text-left"
for="{{ action.vars.id }}"
data-toggle="collapse"
data-target="#form-node-page-action-new">
{{ action.vars.label }}
</label>
<div class="d-none">
{{ form_row(action, options) }}
</div>
</h2>
</div>
<div id="form-node-page-action-new" class="collapse {% if not entity.id %}show{% endif %}" data-parent="#node-page-action">
<div class="card-body">
{{ form_row(form.pageType) }}
</div>
</div>
</div>
<div class="card">
{% set action = form.pageAction[1] %}
<div class="card-header p-0">
<h2 class="mb-0">
<label class="btn btn-link btn-block text-left"
for="{{ action.vars.id }}"
data-toggle="collapse"
data-target="#form-node-page-action-existing">
{{ action.vars.label }}
</label>
<div class="d-none">
{{ form_row(action) }}
</div>
</h2>
</div>
<div id="form-node-page-action-existing" class="collapse" data-parent="#node-page-action">
<div class="card-body">
{{ form_row(form.pageEntity) }}
</div>
</div>
</div>
<div class="card">
{% set action = form.pageAction[2] %}
<div class="card-header p-0">
<h2 class="mb-0">
<label class="btn btn-link btn-block text-left"
for="{{ action.vars.id }}"
data-toggle="collapse"
data-target="#form-node-page-action-none">
{{ action.vars.label }}
</label>
<div class="d-none">
{{ form_row(action) }}
</div>
</h2>
</div>
<div id="form-node-page-action-none" class="collapse" data-parent="#node-page-action">
<div class="card-body">
Aucune action
</div>
</div>
</div>
{% if entity.id %}
<div class="card">
{% set action = form.pageAction[3] %}
{% set options = {'attr': {'checked': 'checked'}} %}
<div class="card-header p-0">
<h2 class="mb-0">
<label class="btn btn-link btn-block text-left"
for="{{ action.vars.id }}"
data-toggle="collapse"
data-target="#form-node-page-action-keep">
{{ action.vars.label }}
</label>
<div class="d-none">
{{ form_row(action, options) }}
</div>
</h2>
</div>
<div id="form-node-page-action-keep" class="collapse show" data-parent="#node-page-action">
<div class="card-body">
Aucune action
</div>
</div>
</div>
{% endif %}
</div>
{{ form_rest(form) }}

View file

@ -8,7 +8,7 @@
</div>
<div class="modal-body">
<form action="{{ path('admin_site_node_edit', {entity: entity.id}) }}" id="form-node-edit" method="POST">
{{ form_widget(form) }}
{{ include('site/node_admin/_form.html.twig') }}
</form>
</div>
<div class="modal-footer">

View file

@ -8,7 +8,7 @@
</div>
<div class="modal-body">
<form action="{{ path('admin_site_node_new', {node: node.id}) }}" id="form-node-new" method="POST">
{{ form_widget(form) }}
{{ include('site/node_admin/_form.html.twig') }}
</form>
</div>
<div class="modal-footer">

View file

@ -10,6 +10,12 @@
{% endfor %}
{% endset %}
{% set formOthers %}
{% for item in ['name'] %}
{{ form_row(form[item]) }}
{% endfor %}
{% endset %}
{% set formSitemap %}
{% endset %}
@ -25,6 +31,9 @@
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#form-page-og">OpenGraph</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#form-page-others">Autres</a>
</li>
</ul>
<div class="tab-content">
@ -34,7 +43,9 @@
<div class="tab-pane p-3" id="form-page-og">
{{ formOpenGraph|raw }}
</div>
<div class="tab-pane p-3" id="form-page-others">
{{ formOthers|raw }}
</div>
</div>
</div>
</div>

View file

@ -13,6 +13,17 @@
<span class="fa fa-save pr-1"></span>
Enregistrer
</button>
<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">
⋅⋅⋅
</span>
</button>
<div class="dropdown-menu dropdown-menu-right">
<button type="submit" form="form-delete" class="dropdown-item">
Supprimer
</button>
</div>
</div>
</div>
</div>
@ -29,4 +40,9 @@
{{ form_rest(form) }}
</form>
<form method="post" action="{{ path('admin_site_page_delete', {entity: entity.id}) }}" id="form-delete" data-form-confirm>
<input type="hidden" name="_method" value="DELETE">
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ entity.id) }}">
</form>
{% endblock %}

View file

@ -70,30 +70,42 @@
{% set move = path('admin_site_node_move', {entity: node.id}) %}
{% set edit = path('admin_site_node_edit', {entity: node.id}) %}
{% set new = path('admin_site_node_new', {node: node.id}) %}
<div class="list-group-item">
<div class="float-right">
{% if node.page %}
<a href="{{ path('admin_site_page_edit', {entity: node.page.id}) }}" class="btn btn-sm btn-warning text-white mr-1">
<span class="fa fa-file-alt"></span>
Page
</a>
{% endif %}
<button data-modal="{{ edit }}" type="submit" class="btn btn-sm btn-success mr-1">
<span data-modal="{{ edit }}" class="fa fa-pen"></span>
</button>
<button data-modal="{{ move }}" type="submit" class="btn btn-sm btn-dark mr-1">
<span data-modal="{{ move }}" class="fa fa-arrows-alt"></span>
Éditer
</button>
<button type="submit" form="form-node-visibility-{{ node.id }}" class="btn btn-sm btn-light border-dark mr-1">
{% if node.isVisible %}
<span class="fa fa-eye"></span>
Visible
{% else %}
<span class="fa fa-eye-slash"></span>
Caché
{% endif %}
</button>
<button data-modal="{{ move }}" type="submit" class="btn btn-sm btn-dark mr-1">
<span data-modal="{{ move }}" class="fa fa-arrows-alt"></span>
</button>
<button data-modal="{{ new }}" type="submit" class="btn btn-sm btn-primary mr-1">
<span data-modal="{{ new }}" class="fa fa-plus"></span>
</button>
<button type="submit" form="form-node-delete-{{ node.id }}" class="btn btn-sm btn-danger">
<span class="fa fa-trash"></span>
Supprimer
</button>
</div>