diff --git a/CHANGELOG.md b/CHANGELOG.md
index efb9cabf0..648058e67 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -81,10 +81,47 @@ Version 1.5.13
- allow to customize logo on login screen and main layout ;
- remove desert background image on page, handle it from cache in login screen;
-Master branch
-=============
+Version 1.5.14
+==============
- fix errors in pagination
- fix search: usage of parenthesis
- add DQL function REPLACE for replacing in strings: "REPLACE(string, 'from', 'to')"
- add function to format phonenumber
+- improve `chill_print_or_message` to support date time;
+- add a module `show_hide` for javascript;
+- load assets using functions ;
+- load a `runtime.js` assets for objects shared by webpack ;
+
+Version 1.5.15
+==============
+
+- create an api for rendering entities
+- css: render the placeholder in expanded choice item as italic (the "no specified" choice")
+- css: add an extra space around choices expanded widget
+- add Tabs parametric feature to easily render tabs panels
+- css: add a margin on the button "delete entry" in collection
+- module `show_hide`: add the possibility to launch a show hide manually and not on page loading. Useful when show/hide occurs in collection.
+- module `show_hide`: add events to module
+- [phonenumber validation] allow to validate against mobile **or** landline/voip phonenumbers;
+- [phonenumber validation & format] format and validation does not make the app fail when network is not available;
+
+Version 1.5.16
+==============
+
+- [translation] in french, replace "Modifier" by "Enregistrer" in the edit form
+- [entity render] do not throw an exception when null element are passed to `chill_entity_render_box` and `chill_entity_render_string`
+
+Version 1.5.17
+==============
+
+- [chill entity render] fix error when fallback to default entity render (usage of `__toString()`)
+- [CRUD] add step delete
+- [CRUD] check that action exists before inserting them in edit and view template
+- [CRUD] fix error when no crud are created
+
+Version 1.5.18
+==============
+
+- [webpack] add namespace for import sass ;
+- [activity] move activity.scss to own bundle ;
diff --git a/CRUD/Controller/CRUDController.php b/CRUD/Controller/CRUDController.php
new file mode 100644
index 000000000..c7d4586d4
--- /dev/null
+++ b/CRUD/Controller/CRUDController.php
@@ -0,0 +1,1044 @@
+
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+namespace Chill\MainBundle\CRUD\Controller;
+
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Doctrine\ORM\QueryBuilder;
+use Chill\MainBundle\Pagination\PaginatorFactory;
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Translation\TranslatorInterface;
+use Symfony\Component\Form\Extension\Core\Type\SubmitType;
+use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
+use Symfony\Component\Security\Core\Role\Role;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Chill\MainBundle\CRUD\Resolver\Resolver;
+use Chill\MainBundle\Pagination\PaginatorInterface;
+use Chill\MainBundle\CRUD\Form\CRUDDeleteEntityForm;
+
+/**
+ *
+ *
+ */
+class CRUDController extends AbstractController
+{
+ /**
+ * The crud configuration
+ *
+ * This configuration si defined by `chill_main['crud']`.
+ *
+ * @var array
+ */
+ protected $crudConfig;
+
+ public function setCrudConfig(array $config)
+ {
+ $this->crudConfig = $config;
+ }
+
+ public function delete(Request $request, $id)
+ {
+ return $this->deleteAction('delete', $request, $id);
+ }
+
+ protected function deleteAction(string $action, Request $request, $id, $formClass = null)
+ {
+ $this->onPreDelete($action, $request, $id);
+
+ $entity = $this->getEntity($action, $id, $request);
+
+ $postFetch = $this->onPostFetchEntity($action, $request, $entity);
+
+ if ($postFetch instanceof Response) {
+ return $postFetch;
+ }
+
+ if (NULL === $entity) {
+ throw $this->createNotFoundException(sprintf("The %s with id %s "
+ . "is not found"), $this->getCrudName(), $id);
+ }
+
+ $response = $this->checkACL($action, $entity);
+ if ($response instanceof Response) {
+ return $response;
+ }
+
+ $response = $this->onPostCheckACL($action, $request, $entity);
+ if ($response instanceof Response) {
+ return $response;
+ }
+
+ $form = $this->createFormFor($action, $entity, $formClass);
+
+ $form->handleRequest($request);
+
+ if ($form->isSubmitted() && $form->isValid()) {
+ $this->onFormValid($entity, $form, $request);
+ $em = $this->getDoctrine()->getManager();
+
+ $this->onPreRemove($action, $entity, $form, $request);
+ $this->removeEntity($action, $entity, $form, $request);
+ $this->onPostRemove($action, $entity, $form, $request);
+
+ $this->onPreFlush($action, $entity, $form, $request);
+ $em->flush();
+ $this->onPostFlush($action, $entity, $form, $request);
+
+ $this->addFlash('success', $this->generateFormSuccessMessage($action, $entity));
+
+ $result = $this->onBeforeRedirectAfterSubmission($action, $entity, $form, $request);
+
+ if ($result instanceof Response) {
+ return $result;
+ }
+
+ return $this->redirectToRoute('chill_crud_'.$this->getCrudName().'_view', ['id' => $entity->getId()]);
+
+ } elseif ($form->isSubmitted()) {
+ $this->addFlash('error', $this->generateFormErrorMessage($action, $form));
+ }
+
+ $defaultTemplateParameters = [
+ 'form' => $form->createView(),
+ 'entity' => $entity,
+ 'crud_name' => $this->getCrudName()
+ ];
+
+ return $this->render(
+ $this->getTemplateFor($action, $entity, $request),
+ $this->generateTemplateParameter($action, $entity, $request, $defaultTemplateParameters)
+ );
+ }
+
+ protected function onPreDelete(string $action, Request $request) {}
+
+ protected function onPreRemove(string $action, $entity, FormInterface $form, Request $request) {}
+
+ protected function onPostRemove(string $action, $entity, FormInterface $form, Request $request) {}
+
+ protected function removeEntity(string $action, $entity, FormInterface $form, Request $request)
+ {
+ $this->getDoctrine()
+ ->getManager()
+ ->remove($entity);
+ }
+
+ /**
+ * Base method called by index action.
+ *
+ * @param Request $request
+ * @return type
+ */
+ public function index(Request $request)
+ {
+ return $this->indexEntityAction('index', $request);
+ }
+
+ /**
+ * Build an index page.
+ *
+ * Some steps may be overriden during this process of rendering.
+ *
+ * This method:
+ *
+ * 1. Launch `onPreIndex`
+ * x. check acl. If it does return a response instance, return it
+ * x. launch `onPostCheckACL`. If it does return a response instance, return it
+ * 1. count the items, using `countEntities`
+ * 2. build a paginator element from the the number of entities ;
+ * 3. Launch `onPreIndexQuery`. If it does return a response instance, return it
+ * 3. build a query, using `queryEntities`
+ * x. fetch the results, using `getQueryResult`
+ * x. Launch `onPostIndexFetchQuery`. If it does return a response instance, return it
+ * 4. create default parameters:
+ *
+ * The default parameters are:
+ *
+ * * entities: the list en entities ;
+ * * crud_name: the name of the crud ;
+ * * paginator: a paginator element ;
+ * 5. Launch rendering, the parameter is fetch using `getTemplateFor`
+ * The parameters may be personnalized using `generateTemplateParameter`.
+ *
+ * @param string $action
+ * @param Request $request
+ * @return type
+ */
+ protected function indexEntityAction($action, Request $request)
+ {
+ $this->onPreIndex($action, $request);
+
+ $response = $this->checkACL($action, null);
+ if ($response instanceof Response) {
+ return $response;
+ }
+
+ $response = $this->onPostCheckACL($action, $request, $entity);
+ if ($response instanceof Response) {
+ return $response;
+ }
+
+ $totalItems = $this->countEntities($action, $request);
+ $paginator = $this->getPaginatorFactory()->create($totalItems);
+
+ $response = $this->onPreIndexBuildQuery($action, $request, $totalItems,
+ $paginator);
+
+ if ($response instanceof Response) {
+ return $response;
+ }
+
+ $query = $this->queryEntities($action, $request, $paginator);
+
+ $response = $this->onPostIndexBuildQuery($action, $request, $totalItems,
+ $paginator, $query);
+
+ if ($response instanceof Response) {
+ return $response;
+ }
+
+ $entities = $this->getQueryResult($action, $request, $totalItems, $paginator, $query);
+
+ $response = $this->onPostIndexFetchQuery($action, $request, $totalItems,
+ $paginator, $entities);
+
+ if ($response instanceof Response) {
+ return $response;
+ }
+
+ $defaultTemplateParameters = [
+ 'entities' => $entities,
+ 'crud_name' => $this->getCrudName(),
+ 'paginator' => $paginator
+ ];
+
+ return $this->render(
+ $this->getTemplateFor($action, $entities, $request),
+ $this->generateTemplateParameter($action, $entities, $request, $defaultTemplateParameters)
+ );
+ }
+
+ /**
+ *
+ * @param string $action
+ * @param Request $request
+ */
+ protected function onPreIndex(string $action, Request $request) { }
+
+ /**
+ * method used by indexAction
+ *
+ * @param string $action
+ * @param Request $request
+ * @param int $totalItems
+ * @param PaginatorInterface $paginator
+ */
+ protected function onPreIndexBuildQuery(string $action, Request $request, int $totalItems, PaginatorInterface $paginator) { }
+
+ /**
+ * method used by indexAction
+ *
+ * @param string $action
+ * @param Request $request
+ * @param int $totalItems
+ * @param PaginatorInterface $paginator
+ * @param mixed $query
+ */
+ protected function onPostIndexBuildQuery(string $action, Request $request, int $totalItems, PaginatorInterface $paginator, $query) { }
+
+ /**
+ * method used by indexAction
+ *
+ * @param string $action
+ * @param Request $request
+ * @param int $totalItems
+ * @param PaginatorInterface $paginator
+ * @param mixed $entities
+ */
+ protected function onPostIndexFetchQuery(string $action, Request $request, int $totalItems, PaginatorInterface $paginator, $entities) { }
+
+ /**
+ * Query the entity.
+ *
+ * By default, get all entities.
+ *
+ * The method `orderEntity` is called internally to order entities.
+ *
+ * It returns, by default, a query builder.
+ *
+ * @param string $action
+ * @param Request $request
+ * @param PaginatorInterface $paginator
+ * @return type
+ */
+ protected function queryEntities(string $action, Request $request, PaginatorInterface $paginator)
+ {
+ $query = $this->getDoctrine()->getManager()
+ ->createQueryBuilder()
+ ->select('e')
+ ->from($this->getEntityClass(), 'e')
+ ->setFirstResult($paginator->getCurrentPage()->getFirstItemNumber())
+ ->setMaxResults($paginator->getItemsPerPage())
+ ;
+
+ $this->orderQuery($action, $query, $request, $paginator);
+
+ return $query;
+ }
+
+
+ /**
+ * Add ordering fields in the query build by self::queryEntities
+ *
+ * @param string $action
+ * @param QueryBuilder|mixed $query by default, an instance of QueryBuilder
+ * @param Request $request
+ * @param PaginatorInterface $paginator
+ * @return QueryBuilder|mixed
+ */
+ protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
+ {
+ return $query;
+ }
+
+ /**
+ * Get the result of the query
+ *
+ * @param string $action
+ * @param Request $request
+ * @param int $totalItems
+ * @param PaginatorInterface $paginator
+ * @param mixed $query
+ * @return mixed
+ */
+ protected function getQueryResult(string $action, Request $request, int $totalItems, PaginatorInterface $paginator, $query)
+ {
+ return $query->getQuery()->getResult();
+ }
+
+ /**
+ * Count the number of entities
+ *
+ * @param string $action
+ * @param Request $request
+ * @return int
+ */
+ protected function countEntities(string $action, Request $request): int
+ {
+ return $this->getDoctrine()->getManager()
+ ->createQuery("SELECT COUNT(e) FROM ".$this->getEntityClass()." e")
+ ->getSingleScalarResult()
+ ;
+ }
+
+ /**
+ * BAse method for edit action
+ *
+ * IMplemented by the method formEditAction, with action as 'edit'
+ *
+ * @param Request $request
+ * @param mixed $id
+ * @return Response
+ */
+ public function edit(Request $request, $id): Response
+ {
+ return $this->formEditAction('edit', $request, $id);
+ }
+
+ /**
+ * Base method for new action
+ *
+ * Implemented by the method formNewAction, with action as 'new'
+ *
+ * @param Request $request
+ * @return Response
+ */
+ public function new(Request $request): Response
+ {
+ return $this->formCreateAction('new', $request);
+ }
+
+ /**
+ * Base method for the view action.
+ *
+ * Implemented by the method viewAction, with action as 'view'
+ *
+ * @param Request $request
+ * @param mixed $id
+ * @return Response
+ */
+ public function view(Request $request, $id): Response
+ {
+ return $this->viewAction('view', $request, $id);
+ }
+
+ /**
+ * The view action.
+ *
+ * Some steps may be overriden during this process of rendering:
+ *
+ * This method:
+ *
+ * 1. fetch the entity, using `getEntity`
+ * 2. launch `onPostFetchEntity`. If postfetch is an instance of Response,
+ * this response is returned.
+ * 2. throw an HttpNotFoundException if entity is null
+ * 3. check ACL using `checkACL` ;
+ * 4. launch `onPostCheckACL`. If the result is an instance of Response,
+ * this response is returned ;
+ * 5. generate default template parameters:
+ *
+ * * `entity`: the fetched entity ;
+ * * `crud_name`: the crud name
+ * 6. Launch rendering, the parameter is fetch using `getTemplateFor`
+ * The parameters may be personnalized using `generateTemplateParameter`.
+ *
+ * @param string $action
+ * @param Request $request
+ * @param mixed $id
+ * @return Response
+ */
+ protected function viewAction(string $action, Request $request, $id)
+ {
+ $entity = $this->getEntity($action, $id, $request);
+
+ $postFetch = $this->onPostFetchEntity($action, $request, $entity);
+
+ if ($postFetch instanceof Response) {
+ return $postFetch;
+ }
+
+ if (NULL === $entity) {
+ throw $this->createNotFoundException(sprintf("The %s with id %s "
+ . "is not found"), $this->getCrudName(), $id);
+ }
+
+ $response = $this->checkACL($action, $entity);
+ if ($response instanceof Response) {
+ return $response;
+ }
+
+ $response = $this->onPostCheckACL($action, $request, $entity);
+ if ($response instanceof Response) {
+ return $response;
+ }
+
+ $defaultTemplateParameters = [
+ 'entity' => $entity,
+ 'crud_name' => $this->getCrudName()
+ ];
+
+ return $this->render(
+ $this->getTemplateFor($action, $entity, $request),
+ $this->generateTemplateParameter($action, $entity, $request, $defaultTemplateParameters)
+ );
+ }
+
+ /**
+ * The edit action.
+ *
+ * Some steps may be overriden during this process of rendering:
+ *
+ * This method:
+ *
+ * 1. fetch the entity, using `getEntity`
+ * 2. launch `onPostFetchEntity`. If postfetch is an instance of Response,
+ * this response is returned.
+ * 2. throw an HttpNotFoundException if entity is null
+ * 3. check ACL using `checkACL` ;
+ * 4. launch `onPostCheckACL`. If the result is an instance of Response,
+ * this response is returned ;
+ * 5. generate a form using `createFormFor`, and handle request on this form;
+ *
+ * If the form is valid, the entity is stored and flushed, and a redirection
+ * is returned.
+ *
+ * In this case, those hooks are available:
+ *
+ * * onFormValid
+ * * onPreFlush
+ * * onPostFlush
+ * * onBeforeRedirectAfterSubmission. If this method return an instance of
+ * Response, this response is returned.
+ *
+ * 5. generate default template parameters:
+ *
+ * * `entity`: the fetched entity ;
+ * * `crud_name`: the crud name ;
+ * * `form`: the formview instance.
+ *
+ * 6. Launch rendering, the parameter is fetch using `getTemplateFor`
+ * The parameters may be personnalized using `generateTemplateParameter`.
+ *
+ * @param string $action
+ * @param Request $request
+ * @param mixed $id
+ * @param string $formClass
+ * @param array $formOptions
+ * @return Response
+ * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
+ */
+ protected function formEditAction(string $action, Request $request, $id, string $formClass = null, array $formOptions = []): Response
+ {
+ $entity = $this->getEntity($action, $id, $request);
+
+ if (NULL === $entity) {
+ throw $this->createNotFoundException(sprintf("The %s with id %s "
+ . "is not found"), $this->getCrudName(), $id);
+ }
+
+ $response = $this->checkACL($action, $entity);
+ if ($response instanceof Response) {
+ return $response;
+ }
+
+ $response = $this->onPostCheckACL($action, $request, $entity);
+ if ($response instanceof Response) {
+ return $response;
+ }
+
+ $form = $this->createFormFor($action, $entity, $formClass, $formOptions);
+
+ $form->handleRequest($request);
+
+ if ($form->isSubmitted() && $form->isValid()) {
+ $this->onFormValid($entity, $form, $request);
+ $em = $this->getDoctrine()->getManager();
+
+ $this->onPreFlush($action, $entity, $form, $request);
+ $em->flush();
+ $this->onPostFlush($action, $entity, $form, $request);
+
+ $this->addFlash('success', $this->generateFormSuccessMessage($action, $entity));
+
+ $result = $this->onBeforeRedirectAfterSubmission($action, $entity, $form, $request);
+
+ if ($result instanceof Response) {
+ return $result;
+ }
+
+ return $this->redirectToRoute('chill_crud_'.$this->getCrudName().'_index');
+
+ } elseif ($form->isSubmitted()) {
+ $this->addFlash('error', $this->generateFormErrorMessage($action, $form));
+ }
+
+ $defaultTemplateParameters = [
+ 'form' => $form->createView(),
+ 'entity' => $entity,
+ 'crud_name' => $this->getCrudName()
+ ];
+
+ return $this->render(
+ $this->getTemplateFor($action, $entity, $request),
+ $this->generateTemplateParameter($action, $entity, $request, $defaultTemplateParameters)
+ );
+ }
+
+ /**
+ * The new (or creation) action.
+ *
+ * Some steps may be overriden during this process of rendering:
+ *
+ * This method:
+ *
+ * 1. Create or duplicate an entity:
+ *
+ * If the `duplicate` parameter is present, the entity is duplicated
+ * using the `duplicate` method.
+ *
+ * If not, the entity is created using the `create` method.
+ * 3. check ACL using `checkACL` ;
+ * 4. launch `onPostCheckACL`. If the result is an instance of Response,
+ * this response is returned ;
+ * 5. generate a form using `createFormFor`, and handle request on this form;
+ *
+ * If the form is valid, the entity is stored and flushed, and a redirection
+ * is returned.
+ *
+ * In this case, those hooks are available:
+ *
+ * * onFormValid
+ * * onPreFlush
+ * * onPostFlush
+ * * onBeforeRedirectAfterSubmission. If this method return an instance of
+ * Response, this response is returned.
+ *
+ * 5. generate default template parameters:
+ *
+ * * `entity`: the fetched entity ;
+ * * `crud_name`: the crud name ;
+ * * `form`: the formview instance.
+ *
+ * 6. Launch rendering, the parameter is fetch using `getTemplateFor`
+ * The parameters may be personnalized using `generateTemplateParameter`.
+ *
+ * @param string $action
+ * @param Request $request
+ * @param type $formClass
+ * @return Response
+ */
+ protected function formCreateAction(string $action, Request $request, $formClass = null): Response
+ {
+ if ($request->query->has('duplicate')) {
+ $entity = $this->duplicateEntity($action, $request);
+ } else {
+ $entity = $this->createEntity($action, $request);
+ }
+
+ $response = $this->checkACL($action, $entity);
+ if ($response instanceof Response) {
+ return $response;
+ }
+
+ $response = $this->onPostCheckACL($action, $request, $entity);
+ if ($response instanceof Response) {
+ return $response;
+ }
+
+ $form = $this->createFormFor($action, $entity, $formClass);
+
+ $form->handleRequest($request);
+
+ if ($form->isSubmitted() && $form->isValid()) {
+ $this->onFormValid($entity, $form, $request);
+ $em = $this->getDoctrine()->getManager();
+
+ $this->onPrePersist($action, $entity, $form, $request);
+ $em->persist($entity);
+ $this->onPostPersist($action, $entity, $form, $request);
+
+ $this->onPreFlush($action, $entity, $form, $request);
+ $em->flush();
+ $this->onPostFlush($action, $entity, $form, $request);
+ $this->getPaginatorFactory();
+ $this->addFlash('success', $this->generateFormSuccessMessage($action, $entity));
+
+ $result = $this->onBeforeRedirectAfterSubmission($action, $entity, $form, $request);
+
+ if ($result instanceof Response) {
+ return $result;
+ }
+
+ return $this->redirectToRoute('chill_crud_'.$this->getCrudName().'_view', ['id' => $entity->getId()]);
+
+ } elseif ($form->isSubmitted()) {
+ $this->addFlash('error', $this->generateFormErrorMessage($action, $form));
+ }
+
+ $defaultTemplateParameters = [
+ 'form' => $form->createView(),
+ 'entity' => $entity,
+ 'crud_name' => $this->getCrudName()
+ ];
+
+ return $this->render(
+ $this->getTemplateFor($action, $entity, $request),
+ $this->generateTemplateParameter($action, $entity, $request, $defaultTemplateParameters)
+ );
+ }
+
+ /**
+ * get the instance of the entity with the given id
+ *
+ * @param string $id
+ * @return object
+ */
+ protected function getEntity($action, $id, Request $request): ?object
+ {
+ return $this->getDoctrine()
+ ->getRepository($this->getEntityClass())
+ ->find($id);
+ }
+
+ /**
+ * Duplicate an entity
+ *
+ * @param string $action
+ * @param Request $request
+ * @return mixed
+ */
+ protected function duplicateEntity(string $action, Request $request)
+ {
+ $id = $request->query->get('duplicate_id', 0);
+ $originalEntity = $this->getEntity($action, $id, $request);
+
+ $this->getDoctrine()->getManager()
+ ->detach($originalEntity);
+
+ return $originalEntity;
+ }
+
+ /**
+ *
+ * @return string the complete fqdn of the class
+ */
+ protected function getEntityClass(): string
+ {
+ return $this->crudConfig['class'];
+ }
+
+ /**
+ *
+ * @return string the crud name
+ */
+ protected function getCrudName(): string
+ {
+ return $this->crudConfig['name'];
+ }
+
+ /**
+ * check the acl. Called by every action.
+ *
+ * By default, check the role given by `getRoleFor` for the value given in
+ * $entity.
+ *
+ * Throw an \Symfony\Component\Security\Core\Exception\AccessDeniedHttpException
+ * if not accessible.
+ *
+ * @param string $action
+ * @param mixed $entity
+ * @throws \Symfony\Component\Security\Core\Exception\AccessDeniedHttpException
+ */
+ protected function checkACL(string $action, $entity)
+ {
+ $this->denyAccessUnlessGranted($this->getRoleFor($action), $entity);
+ }
+
+ /**
+ * get the role given from the config.
+ *
+ * @param string $action
+ * @return string
+ */
+ protected function getRoleFor($action)
+ {
+ if (NULL !== ($this->getActionConfig($action)['role'])) {
+ return $this->getActionConfig($action)['role'];
+ }
+
+ return $this->buildDefaultRole($action);
+ }
+
+ /**
+ * build a default role name, using the crud resolver.
+ *
+ * This method should not be overriden. Override `getRoleFor` instead.
+ *
+ * @param string $action
+ * @return string
+ */
+ protected function buildDefaultRole($action)
+ {
+ return $this->getCrudResolver()->buildDefaultRole($this->getCrudName(),
+ $action);
+ }
+
+ /**
+ * get the default form class from config
+ *
+ * @param string $action
+ * @return string the FQDN of the form class
+ */
+ protected function getFormClassFor($action)
+ {
+ if ($action === 'delete') {
+ return $this->crudConfig[$action]['form_class']
+ ?? $this->getDefaultDeleteFormClass($action);
+ }
+
+ return $this->crudConfig[$action]['form_class']
+ ?? $this->crudConfig['form_class'];
+ }
+
+ protected function getDefaultDeleteFormClass($action)
+ {
+ return CRUDDeleteEntityForm::class;
+ }
+
+ /**
+ * Create a form
+ *
+ * use the method `getFormClassFor`
+ *
+ * A hook is available: `customizeForm` allow you to customize the form
+ * if needed.
+ *
+ * It is preferable to override customizeForm instead of overriding
+ * this method.
+ *
+ * @param string $action
+ * @param mixed $entity
+ * @param string $formClass
+ * @param array $formOptions
+ * @return FormInterface
+ */
+ protected function createFormFor(string $action, $entity, string $formClass = null, array $formOptions = []): FormInterface
+ {
+ $formClass = $formClass ?? $this->getFormClassFor($action);
+
+ $form = $this->createForm($formClass, $entity, $formOptions);
+
+ $this->customizeForm($action, $form);
+
+ return $form;
+ }
+
+ /**
+ * Customize the form created by createFormFor.
+ *
+ * @param string $action
+ * @param FormInterface $form
+ */
+ protected function customizeForm(string $action, FormInterface $form)
+ {
+
+ }
+
+ /**
+ * Generate a message which explains an error about the form.
+ *
+ * Used in form actions
+ *
+ * @param string $action
+ * @param FormInterface $form
+ * @return string
+ */
+ protected function generateFormErrorMessage(string $action, FormInterface $form): string
+ {
+ $msg = 'This form contains errors';
+
+ return $this->getTranslator()->trans($msg);
+ }
+
+ /**
+ * Generate a success message when a form could be flushed successfully
+ *
+ * @param string $action
+ * @param mixed $entity
+ * @return string
+ */
+ protected function generateFormSuccessMessage($action, $entity): string
+ {
+ switch ($action) {
+ case 'edit':
+ $msg = "crud.edit.success";
+ break;
+ case 'new':
+ $msg = "crud.new.success";
+ break;
+ case 'delete':
+ $msg = "crud.delete.success";
+ break;
+ default:
+ $msg = "crud.default.success";
+ }
+
+ return $this->getTranslator()->trans($msg);
+ }
+
+ /**
+ * Customize template parameters.
+ *
+ * @param string $action
+ * @param mixed $entity
+ * @param Request $request
+ * @param array $defaultTemplateParameters
+ * @return array
+ */
+ protected function generateTemplateParameter(
+ string $action,
+ $entity,
+ Request $request,
+ array $defaultTemplateParameters = []
+ ) {
+ return $defaultTemplateParameters;
+ }
+
+ /**
+ * Create an entity.
+ *
+ * @param string $action
+ * @param Request $request
+ * @return object
+ */
+ protected function createEntity(string $action, Request $request): object
+ {
+ $type = $this->getEntityClass();
+
+ return new $type;
+ }
+
+ /**
+ * Get the template for the current crud.
+ *
+ * This template may be defined in configuration. If any template are
+ * defined, return the default template for the actions new, edit, index,
+ * and view.
+ *
+ * @param string $action
+ * @param mixed $entity the entity for the current request, or an array of entities
+ * @param Request $request
+ * @return string the path to the template
+ * @throws \LogicException if no template are available
+ */
+ protected function getTemplateFor($action, $entity, Request $request)
+ {
+ if ($this->hasCustomTemplate($action, $entity, $request)) {
+ return $this->getActionConfig($action)['template'];
+ }
+
+ switch ($action) {
+ case 'new':
+ return '@ChillMain/CRUD/new.html.twig';
+ case 'edit':
+ return '@ChillMain/CRUD/edit.html.twig';
+ case 'index':
+ return '@ChillMain/CRUD/index.html.twig';
+ case 'view':
+ return '@ChillMain/CRUD/view.html.twig';
+ case 'delete':
+ return '@ChillMain/CRUD/delete.html.twig';
+ default:
+ throw new \LogicException("the view for action $action is not "
+ . "defined. You should override ".__METHOD__." to add this "
+ . "action");
+ }
+ }
+
+ protected function hasCustomTemplate($action, $entity, Request $request): bool
+ {
+ return !empty($this->getActionConfig($action)['template']);
+ }
+
+ protected function onPreFlush(string $action, $entity, FormInterface $form, Request $request)
+ {
+ }
+
+ protected function onPostFlush(string $action, $entity, FormInterface $form, Request $request)
+ {
+ }
+
+ protected function onPrePersist(string $action, $entity, FormInterface $form, Request $request)
+ {
+ }
+
+ protected function onPostPersist(string $action, $entity, FormInterface $form, Request $request)
+ {
+ }
+
+ protected function onPostFetchEntity($action, Request $request, $entity): ?Response
+ {
+ return null;
+ }
+
+ protected function onPostCheckACL($action, Request $request, $entity): ?Response
+ {
+ return null;
+ }
+
+ protected function onFormValid(object $entity, FormInterface $form, Request $request)
+ {
+ }
+
+ /**
+ * Return a redirect response depending on the value of submit button.
+ *
+ * The handled values are :
+ *
+ * * save-and-close: return to index of current crud ;
+ * * save-and-new: return to new page of current crud ;
+ * * save-and-view: return to view page of current crud ;
+ *
+ * @param string $action
+ * @param mixed $entity
+ * @param FormInterface $form
+ * @param Request $request
+ * @return \Symfony\Component\HttpFoundation\RedirectResponse
+ */
+ protected function onBeforeRedirectAfterSubmission(string $action, $entity, FormInterface $form, Request $request)
+ {
+ $next = $request->request->get("submit", "save-and-close");
+
+ switch ($next) {
+ case "save-and-close":
+ return $this->redirectToRoute('chill_crud_'.$this->getCrudName().'_index');
+ case "save-and-new":
+ return $this->redirectToRoute('chill_crud_'.$this->getCrudName().'_new');
+ default:
+ return $this->redirectToRoute('chill_crud_'.$this->getCrudName().'_view', [
+ 'id' => $entity->getId()
+ ]);
+ }
+ }
+
+ protected function getActionConfig(string $action)
+ {
+ return $this->crudConfig['actions'][$action];
+ }
+
+ protected function getPaginatorFactory(): PaginatorFactory
+ {
+ return $this->get(PaginatorFactory::class);
+ }
+
+ protected function getTranslator(): TranslatorInterface
+ {
+ return $this->container->get('translator');
+ }
+
+ protected function getAuthorizationHelper(): AuthorizationHelper
+ {
+ return $this->container->get(AuthorizationHelper::class);
+ }
+
+ protected function getReachableCenters(Role $role, Scope $scope = null)
+ {
+ return $this->getAuthorizationHelper()
+ ->getReachableCenters($this->getUser(), $role, $scope)
+ ;
+ }
+
+ protected function getEventDispatcher(): EventDispatcherInterface
+ {
+ return $this->get(EventDispatcherInterface::class);
+ }
+
+ protected function getCrudResolver(): Resolver
+ {
+ return $this->get(Resolver::class);
+ }
+
+ public static function getSubscribedServices()
+ {
+ return \array_merge(
+ parent::getSubscribedServices(),
+ [
+ PaginatorFactory::class => PaginatorFactory::class,
+ 'translator' => TranslatorInterface::class,
+ AuthorizationHelper::class => AuthorizationHelper::class,
+ EventDispatcherInterface::class => EventDispatcherInterface::class,
+ Resolver::class => Resolver::class,
+ ]
+ );
+ }
+}
diff --git a/CRUD/Form/CRUDDeleteEntityForm.php b/CRUD/Form/CRUDDeleteEntityForm.php
new file mode 100644
index 000000000..609b56f58
--- /dev/null
+++ b/CRUD/Form/CRUDDeleteEntityForm.php
@@ -0,0 +1,33 @@
+
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+namespace Chill\MainBundle\CRUD\Form;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\Extension\Core\Type\HiddenType;
+
+
+/**
+ *
+ *
+ */
+class CRUDDeleteEntityForm extends AbstractType
+{
+}
diff --git a/CRUD/Resolver/Resolver.php b/CRUD/Resolver/Resolver.php
new file mode 100644
index 000000000..64aeb61bc
--- /dev/null
+++ b/CRUD/Resolver/Resolver.php
@@ -0,0 +1,102 @@
+
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+namespace Chill\MainBundle\CRUD\Resolver;
+
+use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Component\PropertyAccess\PropertyAccess;
+
+/**
+ *
+ *
+ */
+class Resolver
+{
+ /**
+ *
+ * @var EntityManagerInterface
+ */
+ protected $em;
+
+ /**
+ *
+ * @var \Symfony\Component\PropertyAccess\PropertyAccessor
+ */
+ protected $propertyAccess;
+
+ /**
+ *
+ * @var array
+ */
+ protected $crudConfig;
+
+ /**
+ * @deprecated
+ */
+ const ROLE_VIEW = 'role.view';
+
+ /**
+ * @deprecated
+ */
+ const ROLE_EDIT = 'role.edit';
+
+ /**
+ * The key to get the role necessary for the action
+ */
+ const ROLE = 'role';
+
+ function __construct(EntityManagerInterface $em, array $crudConfig)
+ {
+ $this->em = $em;
+
+ foreach($crudConfig as $conf) {
+ $this->crudConfig[$conf['name']] = $conf;
+ }
+ }
+
+ public function getConfigValue($key, $crudName, $action = null)
+ {
+ $config = $this->crudConfig[$crudName];
+
+ switch ($key) {
+ case self::ROLE:
+ return $config['actions'][$action]['role'] ?? $this->buildDefaultRole($crudName, $action);
+ }
+ }
+
+ public function buildDefaultRole($crudName, $action)
+ {
+ if (empty($this->crudConfig[$crudName]['base_role'])) {
+ throw new \LogicException(sprintf("the base role is not defined. You must define "
+ . "on or override %s or %s methods", __METHOD__, "getRoleFor"));
+ }
+
+ return \strtoupper(
+ $this->crudConfig[$crudName]['base_role'].
+ '_'.
+ $action);
+ }
+
+ public function hasAction($crudName, $action)
+ {
+ return \array_key_exists($action,
+ $this->crudConfig[$crudName]['actions']);
+ }
+}
diff --git a/CRUD/Routing/CRUDRoutesLoader.php b/CRUD/Routing/CRUDRoutesLoader.php
new file mode 100644
index 000000000..12f261d21
--- /dev/null
+++ b/CRUD/Routing/CRUDRoutesLoader.php
@@ -0,0 +1,78 @@
+
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+namespace Chill\MainBundle\CRUD\Routing;
+
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+
+/**
+ * Load the route for CRUD
+ *
+ */
+class CRUDRoutesLoader
+{
+ protected $config = [];
+
+ public function __construct($config)
+ {
+ $this->config = $config;
+ }
+
+ public function load()
+ {
+ $collection = new RouteCollection();
+
+ foreach ($this->config as $config) {
+ $collection->addCollection($this->loadConfig($config));
+ }
+
+ return $collection;
+ }
+
+ protected function loadConfig($config): RouteCollection
+ {
+ $collection = new RouteCollection();
+ foreach ($config['actions'] as $name => $action) {
+ $defaults = [
+ '_controller' => 'cscrud_'.$config['name'].'_controller'.':'.($action['controller_action'] ?? $name)
+ ];
+
+ if ($name === 'index') {
+ $path = "{_locale}".$config['base_path'];
+ $route = new Route($path, $defaults);
+ } elseif ($name === 'new') {
+ $path = "{_locale}".$config['base_path'].'/'.$name;
+ $route = new Route($path, $defaults);
+ } else {
+ $path = "{_locale}".$config['base_path'].($action['path'] ?? '/{id}/'.$name);
+ $requirements = $action['requirements'] ?? [
+ '{id}' => '\d+'
+ ];
+ $route = new Route($path, $defaults, $requirements);
+ }
+
+ $collection->add('chill_crud_'.$config['name'].'_'.$name, $route);
+ }
+
+ return $collection;
+ }
+}
diff --git a/CRUD/Templating/TwigCRUDResolver.php b/CRUD/Templating/TwigCRUDResolver.php
new file mode 100644
index 000000000..a593b08b0
--- /dev/null
+++ b/CRUD/Templating/TwigCRUDResolver.php
@@ -0,0 +1,66 @@
+
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+namespace Chill\MainBundle\CRUD\Templating;
+
+use Chill\MainBundle\CRUD\Resolver\Resolver;
+use Twig\TwigFilter;
+use Twig\TwigFunction;
+use Twig\Extension\AbstractExtension;
+use Twig\Environment;
+
+/**
+ * Twig filters to display data in crud template
+ *
+ */
+class TwigCRUDResolver extends AbstractExtension
+{
+ /**
+ *
+ * @var Resolver
+ */
+ protected $resolver;
+
+ function __construct(Resolver $resolver)
+ {
+ $this->resolver = $resolver;
+ }
+
+ public function getFunctions()
+ {
+ return [
+ new TwigFunction('chill_crud_config', [$this, 'getConfig'],
+ ['is_safe' => 'html']),
+ new TwigFunction('chill_crud_action_exists', [$this, 'hasAction'],
+ []),
+ ];
+ }
+
+ public function getConfig($configKey, $crudName, $action = null)
+ {
+ return $this->resolver->getConfigValue($configKey, $crudName, $action);
+ }
+
+ public function hasAction($crudName, $action)
+ {
+ return $this->resolver->hasAction($crudName, $action);
+ }
+
+}
diff --git a/ChillMainBundle.php b/ChillMainBundle.php
index ca6e60751..2b4b4649b 100644
--- a/ChillMainBundle.php
+++ b/ChillMainBundle.php
@@ -14,6 +14,7 @@ use Chill\MainBundle\DependencyInjection\CompilerPass\NotificationCounterCompile
use Chill\MainBundle\DependencyInjection\CompilerPass\MenuCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\ACLFlagsCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\GroupingCenterCompilerPass;
+use Chill\MainBundle\Templating\Entity\CompilerPass as RenderEntityCompilerPass;
class ChillMainBundle extends Bundle
@@ -31,5 +32,6 @@ class ChillMainBundle extends Bundle
$container->addCompilerPass(new MenuCompilerPass());
$container->addCompilerPass(new ACLFlagsCompilerPass());
$container->addCompilerPass(new GroupingCenterCompilerPass());
+ $container->addCompilerPass(new RenderEntityCompilerPass());
}
}
diff --git a/Controller/AdminCountryCRUDController.php b/Controller/AdminCountryCRUDController.php
new file mode 100644
index 000000000..310a36c60
--- /dev/null
+++ b/Controller/AdminCountryCRUDController.php
@@ -0,0 +1,20 @@
+paginatorFactory = $paginator;
+ }
+}
diff --git a/Controller/DefaultController.php b/Controller/DefaultController.php
index b10bf0d23..5909f5f1c 100644
--- a/Controller/DefaultController.php
+++ b/Controller/DefaultController.php
@@ -20,4 +20,62 @@ class DefaultController extends Controller
{
return $this->redirect($this->generateUrl('chill_main_homepage'));
}
-}
\ No newline at end of file
+
+ public function testAction()
+ {
+ return $this->render('ChillMainBundle:Tabs:index.html.twig', [
+ 'tabs' => [
+ 'test1' => [
+ [
+ 'name' => "Link 1",
+ 'content' => "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae auctor eu augue ut. Elementum nisi quis eleifend quam. Faucibus purus in massa tempor nec. Turpis massa sed elementum tempus egestas sed sed risus. Etiam sit amet nisl purus in mollis nunc sed id. Enim nunc faucibus a pellentesque sit amet porttitor eget. Risus nec feugiat in fermentum posuere. Augue mauris augue neque gravida. Sollicitudin aliquam ultrices sagittis orci a scelerisque purus semper eget. Id leo in vitae turpis massa sed elementum tempus egestas. Mauris commodo quis imperdiet massa. Fames ac turpis egestas integer eget aliquet nibh praesent. Urna porttitor rhoncus dolor purus non enim praesent elementum. Donec enim diam vulputate ut pharetra sit. Auctor neque vitae tempus quam. Mattis rhoncus urna neque viverra justo nec ultrices.",
+ ],
+ [
+ 'name' => "Link 2",
+ 'content' => "Dui sapien eget mi proin sed libero. Neque volutpat ac tincidunt vitae semper quis lectus nulla. Turpis nunc eget lorem dolor. Phasellus egestas tellus rutrum tellus. Diam sit amet nisl suscipit adipiscing bibendum est ultricies integer. Duis ultricies lacus sed turpis tincidunt id. Nisl suscipit adipiscing bibendum est ultricies integer. Elementum nibh tellus molestie nunc non blandit massa enim. Faucibus in ornare quam viverra orci sagittis eu. Neque volutpat ac tincidunt vitae semper quis lectus nulla. Accumsan sit amet nulla facilisi morbi. Leo vel fringilla est ullamcorper eget nulla facilisi etiam dignissim. Amet est placerat in egestas erat imperdiet sed euismod. Quis auctor elit sed vulputate mi. Mauris nunc congue nisi vitae suscipit tellus mauris a diam. At volutpat diam ut venenatis. Facilisis gravida neque convallis a cras semper.",
+ ],
+ [
+ 'name' => "Link 3",
+ 'content' => "In ornare quam viverra orci sagittis eu volutpat. Ac tincidunt vitae semper quis lectus nulla at volutpat. Placerat duis ultricies lacus sed turpis tincidunt. Augue interdum velit euismod in pellentesque. Felis eget nunc lobortis mattis aliquam. Volutpat lacus laoreet non curabitur gravida arcu. Gravida cum sociis natoque penatibus et magnis dis parturient montes. Nisl pretium fusce id velit ut tortor. Nunc scelerisque viverra mauris in aliquam sem fringilla ut. Magna eget est lorem ipsum dolor sit. Non consectetur a erat nam at lectus urna. Eget est lorem ipsum dolor sit amet consectetur adipiscing elit. Sed velit dignissim sodales ut.",
+ ],
+ [
+ 'name' => "Link 4",
+ 'content' => "Ut tellus elementum sagittis vitae et. Vitae purus faucibus ornare suspendisse sed nisi lacus sed viverra. Hendrerit gravida rutrum quisque non tellus orci ac auctor augue. Eleifend quam adipiscing vitae proin sagittis nisl rhoncus mattis rhoncus. Dictumst quisque sagittis purus sit. Suspendisse sed nisi lacus sed viverra. Pretium quam vulputate dignissim suspendisse in est ante. Id eu nisl nunc mi ipsum. Ut venenatis tellus in metus vulputate. Ut morbi tincidunt augue interdum velit euismod.",
+ ],
+ [
+ 'name' => "Link 5",
+ 'content' => "Vel elit scelerisque mauris pellentesque pulvinar. Ornare suspendisse sed nisi lacus sed viverra tellus. Massa tincidunt dui ut ornare lectus sit. Congue nisi vitae suscipit tellus mauris a diam. At auctor urna nunc id cursus metus aliquam. Viverra accumsan in nisl nisi scelerisque eu ultrices vitae. Mattis aliquam faucibus purus in massa tempor nec feugiat. Et leo duis ut diam quam. Auctor augue mauris augue neque. Purus ut faucibus pulvinar elementum integer enim neque volutpat. Scelerisque felis imperdiet proin fermentum leo. Diam sit amet nisl suscipit adipiscing bibendum est ultricies. Consectetur libero id faucibus nisl tincidunt. Vel fringilla est ullamcorper eget nulla facilisi. Pharetra diam sit amet nisl suscipit adipiscing. Dignissim diam quis enim lobortis. Auctor eu augue ut lectus arcu bibendum at varius.",
+ ]
+ ],
+ 'test2' => [
+ [
+ 'name' => "Link 1",
+ 'link' => "http://localhost",
+ 'content' => "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae auctor eu augue ut. Elementum nisi quis eleifend quam. Faucibus purus in massa tempor nec. Turpis massa sed elementum tempus egestas sed sed risus. Etiam sit amet nisl purus in mollis nunc sed id. Enim nunc faucibus a pellentesque sit amet porttitor eget. Risus nec feugiat in fermentum posuere. Augue mauris augue neque gravida. Sollicitudin aliquam ultrices sagittis orci a scelerisque purus semper eget. Id leo in vitae turpis massa sed elementum tempus egestas. Mauris commodo quis imperdiet massa. Fames ac turpis egestas integer eget aliquet nibh praesent. Urna porttitor rhoncus dolor purus non enim praesent elementum. Donec enim diam vulputate ut pharetra sit. Auctor neque vitae tempus quam. Mattis rhoncus urna neque viverra justo nec ultrices.",
+ ],
+ [
+ 'name' => "Link 2",
+ //'link' => "http://localhost",
+ 'content' => "Dui sapien eget mi proin sed libero. Neque volutpat ac tincidunt vitae semper quis lectus nulla. Turpis nunc eget lorem dolor. Phasellus egestas tellus rutrum tellus. Diam sit amet nisl suscipit adipiscing bibendum est ultricies integer. Duis ultricies lacus sed turpis tincidunt id. Nisl suscipit adipiscing bibendum est ultricies integer. Elementum nibh tellus molestie nunc non blandit massa enim. Faucibus in ornare quam viverra orci sagittis eu. Neque volutpat ac tincidunt vitae semper quis lectus nulla. Accumsan sit amet nulla facilisi morbi. Leo vel fringilla est ullamcorper eget nulla facilisi etiam dignissim. Amet est placerat in egestas erat imperdiet sed euismod. Quis auctor elit sed vulputate mi. Mauris nunc congue nisi vitae suscipit tellus mauris a diam. At volutpat diam ut venenatis. Facilisis gravida neque convallis a cras semper.",
+ ],
+ [
+ 'name' => "Link 3",
+ //'link' => "http://localhost",
+ 'content' => "In ornare quam viverra orci sagittis eu volutpat. Ac tincidunt vitae semper quis lectus nulla at volutpat. Placerat duis ultricies lacus sed turpis tincidunt. Augue interdum velit euismod in pellentesque. Felis eget nunc lobortis mattis aliquam. Volutpat lacus laoreet non curabitur gravida arcu. Gravida cum sociis natoque penatibus et magnis dis parturient montes. Nisl pretium fusce id velit ut tortor. Nunc scelerisque viverra mauris in aliquam sem fringilla ut. Magna eget est lorem ipsum dolor sit. Non consectetur a erat nam at lectus urna. Eget est lorem ipsum dolor sit amet consectetur adipiscing elit. Sed velit dignissim sodales ut.",
+ ],
+ [
+ 'name' => "Link 4",
+ 'link' => "http://localhost",
+ //'content' => "Ut tellus elementum sagittis vitae et. Vitae purus faucibus ornare suspendisse sed nisi lacus sed viverra. Hendrerit gravida rutrum quisque non tellus orci ac auctor augue. Eleifend quam adipiscing vitae proin sagittis nisl rhoncus mattis rhoncus. Dictumst quisque sagittis purus sit. Suspendisse sed nisi lacus sed viverra. Pretium quam vulputate dignissim suspendisse in est ante. Id eu nisl nunc mi ipsum. Ut venenatis tellus in metus vulputate. Ut morbi tincidunt augue interdum velit euismod.",
+ ],
+ [
+ 'name' => "Link 5",
+ //'link' => "http://localhost",
+ 'content' => "Vel elit scelerisque mauris pellentesque pulvinar. Ornare suspendisse sed nisi lacus sed viverra tellus. Massa tincidunt dui ut ornare lectus sit. Congue nisi vitae suscipit tellus mauris a diam. At auctor urna nunc id cursus metus aliquam. Viverra accumsan in nisl nisi scelerisque eu ultrices vitae. Mattis aliquam faucibus purus in massa tempor nec feugiat. Et leo duis ut diam quam. Auctor augue mauris augue neque. Purus ut faucibus pulvinar elementum integer enim neque volutpat. Scelerisque felis imperdiet proin fermentum leo. Diam sit amet nisl suscipit adipiscing bibendum est ultricies. Consectetur libero id faucibus nisl tincidunt. Vel fringilla est ullamcorper eget nulla facilisi. Pharetra diam sit amet nisl suscipit adipiscing. Dignissim diam quis enim lobortis. Auctor eu augue ut lectus arcu bibendum at varius.",
+ ]
+ ]
+ ]
+ ]);
+ }
+
+}
diff --git a/DependencyInjection/ChillMainExtension.php b/DependencyInjection/ChillMainExtension.php
index f4ec46fe8..d33ef4eb9 100644
--- a/DependencyInjection/ChillMainExtension.php
+++ b/DependencyInjection/ChillMainExtension.php
@@ -32,6 +32,8 @@ use Chill\MainBundle\Doctrine\DQL\JsonAggregate;
use Chill\MainBundle\Doctrine\DQL\JsonbExistsInArray;
use Chill\MainBundle\Doctrine\DQL\Similarity;
use Chill\MainBundle\Doctrine\DQL\OverlapsI;
+use Symfony\Component\DependencyInjection\Definition;
+use Symfony\Component\DependencyInjection\Reference;
use Chill\MainBundle\Doctrine\DQL\Replace;
/**
@@ -118,6 +120,8 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
$loader->load('services/phonenumber.yml');
$loader->load('services/cache.yml');
$loader->load('services/templating.yml');
+
+ $this->configureCruds($container, $config['cruds'], $loader);
}
public function getConfiguration(array $config, ContainerBuilder $container)
@@ -196,4 +200,54 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
'channels' => array('chill')
));
}
+
+ /**
+ *
+ * @param ContainerBuilder $container
+ * @param array $config the config under 'cruds' key
+ * @return null
+ */
+ protected function configureCruds(ContainerBuilder $container, $config, Loader\YamlFileLoader $loader)
+ {
+ if (count($config) === 0) {
+ return;
+ }
+
+ $loader->load('services/crud.yml');
+
+ $container->setParameter('chill_main_crud_route_loader_config', $config);
+
+ $definition = new Definition();
+ $definition
+ ->setClass(\Chill\MainBundle\CRUD\Routing\CRUDRoutesLoader::class)
+ ->addArgument('%chill_main_crud_route_loader_config%')
+ ;
+
+ $container->setDefinition('chill_main_crud_route_loader', $definition);
+
+ $alreadyExistingNames = [];
+
+ foreach ($config as $crudEntry) {
+ $controller = $crudEntry['controller'];
+ $controllerServiceName = 'cscrud_'.$crudEntry['name'].'_controller';
+ $name = $crudEntry['name'];
+
+ // check for existing crud names
+ if (\in_array($name, $alreadyExistingNames)) {
+ throw new LogicException(sprintf("the name %s is defined twice in CRUD", $name));
+ }
+
+ if (!$container->has($controllerServiceName)) {
+ $controllerDefinition = new Definition($controller);
+ $controllerDefinition->addTag('controller.service_arguments');
+ $controllerDefinition->setAutoconfigured(true);
+ $controllerDefinition->setClass($crudEntry['controller']);
+ $container->setDefinition($controllerServiceName, $controllerDefinition);
+ }
+
+ $container->setParameter('chill_main_crud_config_'.$name, $crudEntry);
+ $container->getDefinition($controllerServiceName)
+ ->addMethodCall('setCrudConfig', ['%chill_main_crud_config_'.$name.'%']);
+ }
+ }
}
diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php
index 7909bbd01..04a1fb87a 100644
--- a/DependencyInjection/Configuration.php
+++ b/DependencyInjection/Configuration.php
@@ -116,6 +116,58 @@ class Configuration implements ConfigurationInterface
->append($this->addWidgetsConfiguration('homepage', $this->containerBuilder))
->end() // end of widgets/children
->end() // end of widgets
+ ->arrayNode('cruds')
+ ->defaultValue([])
+ ->arrayPrototype()
+ ->children()
+ ->scalarNode('class')->cannotBeEmpty()->isRequired()->end()
+ ->scalarNode('controller')
+ ->cannotBeEmpty()
+ ->defaultValue(\Chill\MainBundle\CRUD\Controller\CRUDController::class)
+ ->end()
+ ->scalarNode('name')->cannotBeEmpty()->isRequired()->end()
+ ->scalarNode('base_path')->cannotBeEmpty()->isRequired()->end()
+ ->scalarNode('base_role')->defaultNull()->end()
+ ->scalarNode('form_class')->defaultNull()->end()
+ ->arrayNode('actions')
+ ->defaultValue([
+ 'edit' => [],
+ 'new' => []
+ ])
+ ->useAttributeAsKey('name')
+ ->arrayPrototype()
+ ->children()
+ ->scalarNode('controller_action')
+ ->defaultNull()
+ ->info('the method name to call in the route. Will be set to the action name if left empty.')
+ ->example("'action'")
+ ->end()
+ ->scalarNode('path')
+ ->defaultNull()
+ ->info('the path that will be **appended** after the base path. Do not forget to add '
+ . 'arguments for the method. Will be set to the action name, including an `{id}` '
+ . 'parameter if left empty.')
+ ->example('/{id}/my-action')
+ ->end()
+ ->arrayNode('requirements')
+ ->ignoreExtraKeys(false)
+ ->info('the requirements for the route. Will be set to `[ \'id\' => \'\d+\' ]` if left empty.')
+ ->end()
+ ->scalarNode('role')
+ ->defaultNull()
+ ->info('the role that will be required for this action. Override option `base_role`')
+ ->end()
+ ->scalarNode('template')
+ ->defaultNull()
+ ->info('the template to render the view')
+ ->end()
+ ->end()
+ ->end()
+ ->end()
+ ->end()
+ ->end()
+
+ ->end()
->end() // end of root/children
->end() // end of root
;
diff --git a/Phonenumber/PhonenumberHelper.php b/Phonenumber/PhonenumberHelper.php
index 4e06472d3..8b3df2894 100644
--- a/Phonenumber/PhonenumberHelper.php
+++ b/Phonenumber/PhonenumberHelper.php
@@ -20,6 +20,7 @@ namespace Chill\MainBundle\Phonenumber;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
+use GuzzleHttp\Exception\ConnectException;
use Psr\Log\LoggerInterface;
use Psr\Cache\CacheItemPoolInterface;
@@ -122,6 +123,24 @@ class PhonenumberHelper
return \in_array($validation, [ 'landline', 'voip' ]);
}
+ /**
+ * Return true if the phonenumber is a landline or voip phone. Return always false
+ * if the validation is not configured.
+ *
+ * @param string $phonenumber
+ * @return bool
+ */
+ public function isValidPhonenumberAny($phonenumber) : bool
+ {
+ $validation = $this->performTwilioLookup($phonenumber);
+
+ if (NULL === $validation) {
+ return false;
+ }
+
+ return \in_array($validation, [ 'landline', 'voip', 'mobile' ]);
+ }
+
public function format($phonenumber)
{
return $this->performTwilioFormat($phonenumber);
@@ -164,6 +183,14 @@ class PhonenumberHelper
"phonenumber" => $phonenumber
]);
+ return null;
+ } catch (ConnectException $e) {
+ $this->logger->error("[phonenumber helper] Could not format number "
+ . "due to connect error", [
+ "message" => $e->getMessage(),
+ "phonenumber" => $phonenumber
+ ]);
+
return null;
}
@@ -212,6 +239,14 @@ class PhonenumberHelper
"phonenumber" => $phonenumber
]);
+ return null;
+ } catch (ConnectException $e) {
+ $this->logger->error("[phonenumber helper] Could not format number "
+ . "due to connect error", [
+ "message" => $e->getMessage(),
+ "phonenumber" => $phonenumber
+ ]);
+
return null;
}
diff --git a/Phonenumber/Templating.php b/Phonenumber/Templating.php
index daaee8956..45bfceae0 100644
--- a/Phonenumber/Templating.php
+++ b/Phonenumber/Templating.php
@@ -47,6 +47,6 @@ class Templating extends AbstractExtension
public function formatPhonenumber($phonenumber)
{
- return $this->phonenumberHelper->format($phonenumber);
+ return $this->phonenumberHelper->format($phonenumber) ?? $phonenumber;
}
}
diff --git a/Resources/config/routing.yml b/Resources/config/routing.yml
index c6ab5918e..983e5237d 100644
--- a/Resources/config/routing.yml
+++ b/Resources/config/routing.yml
@@ -58,12 +58,6 @@ chill_main_admin_central:
chill_main_admin_permissions:
path: /{_locale}/admin/permissions
defaults: {_controller: ChillMainBundle:Admin:indexPermissions }
- options:
- menus:
- admin_section:
- order: 200
- label: Users and permissions
- icons: [key]
chill_main_search:
path: /{_locale}/search.{_format}
@@ -88,3 +82,7 @@ login_check:
logout:
path: /logout
+
+chill_main_test:
+ path: /{_locale}/main/test
+ defaults: { _controller: ChillMainBundle:Default:test }
\ No newline at end of file
diff --git a/Resources/config/services.yml b/Resources/config/services.yml
index 05807258b..010deb3ca 100644
--- a/Resources/config/services.yml
+++ b/Resources/config/services.yml
@@ -2,19 +2,6 @@ parameters:
# cl_chill_main.example.class: Chill\MainBundle\Example
services:
-
- twig_intl:
- class: Twig_Extensions_Extension_Intl
- tags:
- - { name: twig.extension }
-
- twig_date:
- class: Twig_Extensions_Extension_Date
- arguments:
- - "@translator"
- tags:
- - { name: twig.extension }
-
chill.main.helper.translatable_string:
class: Chill\MainBundle\Templating\TranslatableStringHelper
arguments:
diff --git a/Resources/config/services/crud.yml b/Resources/config/services/crud.yml
new file mode 100644
index 000000000..478122b73
--- /dev/null
+++ b/Resources/config/services/crud.yml
@@ -0,0 +1,16 @@
+services:
+# Chill\MainBundle\CRUD\Routing\CRUDRoutesLoader:
+#
+# tags:
+# - routing.loader
+
+ Chill\MainBundle\CRUD\Resolver\Resolver:
+ arguments:
+ $em: '@Doctrine\ORM\EntityManagerInterface'
+ $crudConfig: '%chill_main_crud_route_loader_config%'
+
+ Chill\MainBundle\CRUD\Templating\TwigCRUDResolver:
+ arguments:
+ $resolver: '@Chill\MainBundle\CRUD\Resolver\Resolver'
+ tags:
+ - { name: twig.extension }
\ No newline at end of file
diff --git a/Resources/config/services/menu.yml b/Resources/config/services/menu.yml
index b56c34d4e..ac331bd5a 100644
--- a/Resources/config/services/menu.yml
+++ b/Resources/config/services/menu.yml
@@ -10,3 +10,9 @@ services:
$authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'
tags:
- { name: 'chill.menu_builder' }
+
+ Chill\MainBundle\Routing\MenuBuilder\AdminSectionMenuBuilder:
+ arguments:
+ $authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'
+ tags:
+ - { name: 'chill.menu_builder' }
diff --git a/Resources/config/services/templating.yml b/Resources/config/services/templating.yml
index 99dfbe574..49836e3f9 100644
--- a/Resources/config/services/templating.yml
+++ b/Resources/config/services/templating.yml
@@ -1,4 +1,21 @@
services:
+ twig_intl:
+ class: Twig_Extensions_Extension_Intl
+ tags:
+ - { name: twig.extension }
+
+ twig_date:
+ class: Twig_Extensions_Extension_Date
+ arguments:
+ - "@translator"
+ tags:
+ - { name: twig.extension }
+
+ twig_text:
+ class: Twig_Extensions_Extension_Text
+ tags:
+ - { name: twig.extension }
+
Chill\MainBundle\Templating\ChillTwigHelper:
tags:
- { name: twig.extension }
@@ -7,5 +24,9 @@ services:
arguments:
$requestStack: '@Symfony\Component\HttpFoundation\RequestStack'
$originalExtension: '@twig.extension.routing'
+ tags:
+ - { name: twig.extension }
+
+ Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension:
tags:
- { name: twig.extension }
\ No newline at end of file
diff --git a/Resources/public/js/collection/collection.scss b/Resources/public/js/collection/collection.scss
index 13fc4a528..ba2f9ce9e 100644
--- a/Resources/public/js/collection/collection.scss
+++ b/Resources/public/js/collection/collection.scss
@@ -13,6 +13,10 @@ div.chill-collection {
li.chill-collection__list__entry:nth-last-child(1n+2) {
margin-bottom: 1rem;
}
+
+ button.chill-collection__list__remove-entry {
+ margin-left: 0.5rem;
+ }
}
button.chill-collection__button--add {
diff --git a/Resources/public/js/collection/collections.js b/Resources/public/js/collection/collections.js
index 60b34fb17..e8e697058 100644
--- a/Resources/public/js/collection/collections.js
+++ b/Resources/public/js/collection/collections.js
@@ -76,7 +76,7 @@ var initializeRemove = function(collection, entry) {
return;
}
- button.classList.add('sc-button', 'bt-delete');
+ button.classList.add('sc-button', 'bt-delete', 'chill-collection__list__remove-entry');
button.textContent = content;
button.addEventListener('click', function(e) {
diff --git a/Resources/public/main.js b/Resources/public/main.js
new file mode 100644
index 000000000..ffbe55c46
--- /dev/null
+++ b/Resources/public/main.js
@@ -0,0 +1,36 @@
+// import jQuery
+const $ = require('jquery');
+// create global $ and jQuery variables
+global.$ = global.jQuery = $;
+
+const moment = require('moment');
+global.moment = moment;
+
+const pikaday = require('pikaday-jquery');
+
+const select2 = require('select2');
+global.select2 = select2;
+
+// import js
+import {chill} from './js/chill.js';
+global.chill = chill;
+
+// css
+require('./sass/scratch.scss');
+require('./css/chillmain.css');
+require('./css/pikaday.css');
+require('./js/collection/collections.js');
+require('./modules/breadcrumb/index.js');
+require('./modules/download-report/index.js');
+//require('./css/scratch.css');
+//require('./css/select2/select2.css');
+require('select2/dist/css/select2.css');
+require('./modules/select_interactive_loading/index.js');
+require('./modules/export-list/export-list.scss');
+
+// img
+require('./img/favicon.ico');
+require('./img/logo-chill-sans-slogan_white.png');
+require('./img/logo-chill-outil-accompagnement_white.png');
+
+
diff --git a/Resources/public/modules/login_page/login.scss b/Resources/public/modules/login_page/login.scss
index e9662c5df..a451171b3 100644
--- a/Resources/public/modules/login_page/login.scss
+++ b/Resources/public/modules/login_page/login.scss
@@ -17,6 +17,10 @@ body {
position: relative;
height: 90%;
padding-top: 10%;
+ footer.footer {
+ position: absolute;
+ bottom: 0;
+ }
}
#content:before {
@@ -63,10 +67,10 @@ button {
p.forgot-password-link {
text-align: center;
-
+
a {
font-weight: 300;
color: #fff;
text-decoration: none;
}
-}
\ No newline at end of file
+}
diff --git a/Resources/public/modules/show_hide/index.js b/Resources/public/modules/show_hide/index.js
new file mode 100644
index 000000000..34f3b80b5
--- /dev/null
+++ b/Resources/public/modules/show_hide/index.js
@@ -0,0 +1 @@
+require("./show_hide.js");
\ No newline at end of file
diff --git a/Resources/public/modules/show_hide/show_hide.js b/Resources/public/modules/show_hide/show_hide.js
new file mode 100644
index 000000000..5f65e2b18
--- /dev/null
+++ b/Resources/public/modules/show_hide/show_hide.js
@@ -0,0 +1,126 @@
+/**
+ * Create a control to show or hide values
+ *
+ * Possible options are:
+ *
+ * - froms: an Element, an Array of Element, or a NodeList. A
+ * listener will be attached to **all** input of those elements
+ * and will trigger the check on changes
+ * - test: a function which will test the element and will return true
+ * if the content must be shown, false if it must be hidden.
+ * The function will receive the `froms` as first argument, and the
+ * event as second argument.
+ * - container: an Element, an Array of Element, or a Node List. The
+ * child nodes will be hidden / shown inside this container
+ * - event_name: the name of the event to listen to. `'change'` by default.
+ *
+ * @param object options
+ */
+var ShowHide = function(options) {
+ var
+ froms = typeof options.froms[Symbol.iterator] === "function" ? options.froms : [ options.froms ], //options.froms;
+ test = options.test,
+ container = typeof options.container[Symbol.iterator] === "function" ? options.container : [ options.container ],
+ is_shown = true,
+ event_name = 'event_name' in options ? options.event_name : 'change',
+ container_content = [],
+ debug = 'debug' in options ? options.debug : false,
+ load_event = 'load_event' in options ? options.load_event : 'load',
+ id = 'uid' in options ? options.id : Math.random();
+
+ var bootstrap = function(event) {
+ if (debug) {
+ console.log('debug is activated on this show-hide', this);
+ }
+ // keep the content in memory
+ for (let c of container.values()) {
+ let contents = [];
+ for (let el of c.childNodes.values()) {
+ contents.push(el);
+ }
+ container_content.push(contents);
+ }
+
+ // attach the listener on each input
+ for (let f of froms.values()) {
+ let inputs = f.querySelectorAll('input');
+ for (let input of inputs.values()) {
+ if (debug) {
+ console.log('attaching event to input', input);
+ }
+ input.addEventListener(event_name, function(e) {
+ onChange(e);
+ });
+ }
+ }
+
+ // first launch of the show/hide
+ onChange(event);
+ };
+
+
+ var onChange = function (event) {
+ var result = test(froms, event), me;
+
+ if (result === true) {
+ if (is_shown === false) {
+ forceShow();
+ me = new CustomEvent('show-hide-show', { detail: { id: id, container: container, froms: froms } });
+ window.dispatchEvent(me);
+ }
+ } else if (result === false) {
+ if (is_shown) {
+ forceHide();
+ me = new CustomEvent('show-hide-hide', { detail: { id: id, container: container, froms: froms } });
+ window.dispatchEvent(me);
+ }
+ } else {
+ throw "the result of test is not a boolean";
+ }
+
+ };
+
+ var forceHide = function() {
+ if (debug) {
+ console.log('force hide');
+ }
+ for (let contents of container_content.values()) {
+ for (let el of contents.values()) {
+ el.remove();
+ }
+ }
+ is_shown = false;
+ };
+
+ var forceShow = function() {
+ if (debug) {
+ console.log('show');
+ }
+ for (let i of container_content.keys()) {
+ var contents = container_content[i];
+ for (let el of contents.values()) {
+ container[i].appendChild(el);
+ }
+ }
+ is_shown = true;
+ };
+
+ var forceCompute = function(event) {
+ onChange(event);
+ };
+
+
+ if (load_event !== null) {
+ window.addEventListener('load', bootstrap);
+ } else {
+ bootstrap(null);
+ }
+
+ return {
+ forceHide: forceHide,
+ forceShow: forceShow,
+ forceCompute: forceCompute,
+ };
+};
+
+export {ShowHide};
diff --git a/Resources/public/modules/tabs/index.js b/Resources/public/modules/tabs/index.js
new file mode 100644
index 000000000..92ee9fd7b
--- /dev/null
+++ b/Resources/public/modules/tabs/index.js
@@ -0,0 +1,2 @@
+require("./tabs.js");
+require("./tabs.scss");
diff --git a/Resources/public/modules/tabs/tabs.js b/Resources/public/modules/tabs/tabs.js
new file mode 100644
index 000000000..dffea48aa
--- /dev/null
+++ b/Resources/public/modules/tabs/tabs.js
@@ -0,0 +1,118 @@
+/*
+ * Remove active class on both elements: link and content
+ */
+let resetActive = function(links, contents)
+{
+ for (items of [links, contents]) {
+ items.forEach(function(item) {
+ if (item.classList.contains('active')) {
+ item.classList.remove('active');
+ }
+ });
+ }
+}
+
+/*
+ * Count links array and return rank of given link
+ */
+let countNewActive = function(links, link)
+{
+ let rank = 0;
+ for (let i = 0; i < links.length; ++i) {
+ rank++;
+ if(links[i] == link) {
+ return rank;
+ }
+ }
+}
+
+/*
+ * Set class active on both new elements: link and content
+ */
+let setNewActive = function(links, contents, rank)
+{
+ if (! links[rank-1]) { rank = 1; }
+ link = links[rank-1];
+
+ link.classList.add('active');
+
+ count = 0;
+ contents.forEach(function(pane) {
+ count++;
+ if (rank == count) {
+ pane.classList.add('active');
+ }
+ });
+}
+
+/*
+ * Set height of content pane
+ */
+let setPaneHeight = function(contents)
+{
+ contents.forEach(function(pane) {
+
+ // let computedStyle = getComputedStyle(pane);
+ // console.log(computedStyle.height);
+ // comment prendre la hauteur d'une div masquée avec display:none
+ });
+}
+
+/*
+ * Check if links are defined in controller
+ * If true, disable javascript listener
+ */
+let isLinkRef = function(link) {
+
+ if (link.getAttribute('href') == "#") {
+
+ return false;
+ }
+ return true;
+
+}
+
+/*
+ * Main function
+ */
+window.addEventListener('load', function()
+{
+ tabParams.forEach(function(unit) {
+
+ let tabPanel = document.querySelector('#'+ unit.id );
+ if (tabPanel) {
+
+ let
+ nav = tabPanel.querySelector('nav'),
+ tabs = nav.querySelectorAll('ul.nav-tabs li.nav-item'),
+ links = nav.querySelectorAll('ul.nav-tabs li.nav-item a.nav-link'),
+ contents = tabPanel.querySelectorAll('div.tab-content div.tab-pane')
+ ;
+
+ if (unit.type == 'pill') {
+ tabPanel.classList.add('pills');
+ }
+
+ if (! unit.initPane) {
+ unit.initPane = 1;
+ }
+
+ setPaneHeight(contents);
+
+ // initial position
+ setNewActive(links, contents, unit.initPane);
+
+ // listen
+ links.forEach(function(link) {
+ if (isLinkRef(link) == false) {
+ link.addEventListener('click', function()
+ {
+ resetActive(links, contents);
+ setNewActive(links, contents, countNewActive(links, link));
+ });
+ }
+ });
+
+ }
+ });
+});
\ No newline at end of file
diff --git a/Resources/public/modules/tabs/tabs.scss b/Resources/public/modules/tabs/tabs.scss
new file mode 100644
index 000000000..96fce3f88
--- /dev/null
+++ b/Resources/public/modules/tabs/tabs.scss
@@ -0,0 +1,98 @@
+
+$navigation-color: #334d5c;
+$body-font-color: #ffffff;
+$tab-font-color: #495057;
+$border-color: #dee2e6;
+$pills-color: #ed9345;
+$radius : .25rem;
+
+div.tabs {
+ margin: 3em;
+
+ nav {
+ ul.nav-tabs {
+
+ display: flex;
+ display: -ms-flexbox;
+ flex-wrap: wrap;
+ -ms-flex-wrap: wrap;
+
+ list-style: none;
+ padding-left: 0;
+ margin-bottom: 0;
+
+ li.nav-item {
+ margin-bottom: -1px;
+
+ a.nav-link {
+ display: block;
+ padding: .5rem 1rem;
+ }
+ }
+
+ }
+ }
+
+ div.tab-content {
+
+ div.tab-pane {
+ display: none;
+
+ &.fade {
+ transition: opacity .15s linear;
+ }
+ &.active {
+ display: block;
+ }
+ }
+ }
+
+ &:not(.pills) {
+ nav {
+ ul.nav-tabs {
+ border-bottom: 1px solid $border-color;
+
+ li.nav-item {
+
+ a.nav-link {
+ border: 1px solid transparent;
+ border-top-left-radius: $radius;
+ border-top-right-radius: $radius;
+ color: $navigation-color;
+ }
+
+ &.show a.nav-link,
+ & a.nav-link.active {
+
+ color: $tab-font-color;
+ background-color: $body-font-color;
+ border-color: $border-color $border-color $body-font-color;
+
+ }
+ }
+ }
+ }
+ }
+
+ &.pills {
+ nav {
+ ul.nav-tabs {
+
+ border-bottom: 0;
+ li.nav-item {
+
+ & a.nav-link {
+ border: 0;
+ border-radius: $radius;
+ }
+
+ &.show > a.nav-link,
+ & a.nav-link.active {
+ color: $body-font-color;
+ background-color: $pills-color;
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Resources/public/sass/_custom.scss b/Resources/public/sass/_custom.scss
index 867d1b071..ee533a7ee 100644
--- a/Resources/public/sass/_custom.scss
+++ b/Resources/public/sass/_custom.scss
@@ -4,7 +4,6 @@
@import 'custom/fonts';
@import 'custom/timeline';
@import 'custom/mixins/entity';
-@import 'custom/activity';
@import 'custom/report';
@import 'custom/person';
@import 'custom/pagination';
diff --git a/Resources/public/sass/contrib/fontawesome/_path.scss b/Resources/public/sass/contrib/fontawesome/_path.scss
index bb457c23a..949ad920b 100644
--- a/Resources/public/sass/contrib/fontawesome/_path.scss
+++ b/Resources/public/sass/contrib/fontawesome/_path.scss
@@ -3,12 +3,12 @@
@font-face {
font-family: 'FontAwesome';
- src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}');
- src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'),
- url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'),
- url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'),
- url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'),
- url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg');
+ src: url('./../../../fonts/fontawesome-webfont.eot?v=#{$fa-version}');
+ src: url('./../../../fonts/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'),
+ url('./../../../fonts/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'),
+ url('./../../../fonts/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'),
+ url('./../../../fonts/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'),
+ url('./../../../fonts/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg');
// src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts
font-weight: normal;
font-style: normal;
diff --git a/Resources/public/sass/custom/_activity.scss b/Resources/public/sass/custom/_activity.scss
deleted file mode 100644
index 604bc3c7a..000000000
--- a/Resources/public/sass/custom/_activity.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-span.entity.entity-activity.activity-reason {
- @include entity($chill-pink, white);
-}
-
-.activity {
- color: $chill-green;
-}
diff --git a/Resources/public/sass/custom/_record_actions.scss b/Resources/public/sass/custom/_record_actions.scss
index 3cd9fc9a5..64ee66077 100644
--- a/Resources/public/sass/custom/_record_actions.scss
+++ b/Resources/public/sass/custom/_record_actions.scss
@@ -10,6 +10,7 @@ ul.record_actions, ul.record_actions_column {
display: flex;
justify-content: flex-end;
padding: 0.5em 0;
+ flex-wrap: wrap-reverse;
li {
display: inline-block;
diff --git a/Resources/public/sass/custom/mixins/entity.scss b/Resources/public/sass/custom/mixins/entity.scss
index d32570534..28cc74d06 100644
--- a/Resources/public/sass/custom/mixins/entity.scss
+++ b/Resources/public/sass/custom/mixins/entity.scss
@@ -1,6 +1,6 @@
@mixin entity($background-color, $color: white) {
font-variant: small-caps;
- display: inline;
+ display: inline-block;
padding: .2em .6em .3em;
font-size: 88%;
font-weight: bold;
@@ -11,5 +11,6 @@
border-radius: .25em;
color: $color;
background-color: $background-color;
+ margin: 0.5em;
}
diff --git a/Resources/public/sass/custom/modules/_buttons.scss b/Resources/public/sass/custom/modules/_buttons.scss
index b465742f7..e070dcd57 100644
--- a/Resources/public/sass/custom/modules/_buttons.scss
+++ b/Resources/public/sass/custom/modules/_buttons.scss
@@ -1,7 +1,7 @@
.sc-button {
margin-bottom: 0.5rem;
- &.bt-submit, &.bt-save, &.bt-create, &.bt-new {
+ &.bt-submit, &.bt-save, &.bt-create, &.bt-new, &.bt-duplicate {
@include button($green, $white);
}
@@ -19,6 +19,7 @@
&:not(.change-icon) {
+ // icons using font-awesome "old way"
&.bt-create::before,
&.bt-save::before,
&.bt-new::before,
@@ -31,6 +32,14 @@
font: normal normal normal 14px/1 FontAwesome;
margin-right: 0.5em;
}
+
+ // icons using font-awesome "new svg way"
+ &.bt-duplicate::before {
+ display: inline-block;
+ width: 1em;
+ margin-right: 0.5em;
+ vertical-align: middle;
+ }
&.bt-save::before {
// add a floppy
@@ -60,6 +69,10 @@
&.bt-show::before, &.bt-view::before {
content: "";
}
+
+ &.bt-duplicate::before {
+ content: url("./copy-solid.svg");
+ }
}
> i.fa {
diff --git a/Resources/public/sass/custom/modules/copy-solid.svg b/Resources/public/sass/custom/modules/copy-solid.svg
new file mode 100644
index 000000000..6acdf87cd
--- /dev/null
+++ b/Resources/public/sass/custom/modules/copy-solid.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/Resources/public/sass/modules/_forms.scss b/Resources/public/sass/modules/_forms.scss
index 970d33a48..b01c802b6 100644
--- a/Resources/public/sass/modules/_forms.scss
+++ b/Resources/public/sass/modules/_forms.scss
@@ -25,10 +25,17 @@ label {
abbr {
display: none;
}
+
+ // mark the label for empty placeholder
+ &[for$="_placeholder"] {
+ font-style: italic;
+ }
}
.inline-choice {
white-space:nowrap;
+ display: inline-block;
+
label {
white-space:normal;
display: inline;
@@ -38,6 +45,11 @@ label {
}
}
+div.choice-widget-expanded {
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+}
+
textarea,
//input,
#{$all-text-inputs},
diff --git a/Resources/translations/messages.fr.yml b/Resources/translations/messages.fr.yml
index 6b7cda525..de70316fe 100644
--- a/Resources/translations/messages.fr.yml
+++ b/Resources/translations/messages.fr.yml
@@ -34,6 +34,7 @@ Save: Enregistrer
This form contains errors: Ce formulaire contient des erreurs
Choose an user: Choisir un utilisateur
'You are going to leave a page with unsubmitted data. Are you sure you want to leave ?': "Vous allez quitter la page alors que des données n'ont pas été enregistrées. Êtes vous sûr de vouloir partir ?"
+No value: Aucune information
Edit: Modifier
Update: Mettre à jour
@@ -228,4 +229,27 @@ Log in with your new password: Connectez-vous avec votre nouveau mot de passe
# impersonate
Exit impersonation: Retour Administrateur
Impersonate: Mode fantôme
-Impersonate mode: Mode fantôme
\ No newline at end of file
+Impersonate mode: Mode fantôme
+
+crud:
+ # general items
+ new:
+ button_action_form: Créer
+ link_edit: Modifier
+ save_and_close: Créer & fermer
+ save_and_show: Créer & voir
+ save_and_new: Créer & nouveau
+ success: Les données ont été créées
+ edit:
+ button_action_form: Enregistrer
+ back_to_view: Voir
+ save_and_close: Enregistrer & fermer
+ save_and_show: Enregistrer & voir
+ success: Les données ont été modifiées
+ delete:
+ success: Les données ont été supprimées
+ link_to_form: Supprimer
+ default:
+ success: Les données ont été enregistrées
+ view:
+ link_duplicate: Dupliquer
diff --git a/Resources/translations/validators.fr.yml b/Resources/translations/validators.fr.yml
index ab3596c79..766bee9da 100644
--- a/Resources/translations/validators.fr.yml
+++ b/Resources/translations/validators.fr.yml
@@ -17,3 +17,4 @@ This username or email does not exists: Cet utilisateur ou email n'est pas prés
#phonenumber
This is not a landline phonenumber: Ce numéro n'est pas une ligne fixe valide
This is not a mobile phonenumber: Ce numéro n'est pas un numéro de portable valide
+This is not a valid phonenumber: Ce numéro de téléphone n'est pas valide
diff --git a/Resources/views/CRUD/_delete_content.html.twig b/Resources/views/CRUD/_delete_content.html.twig
new file mode 100644
index 000000000..5baba11ea
--- /dev/null
+++ b/Resources/views/CRUD/_delete_content.html.twig
@@ -0,0 +1,37 @@
+
+ {% block crud_content_header %}
+
{{ ('crud.'~crud_name~'.title_delete')|trans({ '%as_string%': entity|chill_entity_render_string }) }}
+ {% endblock crud_content_header %}
+
+
{{ ('crud.'~crud_name~'.confirm_message_delete')|trans({ '%as_string%': entity|chill_entity_render_string }) }}
+
+ {{ form_start(form) }}
+
+
+ {% block content_form_actions_back %}
+
+
+ {{ 'Cancel'|trans }}
+
+
+ {% endblock %}
+ {% block content_form_actions_before %}{% endblock %}
+ {% block content_form_actions_view %}
+ {% if is_granted(chill_crud_config('role', crud_name, 'view'), entity) %}
+
+
+ {{ 'crud.edit.back_to_view'|trans }}
+
+
+ {% endif %}
+ {% endblock %}
+ {% block content_form_actions_confirm_delete %}
+
+ {{ ('crud.'~crud_name~'.button_delete')|trans }}
+
+ {% endblock content_form_actions_confirm_delete %}
+ {% block content_form_actions_after %}{% endblock %}
+
+
+ {{ form_end(form) }}
+
diff --git a/Resources/views/CRUD/_edit_content.html.twig b/Resources/views/CRUD/_edit_content.html.twig
new file mode 100644
index 000000000..77b794ce9
--- /dev/null
+++ b/Resources/views/CRUD/_edit_content.html.twig
@@ -0,0 +1,63 @@
+
+ {% block crud_content_header %}
+
{{ ('crud.'~crud_name~'.title_edit')|trans }}
+ {% endblock crud_content_header %}
+
+ {% block crud_content_form %}
+ {{ form_start(form) }}
+
+ {% block crud_content_form_rows %}
+ {% for f in form %}
+ {{ form_row(f) }}
+ {% endfor %}
+ {% endblock crud_content_form_rows %}
+
+ {% block crud_content_form_actions %}
+
+ {% endblock %}
+
+ {{ form_end(form) }}
+ {% endblock %}
+
diff --git a/Resources/views/CRUD/_edit_title.html.twig b/Resources/views/CRUD/_edit_title.html.twig
new file mode 100644
index 000000000..7561ceda4
--- /dev/null
+++ b/Resources/views/CRUD/_edit_title.html.twig
@@ -0,0 +1 @@
+{{ ('crud.'~crud_name~'.title_edit')|trans }}
diff --git a/Resources/views/CRUD/_inc/default.html.twig b/Resources/views/CRUD/_inc/default.html.twig
new file mode 100644
index 000000000..0e9d6dd4c
--- /dev/null
+++ b/Resources/views/CRUD/_inc/default.html.twig
@@ -0,0 +1 @@
+{{ data }}
\ No newline at end of file
diff --git a/Resources/views/CRUD/_index.html.twig b/Resources/views/CRUD/_index.html.twig
new file mode 100644
index 000000000..4e553d962
--- /dev/null
+++ b/Resources/views/CRUD/_index.html.twig
@@ -0,0 +1,48 @@
+
+
+{% block index_header %}
+
{{ ('crud.' ~ crud_name ~ '.index.title')|trans({'%crud_name%': crud_name}) }}
+{% endblock index_header %}
+
+{% if entities|length == 0 %}
+ {% block no_existing_entities %}
+
{{ no_existing_entities_sentences|default('No entities')|trans }}
+ {% endblock %}
+{% else %}
+ {% block table_entities %}
+
+
+
+ {% block table_entities_thead_tr %}
+ id
+ {% endblock %}
+
+
+
+ {% block table_entities_tbody %}
+ {% for entity in entities %}
+
+ {{ entity.id }}
+
+ {% endfor %}
+ {% endblock %}
+
+
+ {% endblock %}
+
+{% endif %}
+
+
+
+{% block list_actions %}
+
+{% endblock list_actions %}
+
diff --git a/Resources/views/CRUD/_new_content.html.twig b/Resources/views/CRUD/_new_content.html.twig
new file mode 100644
index 000000000..93609d8d1
--- /dev/null
+++ b/Resources/views/CRUD/_new_content.html.twig
@@ -0,0 +1,50 @@
+
+ {% block crud_content_header %}
+
{{ ('crud.' ~ crud_name ~ '.title_new')|trans({'%crud_name%' : crud_name }) }}
+ {% endblock crud_content_header %}
+
+ {% block crud_content_form %}
+ {{ form_start(form) }}
+
+ {% block crud_content_form_rows %}
+ {% for f in form if f.vars.name != 'submit' %}
+ {{ form_row(f) }}
+ {% endfor %}
+ {% endblock crud_content_form_rows %}
+
+ {% block crud_content_form_actions %}
+
+ {% endblock %}
+
+ {{ form_end(form) }}
+ {% endblock %}
+
diff --git a/Resources/views/CRUD/_new_title.html.twig b/Resources/views/CRUD/_new_title.html.twig
new file mode 100644
index 000000000..ebd2121a6
--- /dev/null
+++ b/Resources/views/CRUD/_new_title.html.twig
@@ -0,0 +1 @@
+{{ ('crud.' ~ crud_name ~ '.title_new')|trans({'%crud_name%' : crud_name }) }}
\ No newline at end of file
diff --git a/Resources/views/CRUD/_view_content.html.twig b/Resources/views/CRUD/_view_content.html.twig
new file mode 100644
index 000000000..f56a00e1b
--- /dev/null
+++ b/Resources/views/CRUD/_view_content.html.twig
@@ -0,0 +1,63 @@
+
+ {% block crud_content_header %}
+
{{ 'crud.%crud_name%.title_view'|trans({'%crud_name%' : crud_name }) }}
+ {% endblock crud_content_header %}
+
+ {% block crud_content_view %}
+
+ {% block crud_content_view_details %}
+
+ id
+ {{ entity.id|default("No id") }}
+
+ {% endblock crud_content_view_details %}
+
+ {% block crud_content_view_actions %}
+
+ {% endblock crud_content_view_actions %}
+
+ {% endblock crud_content_view %}
+
diff --git a/Resources/views/CRUD/_view_title.html.twig b/Resources/views/CRUD/_view_title.html.twig
new file mode 100644
index 000000000..3473dd298
--- /dev/null
+++ b/Resources/views/CRUD/_view_title.html.twig
@@ -0,0 +1 @@
+{{ 'crud.%crud_name%.title_view'|trans({'%crud_name%' : crud_name }) }}
\ No newline at end of file
diff --git a/Resources/views/CRUD/delete.html.twig b/Resources/views/CRUD/delete.html.twig
new file mode 100644
index 000000000..ab440a602
--- /dev/null
+++ b/Resources/views/CRUD/delete.html.twig
@@ -0,0 +1,8 @@
+{% extends '@ChillMain/layout.html.twig' %}
+
+{% block title %}{{ ('crud.' ~ crud_name ~ '.delete.title')|trans({'%crud_name%': crud_name}) }}{% endblock %}
+
+{% block content %}
+ {% embed '@ChillMain/CRUD/_delete_content.html.twig' %}
+ {% endembed %}
+{% endblock content %}
\ No newline at end of file
diff --git a/Resources/views/CRUD/edit.html.twig b/Resources/views/CRUD/edit.html.twig
new file mode 100644
index 000000000..ecd68ef02
--- /dev/null
+++ b/Resources/views/CRUD/edit.html.twig
@@ -0,0 +1,10 @@
+{% extends '@ChillMain/layout.html.twig' %}
+
+{% block title %}
+{% include('@ChillMain/CRUD/_edit_title.html.twig') %}
+{% endblock %}
+
+{% block content %}
+{% embed '@ChillMain/CRUD/_edit_content.html.twig' %}
+{% endembed %}
+{% endblock %}
diff --git a/Resources/views/CRUD/index.html.twig b/Resources/views/CRUD/index.html.twig
new file mode 100644
index 000000000..00608131d
--- /dev/null
+++ b/Resources/views/CRUD/index.html.twig
@@ -0,0 +1,8 @@
+{% extends '@ChillMain/layout.html.twig' %}
+
+{% block title %}{{ ('crud.' ~ crud_name ~ '.index.title')|trans({'%crud_name%': crud_name}) }}{% endblock %}
+
+{% block content %}
+ {% embed '@ChillMain/CRUD/_index.html.twig' %}
+ {% endembed %}
+{% endblock content %}
\ No newline at end of file
diff --git a/Resources/views/CRUD/new.html.twig b/Resources/views/CRUD/new.html.twig
new file mode 100644
index 000000000..61b5d0b16
--- /dev/null
+++ b/Resources/views/CRUD/new.html.twig
@@ -0,0 +1,10 @@
+{% extends '@ChillMain/layout.html.twig' %}
+
+{% block title %}
+{% include('@ChillMain/CRUD/_new_title.html.twig') %}
+{% endblock %}
+
+{% block content %}
+{% embed '@ChillMain/CRUD/_new_content.html.twig' %}
+{% endembed %}
+{% endblock %}
diff --git a/Resources/views/CRUD/view.html.twig b/Resources/views/CRUD/view.html.twig
new file mode 100644
index 000000000..f32234a33
--- /dev/null
+++ b/Resources/views/CRUD/view.html.twig
@@ -0,0 +1,10 @@
+{% extends '@ChillMain/layout.html.twig' %}
+
+{% block title %}
+{% include('@ChillMain/CRUD/_view_title.html.twig') %}
+{% endblock %}
+
+{% block content %}
+{% embed '@ChillMain/CRUD/_view_content.html.twig' %}
+{% endembed %}
+{% endblock %}
diff --git a/Resources/views/Extensions/PrintOrMessage/blockquote_date.html.twig b/Resources/views/Extensions/PrintOrMessage/blockquote_date.html.twig
new file mode 100644
index 000000000..3a37a6850
--- /dev/null
+++ b/Resources/views/Extensions/PrintOrMessage/blockquote_date.html.twig
@@ -0,0 +1 @@
+{% if value is not empty %}{{ value|localizeddate(date_format, time_format) }} {% else %}{{ message|trans }} {% endif %}
\ No newline at end of file
diff --git a/Resources/views/Extensions/PrintOrMessage/default_date.html.twig b/Resources/views/Extensions/PrintOrMessage/default_date.html.twig
new file mode 100644
index 000000000..e2f50b083
--- /dev/null
+++ b/Resources/views/Extensions/PrintOrMessage/default_date.html.twig
@@ -0,0 +1 @@
+{% if value is not empty %}{{ value|localizeddate(date_format, time_format) }}{% else %}{{ message|trans }} {% endif %}
\ No newline at end of file
diff --git a/Resources/views/Form/fields.html.twig b/Resources/views/Form/fields.html.twig
index 841dd72e2..97e2896dc 100644
--- a/Resources/views/Form/fields.html.twig
+++ b/Resources/views/Form/fields.html.twig
@@ -52,7 +52,7 @@
{% block choice_widget_expanded %}
{% spaceless %}
-