From ec95faded05f04cadbfd2bf608e92365505455ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9C=C3=A9?= Date: Wed, 17 Oct 2018 10:09:54 +0200 Subject: [PATCH] Adds readonly compatibility with Doctrine Mongo ODM documents --- README.md | 12 +- TODO_mongo_odm.md | 12 ++ composer.json | 6 +- docker/phpunit/Dockerfile | 6 + .../EmbeddedListViewConfigPass.php | 62 +++--- .../ListFormFiltersConfigPass.php | 59 +++--- src/Configuration/ShortFormTypeConfigPass.php | 15 +- src/Configuration/ShowViewConfigPass.php | 78 ++++--- src/Controller/EasyAdminController.php | 1 + .../MongoOdmEasyAdminController.php | 87 ++++++++ .../Compiler/MongoOdmPass.php | 22 ++ .../Compiler/TwigPathPass.php | 39 ++++ src/EasyAdminExtensionBundle.php | 8 + .../AbstractPostQueryBuilderSubscriber.php | 142 +++++++++++++ .../MongoOdmPostQueryBuilderSubscriber.php | 100 +++++++++ .../PostQueryBuilderSubscriber.php | 98 +-------- src/Form/Type/EasyAdminEmbeddedListType.php | 50 ++++- src/Resources/config/services.xml | 18 +- .../public/js/autocomplete-create.js | 20 +- .../views/default/embedded_list.html.twig | 190 +++++++++--------- .../default/field_embedded_list.html.twig | 11 +- .../views/default/includes/_actions.html.twig | 4 +- src/Resources/views/default/layout.html.twig | 8 +- src/Resources/views/default/list.html.twig | 22 +- .../views/default/new_ajax.html.twig | 14 +- src/Resources/views/default/show.html.twig | 17 +- .../views/default/show_vertical.html.twig | 5 + .../views/form/bootstrap_4.html.twig | 12 +- .../includes/_create_entity_modal.html.twig | 2 +- .../includes/_include_embedded_list.html.twig | 5 +- src/Security/AdminAuthorizationChecker.php | 24 +-- src/Twig/AdminAuthorizationExtension.php | 11 +- src/Twig/EasyAdminExtensionTwigExtension.php | 176 ++++++++++++++++ src/Twig/EmbeddedListExtension.php | 4 +- .../CustomFormTypeConfigPassTest.php | 48 +++++ .../EmbeddedListViewConfigPassTest.php | 27 +++ .../ListFormFiltersConfigPassTest.php | 62 ++++++ .../Configuration/ShowViewConfigPassTest.php | 18 +- tests/Controller/MongoOdmCompatTest.php | 45 +++++ tests/Controller/UserRolesTest.php | 16 +- tests/Fixtures/AbstractTestCase.php | 24 +++ tests/Fixtures/App/AppKernel.php | 23 ++- .../App/config/config_mongo_odm_compat.yaml | 47 +++++ .../App/config/routing_mongo_odm.yaml | 9 + .../Document/FunctionalTests/RequestLog.php | 61 ++++++ 45 files changed, 1348 insertions(+), 372 deletions(-) create mode 100644 TODO_mongo_odm.md create mode 100644 src/Controller/MongoOdmEasyAdminController.php create mode 100644 src/DependencyInjection/Compiler/MongoOdmPass.php create mode 100644 src/DependencyInjection/Compiler/TwigPathPass.php create mode 100644 src/EventListener/AbstractPostQueryBuilderSubscriber.php create mode 100644 src/EventListener/MongoOdmPostQueryBuilderSubscriber.php create mode 100644 src/Twig/EasyAdminExtensionTwigExtension.php create mode 100644 tests/Configuration/ListFormFiltersConfigPassTest.php create mode 100644 tests/Controller/MongoOdmCompatTest.php create mode 100644 tests/Fixtures/App/config/config_mongo_odm_compat.yaml create mode 100644 tests/Fixtures/App/config/routing_mongo_odm.yaml create mode 100644 tests/Fixtures/AppTestBundle/Document/FunctionalTests/RequestLog.php diff --git a/README.md b/README.md index c58414b4..1920e9da 100644 --- a/README.md +++ b/README.md @@ -233,20 +233,20 @@ easy_admin_extension: #### Options -Embedded lists are useful to show relations to en entity in its *NEW/EDIT/FORM* or *SHOW* view. It relies on the *LIST* view of the related entities you want to embed in the parent EDIT/SHOW view. Options must be defined in `type_options` key for a *NEW/EDIT/FORM* view, or in `template_options` for a *SHOW* view. +Embedded lists are useful to show relations to an object in its *NEW/EDIT/FORM* or *SHOW* view. It relies on the *LIST* view of the related objects you want to embed in the parent EDIT/SHOW view. Options must be defined in `type_options` key for a *NEW/EDIT/FORM* view, or in `template_options` for a *SHOW* view. Available options are : -- `entity`: Entity config name (key under the EasyAdmin `entities` config) +- `entity`/`document`: Entity/Document config name (key under the EasyAdmin `entities`/`documents` config) - `ext_filters`: Request filters to apply on the list - `hidden_fields`: List of fields (columns) to hide from list fields config - `max_results`: Number of items par page (list.max_results config is used if not defined) - `sort`: Sort to apply -- `parent_entity_fqcn`: Parent entity FQCN in order to guess default filters (only when embedded in *SHOW* view, almost never required) -- `parent_entity_property`: Matching property name on parent entity FQCN (only when embedded in *SHOW* view, if `property` is not an ORM field) -- `entity_fqcn`: Listed entities FQCN in order to guess default filters (only when embedded in *SHOW* view, almost never required) +- `parent_object_fqcn`: Parent object FQCN in order to guess default filters (only when embedded in *SHOW* view, almost never required) +- `parent_object_property`: Matching property name on parent object FQCN (only when embedded in *SHOW* view, if `property` is not an ORM/ODM field) +- `object_fqcn`: Listed entities FQCN in order to guess default filters (only when embedded in *SHOW* view, almost never required) -#### Options guesser based on ORM metadata +#### Options guesser based on ORM metadata (for entities only) Service EmbeddedListHelper is intended to guess `entity` entry for embedded_list. It's reads ORM metadata, based on parent entity (the one that embeds the list) and property name. diff --git a/TODO_mongo_odm.md b/TODO_mongo_odm.md new file mode 100644 index 00000000..e62ff089 --- /dev/null +++ b/TODO_mongo_odm.md @@ -0,0 +1,12 @@ +# Utiliser les functions Twig object + +# Menu entries (entry type = document) + +# Passer les configPass en mongo_odm + - EmbeddedListViewConfigPass: OK + - ExcludeFieldsConfigPass + - ListFormFiltersConfigPass OK + - ShortFormTypeConfigPass: OK + - ShowViewConfigPass: ? + +# Tests ! diff --git a/composer.json b/composer.json index 7e5282ca..dc45592d 100644 --- a/composer.json +++ b/composer.json @@ -32,8 +32,9 @@ "twig/twig": "^2.11.3|^3.0" }, "require-dev": { - "doctrine/data-fixtures": "^1.3", + "alterphp/easyadmin-mongo-odm-bundle": "dev-master", "doctrine/doctrine-fixtures-bundle": "^3.0", + "doctrine/mongodb-odm-bundle": "^4.0", "friendsofphp/php-cs-fixer": "^2.11", "php-coveralls/php-coveralls": "^2.0", "phpstan/phpstan": "dev-master", @@ -58,7 +59,8 @@ "sort-packages": true, "platform": { "php": "7.1.3", - "ext-intl": "1.0" + "ext-intl": "1.0", + "ext-mongodb": "1.6.1" } }, "autoload": { diff --git a/docker/phpunit/Dockerfile b/docker/phpunit/Dockerfile index 2913a9f6..084ac19b 100644 --- a/docker/phpunit/Dockerfile +++ b/docker/phpunit/Dockerfile @@ -5,5 +5,11 @@ RUN apt-get update && \ RUN curl --silent --show-error https://getcomposer.org/installer | php +RUN docker-php-ext-install -j$(nproc) mysqli + +# Install MONGO +RUN pecl install mongodb \ + && docker-php-ext-enable mongodb + # RUN apk --no-cache add php7-iconv # RUN apk --no-cache add php7-simplexml diff --git a/src/Configuration/EmbeddedListViewConfigPass.php b/src/Configuration/EmbeddedListViewConfigPass.php index 88ace833..cf78bd10 100644 --- a/src/Configuration/EmbeddedListViewConfigPass.php +++ b/src/Configuration/EmbeddedListViewConfigPass.php @@ -5,8 +5,8 @@ use EasyCorp\Bundle\EasyAdminBundle\Configuration\ConfigPassInterface; /** - * Initializes the configuration for all the views of each entity, which is - * needed when some entity relies on the default configuration for some view. + * Initializes the configuration for all the views of each object of type "%s", which is + * needed when some object of type "%s" relies on the default configuration for some view. */ class EmbeddedListViewConfigPass implements ConfigPassInterface { @@ -31,9 +31,13 @@ public function process(array $backendConfig) */ private function processOpenNewTabConfig(array $backendConfig) { - foreach ($backendConfig['entities'] as $entityName => $entityConfig) { - if (!isset($entityConfig['embeddedList']['open_new_tab'])) { - $backendConfig['entities'][$entityName]['embeddedList']['open_new_tab'] = $this->defaultOpenNewTab; + foreach (['entities', 'documents'] as $objectTypeRootKey) { + if (isset($backendConfig[$objectTypeRootKey]) && \is_array($backendConfig[$objectTypeRootKey])) { + foreach ($backendConfig[$objectTypeRootKey] as $objectName => $objectConfig) { + if (!isset($objectConfig['embeddedList']['open_new_tab'])) { + $backendConfig[$objectTypeRootKey][$objectName]['embeddedList']['open_new_tab'] = $this->defaultOpenNewTab; + } + } } } @@ -45,25 +49,29 @@ private function processOpenNewTabConfig(array $backendConfig) */ private function processSortingConfig(array $backendConfig) { - foreach ($backendConfig['entities'] as $entityName => $entityConfig) { - if ( - !isset($entityConfig['embeddedList']['sort']) - && isset($entityConfig['list']['sort']) - ) { - $backendConfig['entities'][$entityName]['embeddedList']['sort'] = $entityConfig['list']['sort']; - } elseif (isset($entityConfig['embeddedList']['sort'])) { - $sortConfig = $entityConfig['embeddedList']['sort']; - if (!\is_string($sortConfig) && !\is_array($sortConfig)) { - throw new \InvalidArgumentException(\sprintf('The "sort" option of the "embeddedList" view of the "%s" entity contains an invalid value (it can only be a string or an array).', $entityName)); - } + foreach (['entities', 'documents'] as $objectTypeRootKey) { + if (isset($backendConfig[$objectTypeRootKey]) && \is_array($backendConfig[$objectTypeRootKey])) { + foreach ($backendConfig[$objectTypeRootKey] as $objectName => $objectConfig) { + if ( + !isset($objectConfig['embeddedList']['sort']) + && isset($objectConfig['list']['sort']) + ) { + $backendConfig[$objectTypeRootKey][$objectName]['embeddedList']['sort'] = $objectConfig['list']['sort']; + } elseif (isset($objectConfig['embeddedList']['sort'])) { + $sortConfig = $objectConfig['embeddedList']['sort']; + if (!\is_string($sortConfig) && !\is_array($sortConfig)) { + throw new \InvalidArgumentException(\sprintf('The "sort" option of the "embeddedList" view of the "%s" object contains an invalid value (it can only be a string or an array).', $objectName)); + } - if (\is_string($sortConfig)) { - $sortConfig = ['field' => $sortConfig, 'direction' => 'DESC']; - } else { - $sortConfig = ['field' => $sortConfig[0], 'direction' => \strtoupper($sortConfig[1])]; - } + if (\is_string($sortConfig)) { + $sortConfig = ['field' => $sortConfig, 'direction' => 'DESC']; + } else { + $sortConfig = ['field' => $sortConfig[0], 'direction' => \strtoupper($sortConfig[1])]; + } - $backendConfig['entities'][$entityName]['embeddedList']['sort'] = $sortConfig; + $backendConfig[$objectTypeRootKey][$objectName]['embeddedList']['sort'] = $sortConfig; + } + } } } @@ -72,9 +80,13 @@ private function processSortingConfig(array $backendConfig) private function processTemplateConfig(array $backendConfig) { - foreach ($backendConfig['entities'] as $entityName => $entityConfig) { - if (!isset($entityConfig['embeddedList']['template'])) { - $backendConfig['entities'][$entityName]['embeddedList']['template'] = '@EasyAdminExtension/default/embedded_list.html.twig'; + foreach (['entities', 'documents'] as $objectTypeRootKey) { + if (isset($backendConfig[$objectTypeRootKey]) && \is_array($backendConfig[$objectTypeRootKey])) { + foreach ($backendConfig[$objectTypeRootKey] as $objectName => $objectConfig) { + if (!isset($objectConfig['embeddedList']['template'])) { + $backendConfig[$objectTypeRootKey][$objectName]['embeddedList']['template'] = '@EasyAdminExtension/default/embedded_list.html.twig'; + } + } } } diff --git a/src/Configuration/ListFormFiltersConfigPass.php b/src/Configuration/ListFormFiltersConfigPass.php index 67e4eb68..a2a3742f 100644 --- a/src/Configuration/ListFormFiltersConfigPass.php +++ b/src/Configuration/ListFormFiltersConfigPass.php @@ -27,21 +27,30 @@ public function __construct(ManagerRegistry $doctrine) public function process(array $backendConfig): array { - if (!isset($backendConfig['entities'])) { - return $backendConfig; + if (isset($backendConfig['entities']) && \is_array($backendConfig['entities'])) { + $this->processObjectListFormFilters('entity', $backendConfig['entities']); } - foreach ($backendConfig['entities'] as $entityName => $entityConfig) { - if (!isset($entityConfig['list']['form_filters'])) { + if (isset($backendConfig['documents']) && \is_array($backendConfig['documents'])) { + $this->processObjectListFormFilters('document', $backendConfig['documents']); + } + + return $backendConfig; + } + + private function processObjectListFormFilters(string $objectType, array &$objectConfigs) + { + foreach ($objectConfigs as $objectName => $objectConfig) { + if (!isset($objectConfig['list']['form_filters'])) { continue; } $formFilters = []; - foreach ($entityConfig['list']['form_filters'] as $i => $formFilter) { + foreach ($objectConfig['list']['form_filters'] as $key => $formFilter) { // Detects invalid config node if (!\is_string($formFilter) && !\is_array($formFilter)) { - throw new \RuntimeException(\sprintf('The values of the "form_filters" option for the list view of the "%s" entity can only be strings or arrays.', $entityConfig['class'])); + throw new \RuntimeException(\sprintf('The values of the "form_filters" option for the list view of the "%s" object of type "%s" can only be strings or arrays.', $objectConfig['class'], $objectType)); } // Key mapping @@ -49,7 +58,11 @@ public function process(array $backendConfig): array $filterConfig = ['property' => $formFilter]; } else { if (!\array_key_exists('property', $formFilter)) { - throw new \RuntimeException(\sprintf('One of the values of the "form_filters" option for the "list" view of the "%s" entity does not define the mandatory option "property".', $entityConfig['class'])); + if (\is_string($key)) { + $formFilter['property'] = $key; + } else { + throw new \RuntimeException(\sprintf('One of the values of the "form_filters" option for the "list" view of the "%s" object of type "%s" does not define the mandatory option "property".', $objectConfig['class'], $objectType)); + } } $filterConfig = $formFilter; @@ -60,9 +73,11 @@ public function process(array $backendConfig): array // Auto set label with name value $filterConfig['label'] = $filterConfig['label'] ?? $filterConfig['name']; // Auto-set translation_domain - $filterConfig['translation_domain'] = $filterConfig['translation_domain'] ?? $entityConfig['translation_domain']; + $filterConfig['translation_domain'] = $filterConfig['translation_domain'] ?? $objectConfig['translation_domain']; - $this->configureFilter($entityConfig['class'], $filterConfig); + if ('entity' === $objectType) { + $this->configureEntityFormFilter($objectConfig['class'], $filterConfig); + } // If type is not configured at this steps => not guessable if (!isset($filterConfig['type'])) { @@ -73,13 +88,11 @@ public function process(array $backendConfig): array } // set form filters config and form ! - $backendConfig['entities'][$entityName]['list']['form_filters'] = $formFilters; + $objectConfigs[$objectName]['list']['form_filters'] = $formFilters; } - - return $backendConfig; } - private function configureFilter(string $entityClass, array &$filterConfig) + private function configureEntityFormFilter(string $entityClass, array &$filterConfig) { $em = $this->doctrine->getManagerForClass($entityClass); $entityMetadata = $em->getMetadataFactory()->getMetadataFor($entityClass); @@ -92,20 +105,16 @@ private function configureFilter(string $entityClass, array &$filterConfig) return; } - // Only applicable to ORM ClassMetadataInfo instances - if ($entityMetadata instanceof ClassMetadataInfo) { - if ($entityMetadata->hasField($filterConfig['property'])) { - $this->configureFieldFilter( - $entityClass, $entityMetadata->getFieldMapping($filterConfig['property']), $filterConfig); - } elseif ($entityMetadata->hasAssociation($filterConfig['property'])) { - $this->configureAssociationFilter( - $entityClass, $entityMetadata->getAssociationMapping($filterConfig['property']), $filterConfig - ); - } + if ($entityMetadata->hasField($filterConfig['property'])) { + $this->configureEntityPropertyFilter($entityClass, $entityMetadata->getFieldMapping($filterConfig['property']), $filterConfig); + } elseif ($entityMetadata->hasAssociation($filterConfig['property'])) { + $this->configureEntityAssociationFilter( + $entityClass, $entityMetadata->getAssociationMapping($filterConfig['property']), $filterConfig + ); } } - private function configureFieldFilter(string $entityClass, array $fieldMapping, array &$filterConfig) + private function configureEntityPropertyFilter(string $entityClass, array $fieldMapping, array &$filterConfig) { $defaultFilterConfigTypeOptions = []; @@ -160,7 +169,7 @@ private function configureFieldFilter(string $entityClass, array $fieldMapping, ); } - private function configureAssociationFilter(string $entityClass, array $associationMapping, array &$filterConfig) + private function configureEntityAssociationFilter(string $entityClass, array $associationMapping, array &$filterConfig) { $defaultFilterConfigTypeOptions = []; diff --git a/src/Configuration/ShortFormTypeConfigPass.php b/src/Configuration/ShortFormTypeConfigPass.php index 2af9e79f..06e058b7 100644 --- a/src/Configuration/ShortFormTypeConfigPass.php +++ b/src/Configuration/ShortFormTypeConfigPass.php @@ -41,15 +41,16 @@ public function process(array $backendConfig) private function replaceShortNameTypes(array $backendConfig) { - if ( - !isset($backendConfig['entities']) - || !\is_array($backendConfig['entities']) - ) { - return $backendConfig; + if (isset($backendConfig['entities']) && \is_array($backendConfig['entities'])) { + foreach ($backendConfig['entities'] as &$entityConfig) { + $entityConfig = $this->replaceShortFormTypesInObjectConfig($entityConfig); + } } - foreach ($backendConfig['entities'] as &$entity) { - $entity = $this->replaceShortFormTypesInObjectConfig($entity); + if (isset($backendConfig['documents']) && \is_array($backendConfig['documents'])) { + foreach ($backendConfig['documents'] as &$documentConfig) { + $documentConfig = $this->replaceShortFormTypesInObjectConfig($documentConfig); + } } return $backendConfig; diff --git a/src/Configuration/ShowViewConfigPass.php b/src/Configuration/ShowViewConfigPass.php index 7ce10c1c..6d9e40fb 100644 --- a/src/Configuration/ShowViewConfigPass.php +++ b/src/Configuration/ShowViewConfigPass.php @@ -47,21 +47,25 @@ public function process(array $backendConfig) */ private function processCustomShowTypes(array $backendConfig) { - foreach ($backendConfig['entities'] as $entityName => $entityConfig) { - foreach ($entityConfig['show']['fields'] as $fieldName => $fieldMetadata) { - if (\array_key_exists($fieldMetadata['type'], static::$mapTypeToTemplates)) { - $template = $this->isFieldTemplateDefined($fieldMetadata) - ? $fieldMetadata['template'] - : static::$mapTypeToTemplates[$fieldMetadata['type']]; - $entityConfig['show']['fields'][$fieldName]['template'] = $template; - - $entityConfig['show']['fields'][$fieldName]['template_options'] = $this->processTemplateOptions( - $fieldMetadata['type'], $fieldMetadata - ); + foreach (['entities', 'documents'] as $objectTypeRootKey) { + if (isset($backendConfig[$objectTypeRootKey]) && \is_array($backendConfig[$objectTypeRootKey])) { + foreach ($backendConfig[$objectTypeRootKey] as $objectName => $objectConfig) { + foreach ($objectConfig['show']['fields'] as $fieldName => $fieldMetadata) { + if (\array_key_exists($fieldMetadata['type'], static::$mapTypeToTemplates)) { + $template = $this->isFieldTemplateDefined($fieldMetadata) + ? $fieldMetadata['template'] + : static::$mapTypeToTemplates[$fieldMetadata['type']]; + $objectConfig['show']['fields'][$fieldName]['template'] = $template; + + $objectConfig['show']['fields'][$fieldName]['template_options'] = $this->processTemplateOptions( + $fieldMetadata['type'], $fieldMetadata + ); + } + } + + $backendConfig[$objectTypeRootKey][$objectName] = $objectConfig; } } - - $backendConfig['entities'][$entityName] = $entityConfig; } return $backendConfig; @@ -79,19 +83,47 @@ private function processTemplateOptions(string $type, array $fieldMetadata) switch ($type) { case 'embedded_list': - $parentEntityFqcn = $templateOptions['parent_entity_fqcn'] ?? $fieldMetadata['sourceEntity']; - $parentEntityProperty = $templateOptions['parent_entity_property'] ?? $fieldMetadata['property']; - $entityFqcn = $this->embeddedListHelper->getEntityFqcnFromParent( - $parentEntityFqcn, $parentEntityProperty + // Deprecations + if (isset($templateOptions['entity_fqcn']) && !isset($templateOptions['object_fqcn'])) { + $templateOptions['object_fqcn'] = $templateOptions['entity_fqcn']; + unset($templateOptions['entity_fqcn']); + + \trigger_error(\sprintf('The "entity_fqcn" option for embedded_list is deprecated since version 1.4.0 and it will be removed in 2.0. Use the "object_fqcn" option instead.'), E_USER_DEPRECATED); + } + if (isset($templateOptions['parent_entity_fqcn']) && !isset($templateOptions['parent_object_fqcn'])) { + $templateOptions['parent_object_fqcn'] = $templateOptions['parent_entity_fqcn']; + unset($templateOptions['parent_entity_fqcn']); + + \trigger_error(\sprintf('The "parent_entity_fqcn" option for embedded_list is deprecated since version 1.4.0 and it will be removed in 2.0. Use the "parent_object_fqcn" option instead.'), E_USER_DEPRECATED); + } + if (isset($templateOptions['parent_entity_property']) && !isset($templateOptions['parent_object_property'])) { + $templateOptions['parent_object_property'] = $templateOptions['parent_entity_property']; + unset($templateOptions['parent_entity_property']); + + \trigger_error(\sprintf('The "parent_entity_property" option for embedded_list is deprecated since version 1.4.0 and it will be removed in 2.0. Use the "parent_object_property" option instead.'), E_USER_DEPRECATED); + } + + $parentObjectFqcn = $templateOptions['parent_object_fqcn'] ?? $fieldMetadata['sourceEntity']; + $parentObjectProperty = $templateOptions['parent_object_property'] ?? $fieldMetadata['property']; + $objectFqcn = $this->embeddedListHelper->getEntityFqcnFromParent( + $parentObjectFqcn, $parentObjectProperty ); - if (!isset($templateOptions['entity_fqcn'])) { - $templateOptions['entity_fqcn'] = $entityFqcn; + + if (isset($templateOptions['document'])) { + $templateOptions['object_type'] = 'document'; + } else { + $templateOptions['object_type'] = 'entity'; } - if (!isset($templateOptions['parent_entity_property'])) { - $templateOptions['parent_entity_property'] = $parentEntityProperty; + + if (!isset($templateOptions['entity']) && !isset($templateOptions['document'])) { + $templateOptions['entity'] = $this->embeddedListHelper->guessEntityEntry($objectFqcn); + } + + if (!isset($templateOptions['object_fqcn'])) { + $templateOptions['object_fqcn'] = $objectFqcn; } - if (!isset($templateOptions['entity'])) { - $templateOptions['entity'] = $this->embeddedListHelper->guessEntityEntry($entityFqcn); + if (!isset($templateOptions['parent_object_property'])) { + $templateOptions['parent_object_property'] = $parentObjectProperty; } if (!isset($templateOptions['ext_filters'])) { $templateOptions['ext_filters'] = []; diff --git a/src/Controller/EasyAdminController.php b/src/Controller/EasyAdminController.php index ff0f8bae..69b37c9d 100644 --- a/src/Controller/EasyAdminController.php +++ b/src/Controller/EasyAdminController.php @@ -48,6 +48,7 @@ function ($name) use ($hiddenFields) { $requestParameters['referer'] = (string) $masterRequestUri; $viewVars = [ + 'objectType' => 'entity', 'paginator' => $paginator, 'fields' => $fields, '_request_parameters' => $requestParameters, diff --git a/src/Controller/MongoOdmEasyAdminController.php b/src/Controller/MongoOdmEasyAdminController.php new file mode 100644 index 00000000..474f72d3 --- /dev/null +++ b/src/Controller/MongoOdmEasyAdminController.php @@ -0,0 +1,87 @@ +dispatch(EasyAdminMongoOdmEvents::PRE_LIST); + + $fields = $this->document['list']['fields']; + $paginator = $this->mongoOdmFindAll( + $this->document['class'], + $this->request->query->get('page', 1), + $this->config['list']['max_results'] ?: 25, + $this->request->query->get('sortField'), + $this->request->query->get('sortDirection') + ); + + $this->dispatch(EasyAdminMongoOdmEvents::POST_LIST, ['paginator' => $paginator]); + + // Filter displaid columns + $hiddenFields = $this->request->query->get('hidden-fields', []); + $fields = \array_filter( + $this->document['list']['fields'], + function ($name) use ($hiddenFields) { + return !\in_array($name, $hiddenFields); + }, + ARRAY_FILTER_USE_KEY + ); + + // Removes existing referer + $baseMasterRequestUri = !$this->request->isXmlHttpRequest() + ? $this->get('request_stack')->getMasterRequest()->getUri() + : $this->request->headers->get('referer'); + $baseMasterRequestUri = Http::createFromString($baseMasterRequestUri); + $removeRefererModifier = new RemoveQueryParams(['referer']); + $masterRequestUri = $removeRefererModifier->process($baseMasterRequestUri); + + $requestParameters = $this->request->query->all(); + $requestParameters['referer'] = (string) $masterRequestUri; + + return $this->render('@EasyAdminExtension/default/embedded_list.html.twig', [ + 'objectType' => 'document', + 'paginator' => $paginator, + 'fields' => $fields, + '_request_parameters' => $requestParameters, + ]); + } + + /** + * {@inheritdoc} + * + * @throws \Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException + */ + protected function isActionAllowed($actionName) + { + switch ($actionName) { + // autocomplete action is mapped to list action for access permissions + case 'autocomplete': + // embeddedList action is mapped to list action for access permissions + case 'embeddedList': + $actionName = 'list'; + break; + default: + break; + } + + // Get item for edit/show or custom actions => security voters may apply + $easyadminMongoOdm = $this->request->attributes->get('easyadmin_mongo_odm'); + $subject = $easyadminMongoOdm['item'] ?? null; + $this->get(AdminAuthorizationChecker::class)->checksUserAccess($this->document, $actionName, $subject); + + return parent::isActionAllowed($actionName); + } +} diff --git a/src/DependencyInjection/Compiler/MongoOdmPass.php b/src/DependencyInjection/Compiler/MongoOdmPass.php new file mode 100644 index 00000000..026e956c --- /dev/null +++ b/src/DependencyInjection/Compiler/MongoOdmPass.php @@ -0,0 +1,22 @@ +getParameter('kernel.bundles')); + $hasEasyAdminMongoOdmBundle = $mongoOdmBundleClassExists && $mongoOdmBundleLoaded; + + // Disable services specific to EasyAdminMongoOdmBundle + if (!$hasEasyAdminMongoOdmBundle) { + $container->removeDefinition('alterphp.easyadmin_extension.subscriber.mongo_odm_post_query_builder'); + } + } +} diff --git a/src/DependencyInjection/Compiler/TwigPathPass.php b/src/DependencyInjection/Compiler/TwigPathPass.php new file mode 100644 index 00000000..36cf7f69 --- /dev/null +++ b/src/DependencyInjection/Compiler/TwigPathPass.php @@ -0,0 +1,39 @@ +getAlias('twig.loader')->__toString(); + $twigLoaderFilesystemDefinition = $container->getDefinition($twigLoaderFilesystemId); + + // Replaces native EasyAdmin templates + $easyAdminExtensionBundleRefl = new \ReflectionClass(EasyAdminExtensionBundle::class); + if ($easyAdminExtensionBundleRefl->isUserDefined()) { + $easyAdminExtensionBundlePath = \dirname((string) $easyAdminExtensionBundleRefl->getFileName()); + $easyAdminExtensionTwigPath = $easyAdminExtensionBundlePath.'/Resources/views'; + $twigLoaderFilesystemDefinition->addMethodCall( + 'prependPath', + [$easyAdminExtensionTwigPath, 'EasyAdminMongoOdm'] + ); + + // Waiting this PR or any alternative is implemented by Symfony itself + // @see https://github.com/symfony/symfony/pull/30527 + // Put back user default path + $twigDefaultPath = $container->getParameterBag()->resolveValue('%twig.default_path%'); + $userDefaultPath = $twigDefaultPath.'/bundles/EasyAdminMongoOdmBundle/'; + if (\file_exists($userDefaultPath)) { + $twigLoaderFilesystemDefinition->addMethodCall( + 'prependPath', + [$userDefaultPath, 'EasyAdminMongoOdm'] + ); + } + } + } +} diff --git a/src/EasyAdminExtensionBundle.php b/src/EasyAdminExtensionBundle.php index 37547fb2..f7a9e67a 100644 --- a/src/EasyAdminExtensionBundle.php +++ b/src/EasyAdminExtensionBundle.php @@ -2,7 +2,10 @@ namespace AlterPHP\EasyAdminExtensionBundle; +use AlterPHP\EasyAdminExtensionBundle\DependencyInjection\Compiler\MongoOdmPass; +use AlterPHP\EasyAdminExtensionBundle\DependencyInjection\Compiler\TwigPathPass; use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\DoctrineOrmMappingsPass; +use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -12,6 +15,11 @@ public function build(ContainerBuilder $container) { parent::build($container); + // Priority 1 to pass just before RegisterListenerPass (in case we have to enable/disable listeners) ! + $container->addCompilerPass(new MongoOdmPass(), PassConfig::TYPE_BEFORE_REMOVING, 1); + + $container->addCompilerPass(new TwigPathPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION); + $this->addRegisterMappingsPass($container); } diff --git a/src/EventListener/AbstractPostQueryBuilderSubscriber.php b/src/EventListener/AbstractPostQueryBuilderSubscriber.php new file mode 100644 index 00000000..7b5ac501 --- /dev/null +++ b/src/EventListener/AbstractPostQueryBuilderSubscriber.php @@ -0,0 +1,142 @@ +listFormFiltersHelper = $listFormFiltersHelper; + } + + /** + * Called on POST_LIST_QUERY_BUILDER event. + * + * @param GenericEvent $event + */ + public function onPostListQueryBuilder(GenericEvent $event) + { + $queryBuilder = $event->getArgument('query_builder'); + + if (!$this->supportsQueryBuilder($queryBuilder)) { + throw new \RuntimeException('Passed queryBuilder is not supported !'); + } + + // Request filters + if ($event->hasArgument('request')) { + $this->applyRequestFilters($queryBuilder, $event->getArgument('request')->get('ext_filters', [])); + } + + // List form filters + if ($event->hasArgument(static::APPLIABLE_OBJECT_TYPE)) { + $objectConfig = $event->getArgument(static::APPLIABLE_OBJECT_TYPE); + if (isset($objectConfig['list']['form_filters'])) { + $listFormFiltersForm = $this->listFormFiltersHelper->getListFormFilters($objectConfig['list']['form_filters']); + if ($listFormFiltersForm->isSubmitted() && $listFormFiltersForm->isValid()) { + $this->applyFormFilters($queryBuilder, $listFormFiltersForm->getData()); + } + } + } + } + + /** + * Called on POST_SEARCH_QUERY_BUILDER event. + * + * @param GenericEvent $event + */ + public function onPostSearchQueryBuilder(GenericEvent $event) + { + $queryBuilder = $event->getArgument('query_builder'); + + if (!$this->supportsQueryBuilder($queryBuilder)) { + throw new \RuntimeException('Passed queryBuilder is not supported !'); + } + + if ($event->hasArgument('request')) { + $this->applyRequestFilters($queryBuilder, $event->getArgument('request')->get('ext_filters', [])); + } + } + + /** + * Applies request filters on queryBuilder. + * + * @param object $queryBuilder + * @param array $filters + */ + protected function applyRequestFilters($queryBuilder, array $filters = []) + { + foreach ($filters as $field => $value) { + // Empty string and numeric keys is considered as "not applied filter" + if ('' === $value || \is_int($field)) { + continue; + } + + $operator = \is_array($value) ? ListFilter::OPERATOR_IN : ListFilter::OPERATOR_EQUALS; + $listFilter = ListFilter::createFromRequest($field, $operator, $value); + + $this->filterQueryBuilder($queryBuilder, $field, $listFilter); + } + } + + /** + * Applies form filters on queryBuilder. + * + * @param object $queryBuilder + * @param array $filters + */ + protected function applyFormFilters($queryBuilder, array $filters = []) + { + foreach ($filters as $field => $listFilter) { + if (null === $listFilter) { + continue; + } + + $this->filterQueryBuilder($queryBuilder, $field, $listFilter); + } + } + + protected function filterEasyadminAutocompleteValue($value) + { + if (!\is_array($value) || !isset($value['autocomplete']) || 1 !== \count($value)) { + return $value; + } + + return $value['autocomplete']; + } + + /** + * Checks if filter is directly appliable on queryBuilder. + * + * @param object $queryBuilder + * @param string $field + * + * @return bool + */ + protected function isFilterAppliable($queryBuilder, string $field): bool + { + return true; + } +} diff --git a/src/EventListener/MongoOdmPostQueryBuilderSubscriber.php b/src/EventListener/MongoOdmPostQueryBuilderSubscriber.php new file mode 100644 index 00000000..fbc1c406 --- /dev/null +++ b/src/EventListener/MongoOdmPostQueryBuilderSubscriber.php @@ -0,0 +1,100 @@ + ['onPostListQueryBuilder'], + EasyAdminMongoOdmEvents::POST_SEARCH_QUERY_BUILDER => ['onPostSearchQueryBuilder'], + ]; + } + + /** + * {@inheritdoc} + */ + protected function supportsQueryBuilder($queryBuilder): bool + { + return $queryBuilder instanceof QueryBuilder; + } + + /** + * Filters queryBuilder. + * + * @param QueryBuilder $queryBuilder + * @param string $field + * @param ListFilter $listFilter + */ + protected function filterQueryBuilder(QueryBuilder $queryBuilder, string $field, ListFilter $listFilter) + { + $value = $this->filterEasyadminAutocompleteValue($listFilter->getValue()); + // Empty string and numeric keys is considered as "not applied filter" + if (null === $value || '' === $value || \is_int($field)) { + return; + } + + $queryField = $listFilter->getProperty(); + + // Checks if filter is directly appliable on queryBuilder + if (!$this->isFilterAppliable($queryBuilder, $queryField)) { + return; + } + + $operator = $listFilter->getOperator(); + + switch ($operator) { + case ListFilter::OPERATOR_EQUALS: + if ('_NULL' === $value) { + $filterExpr = $queryBuilder->expr()->field($field)->equals(null); + } elseif ('_NOT_NULL' === $value) { + $filterExpr = $queryBuilder->expr()->field($field)->notEqual(null); + } else { + $filterExpr = $queryBuilder->expr()->field($field)->equals($value); + } + break; + case ListFilter::OPERATOR_NOT: + $filterExpr = $queryBuilder->expr()->field($field)->not($value); + break; + case ListFilter::OPERATOR_IN: + // Checks that $value is not an empty Traversable + if (0 < \count($value)) { + $filterExpr = $queryBuilder->expr()->field($field)->in($value); + } + break; + case ListFilter::OPERATOR_GT: + $filterExpr = $queryBuilder->expr()->field($field)->gt($value); + break; + case ListFilter::OPERATOR_GTE: + $filterExpr = $queryBuilder->expr()->field($field)->gte($value); + break; + case ListFilter::OPERATOR_LT: + $filterExpr = $queryBuilder->expr()->field($field)->lt($value); + break; + case ListFilter::OPERATOR_LTE: + $filterExpr = $queryBuilder->expr()->field($field)->lte($value); + break; + + case ListFilter::OPERATOR_NOTIN: + default: + throw new \RuntimeException(\sprintf('Operator "%s" is not supported !', $operator)); + } + + if (isset($filterExpr)) { + $queryBuilder->addAnd($filterExpr); + } + } +} diff --git a/src/EventListener/PostQueryBuilderSubscriber.php b/src/EventListener/PostQueryBuilderSubscriber.php index 8831a6cd..8676edfd 100644 --- a/src/EventListener/PostQueryBuilderSubscriber.php +++ b/src/EventListener/PostQueryBuilderSubscriber.php @@ -6,28 +6,13 @@ use Doctrine\ORM\Query\QueryException; use Doctrine\ORM\QueryBuilder; use EasyCorp\Bundle\EasyAdminBundle\Event\EasyAdminEvents; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\EventDispatcher\GenericEvent; /** * Apply filters on list/search queryBuilder. */ -class PostQueryBuilderSubscriber implements EventSubscriberInterface +class PostQueryBuilderSubscriber extends AbstractPostQueryBuilderSubscriber { - /** - * @var \AlterPHP\EasyAdminExtensionBundle\Helper\ListFormFiltersHelper - */ - protected $listFormFiltersHelper; - - /** - * ListFormFiltersExtension constructor. - * - * @param \AlterPHP\EasyAdminExtensionBundle\Helper\ListFormFiltersHelper $listFormFiltersHelper - */ - public function __construct($listFormFiltersHelper) - { - $this->listFormFiltersHelper = $listFormFiltersHelper; - } + protected const APPLIABLE_OBJECT_TYPE = 'entity'; /** * {@inheritdoc} @@ -41,71 +26,11 @@ public static function getSubscribedEvents() } /** - * Called on POST_LIST_QUERY_BUILDER event. - */ - public function onPostListQueryBuilder(GenericEvent $event) - { - $queryBuilder = $event->getArgument('query_builder'); - - // Request filters - if ($event->hasArgument('request')) { - $this->applyRequestFilters($queryBuilder, $event->getArgument('request')->get('ext_filters', [])); - } - - // List form filters - if ($event->hasArgument('entity')) { - $entityConfig = $event->getArgument('entity'); - if (isset($entityConfig['list']['form_filters'])) { - $listFormFiltersForm = $this->listFormFiltersHelper->getListFormFilters($entityConfig['list']['form_filters']); - if ($listFormFiltersForm->isSubmitted() && $listFormFiltersForm->isValid()) { - $this->applyFormFilters($queryBuilder, $listFormFiltersForm->getData()); - } - } - } - } - - /** - * Called on POST_SEARCH_QUERY_BUILDER event. - */ - public function onPostSearchQueryBuilder(GenericEvent $event) - { - $queryBuilder = $event->getArgument('query_builder'); - - if ($event->hasArgument('request')) { - $this->applyRequestFilters($queryBuilder, $event->getArgument('request')->get('ext_filters', [])); - } - } - - /** - * Applies request filters on queryBuilder. - */ - protected function applyRequestFilters(QueryBuilder $queryBuilder, array $filters = []) - { - foreach ($filters as $field => $value) { - // Empty string and numeric keys is considered as "not applied filter" - if ('' === $value || \is_int($field)) { - continue; - } - - $operator = \is_array($value) ? ListFilter::OPERATOR_IN : ListFilter::OPERATOR_EQUALS; - $listFilter = ListFilter::createFromRequest($field, $operator, $value); - - $this->filterQueryBuilder($queryBuilder, $field, $listFilter); - } - } - - /** - * Applies form filters on queryBuilder. + * {@inheritdoc} */ - protected function applyFormFilters(QueryBuilder $queryBuilder, array $filters = []) + protected function supportsQueryBuilder($queryBuilder): bool { - foreach ($filters as $field => $listFilter) { - if (null === $listFilter) { - continue; - } - - $this->filterQueryBuilder($queryBuilder, $field, $listFilter); - } + return $queryBuilder instanceof QueryBuilder; } /** @@ -206,19 +131,10 @@ protected function filterQueryBuilder(QueryBuilder $queryBuilder, string $field, } } - protected function filterEasyadminAutocompleteValue($value) - { - if (!\is_array($value) || !isset($value['autocomplete']) || 1 !== \count($value)) { - return $value; - } - - return $value['autocomplete']; - } - /** - * Checks if filter is directly appliable on queryBuilder. + * {@inheritdoc} */ - protected function isFilterAppliable(QueryBuilder $queryBuilder, string $field): bool + protected function isFilterAppliable($queryBuilder, string $field): bool { $qbClone = clone $queryBuilder; diff --git a/src/Form/Type/EasyAdminEmbeddedListType.php b/src/Form/Type/EasyAdminEmbeddedListType.php index 753fe445..f3bf3b6f 100644 --- a/src/Form/Type/EasyAdminEmbeddedListType.php +++ b/src/Form/Type/EasyAdminEmbeddedListType.php @@ -37,6 +37,26 @@ public function getBlockPrefix() * {@inheritdoc} */ public function buildView(FormView $view, FormInterface $form, array $options) + { + if (null !== $options['document']) { + $view->vars['object_type'] = 'document'; + $this->buildViewForDocumentList($view, $form, $options); + } else { + $view->vars['object_type'] = 'entity'; + $this->buildViewForEntityList($view, $form, $options); + } + + $view->vars['hidden_fields'] = $options['hidden_fields']; + $view->vars['max_results'] = $options['max_results']; + + if ($options['sort']) { + $sort['field'] = $options['sort'][0]; + $sort['direction'] = $options['sort'][1] ?? 'DESC'; + $view->vars['sort'] = $sort; + } + } + + private function buildViewForEntityList(FormView $view, FormInterface $form, array $options) { $parentData = $form->getParent()->getData(); @@ -45,14 +65,14 @@ public function buildView(FormView $view, FormInterface $form, array $options) // Guess entity FQCN from parent metadata $entityFqcn = $this->embeddedListHelper->getEntityFqcnFromParent(\get_class($parentData), $form->getName()); if (null !== $entityFqcn) { - $view->vars['entity_fqcn'] = $entityFqcn; + $view->vars['object_fqcn'] = $entityFqcn; // Guess embeddedList entity if not set if (null === $embeddedListEntity) { $embeddedListEntity = $this->embeddedListHelper->guessEntityEntry($entityFqcn); } } $view->vars['entity'] = $embeddedListEntity; - $view->vars['parent_entity_property'] = $form->getConfig()->getName(); + $view->vars['parent_object_property'] = $form->getConfig()->getName(); // Only for backward compatibility (when there were no guesser) $propertyAccessor = PropertyAccess::createPropertyAccessor(); @@ -64,15 +84,25 @@ public function buildView(FormView $view, FormInterface $form, array $options) return $filter; }, $embeddedListFilters); $view->vars['ext_filters'] = $extFilters; + } - $view->vars['hidden_fields'] = $options['hidden_fields']; - $view->vars['max_results'] = $options['max_results']; + private function buildViewForDocumentList(FormView $view, FormInterface $form, array $options) + { + $parentData = $form->getParent()->getData(); - if ($options['sort']) { - $sort['field'] = $options['sort'][0]; - $sort['direction'] = $options['sort'][1] ?? 'DESC'; - $view->vars['sort'] = $sort; - } + $view->vars['document'] = $options['document']; + $embeddedListFilters = $options['ext_filters']; + + // Only for backward compatibility (when there were no guesser) + $propertyAccessor = PropertyAccess::createPropertyAccessor(); + $extFilters = \array_map(function ($filter) use ($propertyAccessor, $parentData) { + if (0 === \strpos($filter, 'form:')) { + $filter = $propertyAccessor->getValue($parentData, \substr($filter, 5)); + } + + return $filter; + }, $embeddedListFilters); + $view->vars['ext_filters'] = $extFilters; } /** @@ -81,12 +111,14 @@ public function buildView(FormView $view, FormInterface $form, array $options) public function configureOptions(OptionsResolver $resolver) { $resolver + ->setDefault('document', null) ->setDefault('entity', null) ->setDefault('ext_filters', []) ->setDefault('hidden_fields', []) ->setDefault('max_results', null) ->setDefault('sort', null) ->setDefault('mapped', false) + ->setAllowedTypes('document', ['null', 'string']) ->setAllowedTypes('entity', ['null', 'string']) ->setAllowedTypes('ext_filters', ['array']) ->setAllowedTypes('hidden_fields', ['array']) diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml index 01ea85d2..1af7e6ee 100644 --- a/src/Resources/config/services.xml +++ b/src/Resources/config/services.xml @@ -9,10 +9,23 @@ - + + + + + + + + + + + + + + @@ -20,11 +33,13 @@ %easy_admin_extension.custom_form_types% + %easy_admin_extension.embedded_list.open_new_tab% + @@ -44,6 +59,7 @@ + diff --git a/src/Resources/public/js/autocomplete-create.js b/src/Resources/public/js/autocomplete-create.js index 0eaea540..9e65a94e 100644 --- a/src/Resources/public/js/autocomplete-create.js +++ b/src/Resources/public/js/autocomplete-create.js @@ -34,7 +34,7 @@ function createAutoCompleteCreateFields() { minimumInputLength: 1, language: { noResults: function () { - return ''+button_text+' '+field_name+''; + return ''+button_text+' '+field_name+''; } }, escapeMarkup: function (markup) { @@ -44,28 +44,28 @@ function createAutoCompleteCreateFields() { }); } -function switchToEntityCreation(url_action, select_id, field_name) { +function switchToObjectCreation(url_action, select_id, field_name) { $('#'+select_id).select2('close'); $.ajax({ url : url_action, type: 'GET', success: function(data) { - openCreateEntityModal(data, url_action, field_name, select_id); - $('#create-entity-modal').modal({ backdrop: true, keyboard: true }); + openCreateObjectModal(data, url_action, field_name, select_id); + $('#create-object-modal').modal({ backdrop: true, keyboard: true }); } }); } -function openCreateEntityModal(data, url_action, field_name, select_id) { +function openCreateObjectModal(data, url_action, field_name, select_id) { var data_html = $(data.html); data_html.find('.form-actions > a[name=list]').remove(); data_html.find('.form-actions > a[name=delete]').remove(); - $('#create-entity-modal .modal-body').html(data_html); + $('#create-object-modal .modal-body').html(data_html); $('form[name="'+field_name+'"]').attr('action', url_action); - initCreateEntityAjaxForm(field_name, select_id); + initCreateObjectAjaxForm(field_name, select_id); } -function initCreateEntityAjaxForm(field_name, select_id) { +function initCreateObjectAjaxForm(field_name, select_id) { $('form[name="'+field_name+'"]').submit(function( event ) { event.preventDefault(); var url_action = $(this).attr('action'); @@ -78,7 +78,7 @@ function initCreateEntityAjaxForm(field_name, select_id) { processData: false, success: function(data) { if (data.hasOwnProperty('option')) { - $('#create-entity-modal').modal('hide'); + $('#create-object-modal').modal('hide'); var newOption = new Option(data.option.text, data.option.id, true, true); $('#'+select_id).append(newOption).trigger('change'); // manually trigger the `select2:select` event @@ -88,7 +88,7 @@ function initCreateEntityAjaxForm(field_name, select_id) { }); } if (data.hasOwnProperty('html')) { - openCreateEntityModal(data, url_action, field_name, select_id); + openCreateObjectModal(data, url_action, field_name, select_id); } }, error: function(error){ diff --git a/src/Resources/views/default/embedded_list.html.twig b/src/Resources/views/default/embedded_list.html.twig index 6045a4bf..f6250fba 100644 --- a/src/Resources/views/default/embedded_list.html.twig +++ b/src/Resources/views/default/embedded_list.html.twig @@ -1,6 +1,6 @@ -{% set _entity_config = easyadmin_entity(app.request.query.get('entity')) %} -{% trans_default_domain _entity_config.translation_domain %} -{% set _trans_parameters = { '%entity_name%': _entity_config.name|trans, '%entity_label%': _entity_config.label|trans } %} +{% set _object_config = easyadmin_object(app.request) %} +{% trans_default_domain _object_config.translation_domain %} +{% set _trans_parameters = { '%entity_name%': _object_config.name|trans, '%entity_label%': _object_config.label|trans } %} {% set widget_identifier = app.request.requestUri|embedded_list_identifier %} @@ -9,110 +9,110 @@ {{ 'search.page_title'|transchoice(paginator.nbResults, {}, 'EasyAdminBundle')|raw|spaceless }} {% else %} {% set _default_title = 'list.page_title'|trans(_trans_parameters, 'EasyAdminBundle') %} - {{ (_entity_config.list.title is defined ? _entity_config.list.title|trans(_trans_parameters) : _default_title)|spaceless }} + {{ (_object_config.list.title is defined ? _object_config.list.title|trans(_trans_parameters) : _default_title)|spaceless }} {% endif %} {% endset %} {% block main %} -
- {% set _list_item_actions = easyadmin_get_actions_for_list_item(_entity_config.name) %} - {# Prune forbidden actions AND delete action anyway (not handled with #delete-modal) #} - {% set _list_item_actions = _list_item_actions|prune_item_actions(_entity_config, ['delete']) %} - {% set _columns_count = fields|length + (_list_item_actions|length > 0 ? 1 : 0) %} +
+ {% set _list_item_actions = easyadmin_object_get_actions_for_list_item(objectType, _object_config.name) %} + {# Prune forbidden actions AND delete action anyway (not handled with #delete-modal) #} + {% set _list_item_actions = _list_item_actions|prune_item_actions(_object_config, ['delete']) %} + {% set _columns_count = fields|length + (_list_item_actions|length > 0 ? 1 : 0) %} - - - {% block table_head %} - - {% for field, metadata in fields %} - {% set isSortingField = (metadata.property == app.request.get('sortField')) or ('association' == metadata.type and app.request.get('sortField') starts with metadata.property ~ '.') %} - {% set nextSortDirection = isSortingField ? (app.request.get('sortDirection') == 'DESC' ? 'ASC' : 'DESC') : 'DESC' %} - {% set _column_label = (metadata.label ?: field|humanize)|trans(_trans_parameters) %} - {% set _column_icon = isSortingField ? (nextSortDirection == 'DESC' ? 'fa-arrow-up' : 'fa-arrow-down') : 'fa-sort' %} +
+ + {% block table_head %} + + {% for field, metadata in fields %} + {% set isSortingField = (metadata.property == app.request.get('sortField')) or ('association' == metadata.type and app.request.get('sortField') starts with metadata.property ~ '.') %} + {% set nextSortDirection = isSortingField ? (app.request.get('sortDirection') == 'DESC' ? 'ASC' : 'DESC') : 'DESC' %} + {% set _column_label = (metadata.label ?: field|humanize)|trans(_trans_parameters) %} + {% set _column_icon = isSortingField ? (nextSortDirection == 'DESC' ? 'fa-arrow-up' : 'fa-arrow-down') : 'fa-sort' %} - - {% endfor %} - - {% if _list_item_actions|length > 0 %} - - {% endif %} - - {% endblock table_head %} - + + {% endfor %} - - {% block table_body %} - {% for item in paginator.currentPageResults %} - {# the empty string concatenation is needed when the primary key is an object (e.g. an Uuid object) #} - {% set _item_id = '' ~ attribute(item, _entity_config.primary_key_field_name) %} - - {% for field, metadata in fields %} - {% set isSortingField = metadata.property == app.request.get('sortField') %} - {% set _column_label = (metadata.label ?: field|humanize)|trans(_trans_parameters) %} + {% if _list_item_actions|length > 0 %} + + {% endif %} + + {% endblock table_head %} + - - {% endfor %} + + {% block table_body %} + {% for item in paginator.currentPageResults %} + {# the empty string concatenation is needed when the primary key is an object (e.g. an Uuid object) #} + {% set _item_id = '' ~ attribute(item, _object_config.primary_key_field_name) %} + + {% for field, metadata in fields %} + {% set isSortingField = metadata.property == app.request.get('sortField') %} + {% set _column_label = (metadata.label ?: field|humanize)|trans(_trans_parameters) %} - {% if _list_item_actions|length > 0 %} - {% set _column_label = 'list.row_actions'|trans(_trans_parameters, 'EasyAdminBundle') %} - - {% endif %} - - {% else %} - - - - {% endfor %} - {% endblock table_body %} - + + {% endfor %} - {% if _entity_config.embeddedList.open_new_tab %} - - - - - - {% endif %} -
- {% if metadata.sortable %} - - {{ _column_label|raw }} - - {% else %} - {{ _column_label|raw }} - {% endif %} - - {{ 'list.row_actions'|trans(_trans_parameters, 'EasyAdminBundle') }} -
+ {% if metadata.sortable %} + + {{ _column_label|raw }} + + {% else %} + {{ _column_label|raw }} + {% endif %} +
+ {{ 'list.row_actions'|trans(_trans_parameters, 'EasyAdminBundle') }} +
- {{ easyadmin_render_field_for_list_view(_entity_config.name, item, metadata) }} -
- {% block item_actions %} - {{ include('@EasyAdmin/default/includes/_actions.html.twig', { - actions: _list_item_actions|prune_item_actions(_entity_config, ['delete'], item), - request_parameters: _request_parameters, - translation_domain: _entity_config.translation_domain, - trans_parameters: _trans_parameters, - item_id: _item_id - }, with_context = false) }} - {% endblock item_actions %} -
- {{ 'search.no_results'|trans(_trans_parameters, 'EasyAdminBundle') }} -
+ {{ easyadmin_object_render_field_for_list_view(objectType, _object_config.name, item, metadata) }} +
- {% block open_new_tab %} - - - {{ 'open.new_tab'|trans({}, 'EasyAdminBundle') }} - - {% endblock open_new_tab %} + {% if _list_item_actions|length > 0 %} + {% set _column_label = 'list.row_actions'|trans(_trans_parameters, 'EasyAdminBundle') %} + + {% block item_actions %} + {{ include('@EasyAdmin/default/includes/_actions.html.twig', { + actions: _list_item_actions|prune_item_actions(_object_config, ['delete'], item), + request_parameters: _request_parameters, + translation_domain: _object_config.translation_domain, + trans_parameters: _trans_parameters, + item_id: _item_id + }, with_context = false) }} + {% endblock item_actions %}
+ {% endif %} + + {% else %} + + + {{ 'search.no_results'|trans(_trans_parameters, 'EasyAdminBundle') }} + + + {% endfor %} + {% endblock table_body %} + + + {% if _object_config.embeddedList.open_new_tab %} + + + + {% block open_new_tab %} + + + {{ 'open.new_tab'|trans({}, 'EasyAdminBundle') }} + + {% endblock open_new_tab %} + + + + {% endif %} + - {% block paginator %}{{ include(_entity_config.templates.paginator) }}{% endblock paginator %} + {% block paginator %}{{ include(_object_config.templates.paginator) }}{% endblock paginator %} - +
{% endblock main %} diff --git a/src/Resources/views/default/field_embedded_list.html.twig b/src/Resources/views/default/field_embedded_list.html.twig index d6826f7d..f2a83b79 100644 --- a/src/Resources/views/default/field_embedded_list.html.twig +++ b/src/Resources/views/default/field_embedded_list.html.twig @@ -1,12 +1,15 @@ {% set default_ext_filters = guess_default_filters( - field_options.template_options.entity_fqcn, field_options.template_options.parent_entity_property, item + field_options.template_options.object_fqcn, field_options.template_options.parent_object_property, item ) %} {% set ext_filters = default_ext_filters|merge(field_options.template_options.ext_filters) %} -{% include '@EasyAdmin/includes/_include_embedded_list.html.twig' with { - entity: field_options.template_options.entity, +{% set tpl_params = { + object_type: field_options.template_options.object_type, + object: attribute(field_options.template_options, field_options.template_options.object_type), ext_filters: ext_filters, hidden_fields: field_options.template_options.hidden_fields, max_results: field_options.template_options.max_results, sort: field_options.template_options.sort - } only %} +} %} + +{% include '@EasyAdmin/includes/_include_embedded_list.html.twig' with tpl_params only %} diff --git a/src/Resources/views/default/includes/_actions.html.twig b/src/Resources/views/default/includes/_actions.html.twig index a40d2218..1154ae0f 100644 --- a/src/Resources/views/default/includes/_actions.html.twig +++ b/src/Resources/views/default/includes/_actions.html.twig @@ -1,8 +1,8 @@ {% for action in actions %} {% if 'list' == action.name %} - {% set action_href = request_parameters.referer|default('') ? request_parameters.referer|easyadmin_urldecode : path('easyadmin', request_parameters|merge({ action: 'list' })) %} + {% set action_href = request_parameters.referer|default('') ? request_parameters.referer|easyadmin_urldecode : easyadmin_path(request_parameters|merge({ action: 'list' })) %} {% elseif 'method' == action.type %} - {% set action_href = path('easyadmin', request_parameters|merge({ action: action.name, id: item_id })) %} + {% set action_href = easyadmin_path(request_parameters|merge({ action: action.name, id: item_id })) %} {% elseif 'route' == action.type %} {% set action_href = path(action.name, request_parameters|merge({ action: action.name, id: item_id })) %} {% endif %} diff --git a/src/Resources/views/default/layout.html.twig b/src/Resources/views/default/layout.html.twig index 3615f5a6..a98dae55 100644 --- a/src/Resources/views/default/layout.html.twig +++ b/src/Resources/views/default/layout.html.twig @@ -1,4 +1,6 @@ -{% extends '@!EasyAdmin/default/layout.html.twig' %} +{% extends easyadmin_object_base_twig_path(app.request, 'default/layout.html.twig') %} + +{% set _object_config = easyadmin_object(app.request) %} {% block head_stylesheets %} {{ parent() }} @@ -15,7 +17,7 @@ {% block create_entity_modal %} {{ include('@EasyAdmin/includes/_create_entity_modal.html.twig', { - _translation_domain: _entity_config.translation_domain|default(easyadmin_config('translation_domain')), + _translation_domain: _object_config.translation_domain|default(easyadmin_config('translation_domain')), _trans_parameters: _trans_parameters|default([]), }, with_context = false) }} {% endblock create_entity_modal %} @@ -24,7 +26,7 @@ {% block confirm_modal %} {{ include('@EasyAdmin/includes/_confirm_modal.html.twig', { - _translation_domain: _entity_config.translation_domain|default(easyadmin_config('translation_domain')), + _translation_domain: _object_config.translation_domain|default(easyadmin_config('translation_domain')), _trans_parameters: _trans_parameters|default([]), }, with_context = false) }} {% endblock confirm_modal %} diff --git a/src/Resources/views/default/list.html.twig b/src/Resources/views/default/list.html.twig index 43c7c91d..f6f4920d 100644 --- a/src/Resources/views/default/list.html.twig +++ b/src/Resources/views/default/list.html.twig @@ -1,5 +1,7 @@ -{% extends '@!EasyAdmin/default/list.html.twig' %} +{% extends easyadmin_object_base_twig_path(app.request, 'default/list.html.twig') %} +{% set _object_config = easyadmin_object(app.request) %} +{% set _object_type = easyadmin_object_type(app.request) %} {% set requestExtFilters = app.request.get('ext_filters', {}) %} {% set _request_parameters = _request_parameters|default({})|merge({ ext_filters: requestExtFilters @@ -36,7 +38,7 @@ {# Do not display SEARCH form if not granted #} {% block search_action %} - {% if is_easyadmin_granted(_entity_config, 'search') %} + {% if is_easyadmin_granted(_object_config, 'search') %} {{ parent() }} {% endif %} {% endblock %} @@ -48,20 +50,20 @@ {# Do not display NEW button if not granted #} {% block new_action %} - {% if is_easyadmin_granted(_entity_config, 'new') %} + {% if is_easyadmin_granted(_object_config, 'new') %} {{ parent() }} {% endif %} {% endblock %} {# Do not display list action items if not granted #} {% block item_actions %} - {% set _list_item_actions = _list_item_actions|prune_item_actions(_entity_config, [], item) %} + {% set _list_item_actions = _list_item_actions|prune_item_actions(_object_config, [], item) %} {{ parent() }} {% endblock %} {% block list_form_filters %} - {% if _entity_config.list.form_filters is defined and _entity_config.list.form_filters is not empty %} - {% set list_form_filters = list_form_filters(_entity_config.list.form_filters) %} + {% if _object_config.list.form_filters is defined and _object_config.list.form_filters is not empty %} + {% set list_form_filters = list_form_filters(_object_config.list.form_filters) %}
{{ 'list_form_filters.heading_title'|trans(_trans_parameters, 'EasyAdminBundle') }} @@ -69,14 +71,14 @@ {{ 'list_form_filters.heading_expandcollapse'|trans(_trans_parameters, 'EasyAdminBundle') }}
-
+
{% form_theme list_form_filters '@EasyAdmin/form/bootstrap_4.html.twig' %} {{ block('request_parameters_as_hidden') }} - - - + + +
diff --git a/src/Resources/views/default/new_ajax.html.twig b/src/Resources/views/default/new_ajax.html.twig index fe63b1c1..93c9035b 100644 --- a/src/Resources/views/default/new_ajax.html.twig +++ b/src/Resources/views/default/new_ajax.html.twig @@ -2,17 +2,17 @@ {% block head_stylesheets %} @@ -23,7 +23,7 @@ {% block header %}{% endblock %} {% block sidebar %}{% endblock %} {% block confirm_modal %}{% endblock %} -{% block create_entity_modal %}{% endblock %} +{% block create_object_modal %}{% endblock %} {% block body_javascript %} {% set _select2_locales = ['ar', 'az', 'bg', 'ca', 'cs', 'da', 'de', 'el', 'en', 'es', 'et', 'eu', 'fa', 'fi', 'fr', 'gl', 'he', 'hi', 'hr', 'hu', 'id', 'is', 'it', 'ja', 'km', 'ko', 'lt', 'lv', 'mk', 'ms', 'nb', 'nl', 'pl', 'pt-BR', 'pt', 'ro', 'ru', 'sk', 'sr-Cyrl', 'sr', 'sv', 'th', 'tr', 'uk', 'vi', 'zh-CN', 'zh-TW'] %} {% set _app_language = app.request.locale|split('-')|first|split('_')|first %} @@ -38,7 +38,7 @@ $(function() { // Select2 widget is only enabled for the