Skip to content

Commit f379763

Browse files
committed
Embedded crud
1 parent 0129eb9 commit f379763

File tree

8 files changed

+211
-2
lines changed

8 files changed

+211
-2
lines changed

src/Controller/AbstractCrudController.php

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace EasyCorp\Bundle\EasyAdminBundle\Controller;
44

55
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
6+
use Doctrine\DBAL\Types\Type;
67
use Doctrine\ORM\EntityManagerInterface;
78
use Doctrine\ORM\QueryBuilder;
89
use Doctrine\Persistence\ManagerRegistry;
@@ -129,6 +130,35 @@ public function index(AdminContext $context)
129130
$fields = FieldCollection::new($this->configureFields(Crud::PAGE_INDEX));
130131
$filters = $this->container->get(FilterFactory::class)->create($context->getCrud()->getFiltersConfig(), $fields, $context->getEntity());
131132
$queryBuilder = $this->createIndexQueryBuilder($context->getSearch(), $context->getEntity(), $fields, $filters);
133+
134+
/** @var array|null $embedContext */
135+
$embedContext = $context->getRequest()->query->all('embedContext');
136+
if (\array_key_exists('mappedBy', $embedContext)) {
137+
$filterProperty = $embedContext['mappedBy'];
138+
$filterValue = $embedContext['embeddedIn'] ?? null;
139+
if (null !== $filterValue) {
140+
// Use the parameter conversion capabilities of Doctrine
141+
$metadata = $queryBuilder->getEntityManager()->getClassMetadata($context->getEntity()->getFqcn());
142+
$relatedEntityFqcn = $metadata->getAssociationTargetClass($filterProperty);
143+
$relatedEntityMetadata = $queryBuilder->getEntityManager()->getClassMetadata($relatedEntityFqcn);
144+
$type = $relatedEntityMetadata->getTypeOfField($metadata->getSingleIdentifierFieldName());
145+
if (Type::hasType($type)) {
146+
$doctrineType = Type::getType($type);
147+
$platform = $queryBuilder->getEntityManager()->getConnection()->getDatabasePlatform();
148+
$filterValue = $doctrineType->convertToDatabaseValue($filterValue, $platform);
149+
}
150+
$rootAlias = current($queryBuilder->getRootAliases());
151+
$queryBuilder
152+
->andWhere(':filterValue MEMBER OF '.sprintf('%s.%s', $rootAlias, $filterProperty))
153+
->setParameter('filterValue', $filterValue)
154+
;
155+
}
156+
$field = $fields->getByProperty($filterProperty);
157+
if ($field instanceof FieldDto) {
158+
$fields->unset($field);
159+
}
160+
}
161+
132162
$paginator = $this->container->get(PaginatorFactory::class)->create($queryBuilder);
133163

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

148178
$responseParameters = $this->configureResponseParameters(KeyValueStore::new([
149179
'pageName' => Crud::PAGE_INDEX,
150-
'templateName' => 'crud/index',
180+
'templateName' => null !== $embedContext ? 'crud/embedded' : 'crud/index',
151181
'entities' => $entities,
152182
'paginator' => $paginator,
153183
'global_actions' => $actions->getGlobalActions(),

src/Field/EmbedField.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace EasyCorp\Bundle\EasyAdminBundle\Field;
4+
5+
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface;
6+
use EasyCorp\Bundle\EasyAdminBundle\Form\Type\EmbedType;
7+
8+
final class EmbedField implements FieldInterface
9+
{
10+
use FieldTrait;
11+
12+
public static function new(string $propertyName, ?string $label = null): self
13+
{
14+
return (new self())
15+
->setProperty($propertyName)
16+
->setLabel($label)
17+
->setTemplateName('crud/field/embed')
18+
->setFormType(EmbedType::class)
19+
->setFormTypeOption('mapped', false)
20+
->onlyWhenUpdating()
21+
;
22+
}
23+
}

src/Form/Type/EmbedType.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace EasyCorp\Bundle\EasyAdminBundle\Form\Type;
4+
5+
use Symfony\Component\Form\AbstractType;
6+
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
7+
8+
class EmbedType extends AbstractType
9+
{
10+
public function getBlockPrefix(): string
11+
{
12+
return 'ea_embedded_collection';
13+
}
14+
15+
public function getParent(): string
16+
{
17+
return CollectionType::class;
18+
}
19+
}

src/Registry/TemplateRegistry.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ final class TemplateRegistry
1414
'flash_messages' => '@EasyAdmin/flash_messages.html.twig',
1515
'crud/paginator' => '@EasyAdmin/crud/paginator.html.twig',
1616
'crud/index' => '@EasyAdmin/crud/index.html.twig',
17+
'crud/embedded' => '@EasyAdmin/crud/embedded.html.twig',
1718
'crud/detail' => '@EasyAdmin/crud/detail.html.twig',
1819
'crud/new' => '@EasyAdmin/crud/new.html.twig',
1920
'crud/edit' => '@EasyAdmin/crud/edit.html.twig',
@@ -35,6 +36,7 @@ final class TemplateRegistry
3536
'crud/field/datetimetz' => '@EasyAdmin/crud/field/datetimetz.html.twig',
3637
'crud/field/decimal' => '@EasyAdmin/crud/field/decimal.html.twig',
3738
'crud/field/email' => '@EasyAdmin/crud/field/email.html.twig',
39+
'crud/field/embed' => '@EasyAdmin/crud/field/embed.html.twig',
3840
'crud/field/float' => '@EasyAdmin/crud/field/float.html.twig',
3941
'crud/field/generic' => '@EasyAdmin/crud/field/generic.html.twig',
4042
'crud/field/hidden' => '@EasyAdmin/crud/field/hidden.html.twig',
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #}
2+
{# @var field \EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto #}
3+
{# @var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #}
4+
{% set has_footer = entities|length != 0 %}
5+
{% set has_batch_actions = false %}
6+
{% set some_results_are_hidden = false %}
7+
{% set sort_field_name = app.request.get('sort')|keys|first %}
8+
{% set sort_order = app.request.get('sort')|first %}
9+
10+
{{ block("content", "@EasyAdmin/crud/index.html.twig") }}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #}
2+
{# @var field \EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto #}
3+
{# @var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #}
4+
{% set current_url = ea_url() %}
5+
{% set field = form.vars.ea_vars.field %}
6+
{% set entity = form.vars.ea_vars.entity %}
7+
{% set target_entity = field.doctrineMetadata.get('targetEntity') %}
8+
{% set crudControllers = ea.crudControllers %}
9+
{% set target_entity_crud_fqcn = crudControllers.findCrudFqcnByEntityFqcn(target_entity) %}
10+
11+
{% set url = ea_url().unset('entityId').setController(target_entity_crud_fqcn).setAction('index').set('embedContext', {
12+
mappedBy: field.doctrineMetadata.get('mappedBy'),
13+
embeddedIn: entity.primaryKeyValue
14+
}) %}
15+
16+
{% set id_suffix = '-'~field.property %}
17+
18+
<div id="embed{{ id_suffix }}" class="position-relative embed-loading">
19+
<div class="position-absolute text-center embed-spinner">
20+
<div class="spinner-border text-primary spinner-border-lg mt-2"></div>
21+
</div>
22+
<div class="embed-content"></div>
23+
</div>
24+
25+
<style>
26+
.embed-spinner {
27+
display: none;
28+
top: 50%;
29+
left: 50%;
30+
margin-top: -1rem;
31+
margin-left: -1rem;
32+
z-index: 10;
33+
}
34+
35+
.embed-loading .embed-spinner {
36+
display: block;
37+
}
38+
39+
.embed-loading .embed-content {
40+
opacity: 0.5;
41+
pointer-events: none;
42+
}
43+
</style>
44+
45+
<script>
46+
window.addEventListener('load', () => {
47+
const referrer = window.location.href;
48+
const initialUrl = '{{ url|raw }}'
49+
50+
const embed = document.querySelector('#embed{{ id_suffix }}');
51+
const embedContent = embed.querySelector('.embed-content');
52+
53+
const load = (url) => {
54+
embed.classList.add('embed-loading');
55+
fetch(url)
56+
.then(it => it.text())
57+
.then(it => embedContent.innerHTML = it)
58+
.then(() => {
59+
// override referrer of actions, so we get back to the "main" view afterwards, not the embed
60+
embedContent.querySelectorAll('.actions a').forEach(action => {
61+
const target = new URL(action.href)
62+
target.searchParams.set('referrer', referrer);
63+
action.href = target.toString();
64+
});
65+
66+
// override referrer of actions, so we get back to the "main" view afterwards, not the embed
67+
embedContent.querySelectorAll('.global-actions a').forEach(action => {
68+
const target = new URL(action.href)
69+
const current = new URL(referrer);
70+
target.searchParams.set('relatedEntityId', current.searchParams.get('entityId'));
71+
target.searchParams.set('referrer', referrer);
72+
action.href = target.toString();
73+
})
74+
75+
embedContent.querySelectorAll('.action-delete').forEach((actionElement) => {
76+
actionElement.addEventListener('click', (event) => {
77+
event.preventDefault();
78+
79+
document.querySelector('#modal-delete-button').addEventListener('click', () => {
80+
const deleteFormAction = new URL(actionElement.getAttribute('formaction'));
81+
const deleteForm = document.querySelector('#delete-form');
82+
deleteFormAction.searchParams.set('referrer', referrer);
83+
deleteForm.setAttribute('action', deleteFormAction.toString());
84+
deleteForm.submit();
85+
});
86+
});
87+
});
88+
89+
// intercept sort and pagination
90+
embedContent.querySelectorAll('thead a, .pagination a').forEach(link => {
91+
link.addEventListener('click', evt => {
92+
evt.preventDefault();
93+
load(link.href)
94+
})
95+
})
96+
97+
// intercept search
98+
// embedContent.querySelector('.form-action-search form').addEventListener('submit', evt => {
99+
// evt.preventDefault();
100+
// const data = new FormData(evt.target);
101+
// const params = new URLSearchParams(data).toString()
102+
// const target = new URL(url);
103+
// target.search = params.toString();
104+
// load(target.toString())
105+
// })
106+
107+
// highlight results
108+
// const searchQuery = new URL(url).searchParams.get('query');
109+
// if(searchQuery) {
110+
// $(embedContent).find('table tbody td:not(.actions)').highlight($.merge([searchQuery], searchQuery.split(' ')));
111+
// }
112+
113+
// can be used to re-initialize dynamic content
114+
document.dispatchEvent(new Event('ea.embed.content-loaded'))
115+
})
116+
.finally(() => embed.classList.remove('embed-loading'))
117+
;
118+
}
119+
120+
load(initialUrl);
121+
})
122+
</script>

src/Resources/views/crud/form_theme.html.twig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,3 +879,7 @@
879879
</button>
880880
</div>
881881
{% endblock %}
882+
883+
{% block ea_embedded_collection_row %}
884+
{{ include(ea.templatePath('crud/field/embed')) }}
885+
{% endblock %}

src/Resources/views/crud/index.html.twig

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
{# @var entities \EasyCorp\Bundle\EasyAdminBundle\Collection\EntityCollection #}
33
{# @var paginator \EasyCorp\Bundle\EasyAdminBundle\Orm\EntityPaginator #}
44
{% extends ea.templatePath('layout') %}
5-
{% trans_default_domain ea.i18n.translationDomain %}
65

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

0 commit comments

Comments
 (0)