Skip to content

Commit

Permalink
Embedded crud
Browse files Browse the repository at this point in the history
  • Loading branch information
psihius committed Jul 25, 2024
1 parent 0129eb9 commit ed9e8c0
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 3 deletions.
32 changes: 30 additions & 2 deletions src/Controller/AbstractCrudController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace EasyCorp\Bundle\EasyAdminBundle\Controller;

use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
Expand Down Expand Up @@ -129,6 +130,33 @@ public function index(AdminContext $context)
$fields = FieldCollection::new($this->configureFields(Crud::PAGE_INDEX));
$filters = $this->container->get(FilterFactory::class)->create($context->getCrud()->getFiltersConfig(), $fields, $context->getEntity());
$queryBuilder = $this->createIndexQueryBuilder($context->getSearch(), $context->getEntity(), $fields, $filters);

/** @var array|null $embedContext */
$embedContext = $context->getRequest()->query->all('embedContext');
if (\array_key_exists('mappedBy', $embedContext)) {
$filterProperty = $embedContext['mappedBy'];
$filterValue = $embedContext['embeddedIn'] ?? null;
if (null !== $filterValue) {
// Use the parameter conversion capabilities of Doctrine
$metadata = $queryBuilder->getEntityManager()->getClassMetadata($context->getEntity()->getFqcn());
$relatedEntityFqcn = $metadata->getAssociationTargetClass($filterProperty);
$relatedEntityMetadata = $queryBuilder->getEntityManager()->getClassMetadata($relatedEntityFqcn);
$type = $relatedEntityMetadata->getTypeOfField($metadata->getSingleIdentifierFieldName());
if (Type::hasType($type)) {
$doctrineType = Type::getType($type);
$platform = $queryBuilder->getEntityManager()->getConnection()->getDatabasePlatform();
$filterValue = $doctrineType->convertToDatabaseValue($filterValue, $platform);
}
$rootAlias = current($queryBuilder->getRootAliases());
$queryBuilder->andWhere(':filterValue MEMBER OF '.\sprintf('%s.%s', $rootAlias, $filterProperty))
->setParameter('filterValue', $filterValue);
}
$field = $fields->getByProperty($filterProperty);
if ($field instanceof FieldDto) {
$fields->unset($field);
}
}

$paginator = $this->container->get(PaginatorFactory::class)->create($queryBuilder);

// this can happen after deleting some items and trying to return
Expand All @@ -147,7 +175,7 @@ public function index(AdminContext $context)

$responseParameters = $this->configureResponseParameters(KeyValueStore::new([
'pageName' => Crud::PAGE_INDEX,
'templateName' => 'crud/index',
'templateName' => null !== $embedContext ? 'crud/embedded' : 'crud/index',
'entities' => $entities,
'paginator' => $paginator,
'global_actions' => $actions->getGlobalActions(),
Expand Down Expand Up @@ -559,7 +587,7 @@ protected function ajaxEdit(EntityDto $entityDto, ?string $propertyName, bool $n
{
$field = $entityDto->getFields()->getByProperty($propertyName);
if (null === $field || true === $field->getFormTypeOption('disabled')) {
throw new AccessDeniedException(sprintf('The field "%s" does not exist or it\'s configured as disabled, so it can\'t be modified.', $propertyName));
throw new AccessDeniedException(\sprintf('The field "%s" does not exist or it\'s configured as disabled, so it can\'t be modified.', $propertyName));
}

$this->container->get(EntityUpdater::class)->updateProperty($entityDto, $propertyName, $newValue);
Expand Down
23 changes: 23 additions & 0 deletions src/Field/EmbedField.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace EasyCorp\Bundle\EasyAdminBundle\Field;

use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface;
use EasyCorp\Bundle\EasyAdminBundle\Form\Type\EmbedType;

final class EmbedField implements FieldInterface
{
use FieldTrait;

public static function new(string $propertyName, ?string $label = null): self
{
return (new self())
->setProperty($propertyName)
->setLabel($label)
->setTemplateName('crud/field/embed')
->setFormType(EmbedType::class)
->setFormTypeOption('mapped', false)
->onlyWhenUpdating()
;
}
}
19 changes: 19 additions & 0 deletions src/Form/Type/EmbedType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace EasyCorp\Bundle\EasyAdminBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;

class EmbedType extends AbstractType
{
public function getBlockPrefix(): string
{
return 'ea_embedded_collection';
}

public function getParent(): string
{
return CollectionType::class;
}
}
2 changes: 2 additions & 0 deletions src/Registry/TemplateRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ final class TemplateRegistry
'flash_messages' => '@EasyAdmin/flash_messages.html.twig',
'crud/paginator' => '@EasyAdmin/crud/paginator.html.twig',
'crud/index' => '@EasyAdmin/crud/index.html.twig',
'crud/embedded' => '@EasyAdmin/crud/embedded.html.twig',
'crud/detail' => '@EasyAdmin/crud/detail.html.twig',
'crud/new' => '@EasyAdmin/crud/new.html.twig',
'crud/edit' => '@EasyAdmin/crud/edit.html.twig',
Expand All @@ -35,6 +36,7 @@ final class TemplateRegistry
'crud/field/datetimetz' => '@EasyAdmin/crud/field/datetimetz.html.twig',
'crud/field/decimal' => '@EasyAdmin/crud/field/decimal.html.twig',
'crud/field/email' => '@EasyAdmin/crud/field/email.html.twig',
'crud/field/embed' => '@EasyAdmin/crud/field/embed.html.twig',
'crud/field/float' => '@EasyAdmin/crud/field/float.html.twig',
'crud/field/generic' => '@EasyAdmin/crud/field/generic.html.twig',
'crud/field/hidden' => '@EasyAdmin/crud/field/hidden.html.twig',
Expand Down
10 changes: 10 additions & 0 deletions src/Resources/views/crud/embedded.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #}
{# @var field \EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto #}
{# @var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #}
{% set has_footer = entities|length != 0 %}
{% set has_batch_actions = false %}
{% set some_results_are_hidden = false %}
{% set sort_field_name = app.request.get('sort')|keys|first %}
{% set sort_order = app.request.get('sort')|first %}

{{ block("content", "@EasyAdmin/crud/index.html.twig") }}
122 changes: 122 additions & 0 deletions src/Resources/views/crud/field/embed.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #}
{# @var field \EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto #}
{# @var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #}
{% set current_url = ea_url() %}
{% set field = form.vars.ea_vars.field %}
{% set entity = form.vars.ea_vars.entity %}
{% set target_entity = field.doctrineMetadata.get('targetEntity') %}
{% set crudControllers = ea.crudControllers %}
{% set target_entity_crud_fqcn = crudControllers.findCrudFqcnByEntityFqcn(target_entity) %}

{% set url = ea_url().unset('entityId').setController(target_entity_crud_fqcn).setAction('index').set('embedContext', {
mappedBy: field.doctrineMetadata.get('mappedBy'),
embeddedIn: entity.primaryKeyValue
}) %}

{% set id_suffix = '-'~field.property %}

<div id="embed{{ id_suffix }}" class="position-relative embed-loading">
<div class="position-absolute text-center embed-spinner">
<div class="spinner-border text-primary spinner-border-lg mt-2"></div>
</div>
<div class="embed-content"></div>
</div>

<style>
.embed-spinner {
display: none;
top: 50%;
left: 50%;
margin-top: -1rem;
margin-left: -1rem;
z-index: 10;
}
.embed-loading .embed-spinner {
display: block;
}
.embed-loading .embed-content {
opacity: 0.5;
pointer-events: none;
}
</style>

<script>
window.addEventListener('load', () => {
const referrer = window.location.href;
const initialUrl = '{{ url|raw }}'
const embed = document.querySelector('#embed{{ id_suffix }}');
const embedContent = embed.querySelector('.embed-content');
const load = (url) => {
embed.classList.add('embed-loading');
fetch(url)
.then(it => it.text())
.then(it => embedContent.innerHTML = it)
.then(() => {
// override referrer of actions, so we get back to the "main" view afterwards, not the embed
embedContent.querySelectorAll('.actions a').forEach(action => {
const target = new URL(action.href)
target.searchParams.set('referrer', referrer);
action.href = target.toString();
});
// override referrer of actions, so we get back to the "main" view afterwards, not the embed
embedContent.querySelectorAll('.global-actions a').forEach(action => {
const target = new URL(action.href)
const current = new URL(referrer);
target.searchParams.set('relatedEntityId', current.searchParams.get('entityId'));
target.searchParams.set('referrer', referrer);
action.href = target.toString();
})
embedContent.querySelectorAll('.action-delete').forEach((actionElement) => {
actionElement.addEventListener('click', (event) => {
event.preventDefault();
document.querySelector('#modal-delete-button').addEventListener('click', () => {
const deleteFormAction = new URL(actionElement.getAttribute('formaction'));
const deleteForm = document.querySelector('#delete-form');
deleteFormAction.searchParams.set('referrer', referrer);
deleteForm.setAttribute('action', deleteFormAction.toString());
deleteForm.submit();
});
});
});
// intercept sort and pagination
embedContent.querySelectorAll('thead a, .pagination a').forEach(link => {
link.addEventListener('click', evt => {
evt.preventDefault();
load(link.href)
})
})
// intercept search
// embedContent.querySelector('.form-action-search form').addEventListener('submit', evt => {
// evt.preventDefault();
// const data = new FormData(evt.target);
// const params = new URLSearchParams(data).toString()
// const target = new URL(url);
// target.search = params.toString();
// load(target.toString())
// })
// highlight results
// const searchQuery = new URL(url).searchParams.get('query');
// if(searchQuery) {
// $(embedContent).find('table tbody td:not(.actions)').highlight($.merge([searchQuery], searchQuery.split(' ')));
// }
// can be used to re-initialize dynamic content
document.dispatchEvent(new Event('ea.embed.content-loaded'))
})
.finally(() => embed.classList.remove('embed-loading'))
;
}
load(initialUrl);
})
</script>
4 changes: 4 additions & 0 deletions src/Resources/views/crud/form_theme.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -879,3 +879,7 @@
</button>
</div>
{% endblock %}

{% block ea_embedded_collection_row %}
{{ include(ea.templatePath('crud/field/embed')) }}
{% endblock %}
1 change: 0 additions & 1 deletion src/Resources/views/crud/index.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
{# @var entities \EasyCorp\Bundle\EasyAdminBundle\Collection\EntityCollection #}
{# @var paginator \EasyCorp\Bundle\EasyAdminBundle\Orm\EntityPaginator #}
{% extends ea.templatePath('layout') %}
{% trans_default_domain ea.i18n.translationDomain %}

{% block body_id entities|length > 0 ? 'ea-index-' ~ entities|first.name : '' %}
{% block body_class 'ea-index' ~ (entities|length > 0 ? ' ea-index-' ~ entities|first.name : '') %}
Expand Down

0 comments on commit ed9e8c0

Please sign in to comment.