Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[POC] Replace x-editor #8221

Open
wants to merge 1 commit into
base: 5.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@ module.exports = {
2,
],
'import/no-webpack-loader-syntax': 'off',
'lines-between-class-members': { 'properties': 'off', 'methods': 'always' }
},
};
1 change: 1 addition & 0 deletions assets/js/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ const Admin = {

setup_xeditable(subject) {
Admin.log('[core|setup_xeditable] configure xeditable on', subject);
return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intended? Either leave a comment with a TODO or remove the code below

jQuery('.x-editable', subject).editable({
emptyclass: 'editable-empty btn btn-sm btn-default',
emptytext: '<i class="fas fa-pencil-alt"></i>',
Expand Down
54 changes: 54 additions & 0 deletions assets/js/controllers/editable_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*!
* This file is part of the Sonata Project package.
*
* (c) Thomas Rabaix <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { Controller } from '@hotwired/stimulus'
import parseHTML from '../parse_html'

export default class extends Controller {
submit(form, controller) {
const options = {
body: new FormData(form),
method: 'POST',
};

fetch(form.getAttribute('action') || '', options)
.then((response) => {
if (response.ok) {
return response.text();
}

return Promise.reject(response.text());
})
.then((response) => {
this.element.replaceWith(parseHTML(response));
this.hide();
}).catch((response) => {
controller.replaceWith(parseHTML(response));
});
}

show() {
fetch(this.element.dataset.url)
.then((response) => response.text())
.then((response) => {
$(this.element).popover({
container: 'body',
placement: 'top',
html: true,
content: parseHTML(response),
});

$(this.element).popover('show');
});
}

hide() {
$(this.element).popover('destroy');
}
}
25 changes: 25 additions & 0 deletions assets/js/controllers/editable_form_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*!
* This file is part of the Sonata Project package.
*
* (c) Thomas Rabaix <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { Controller } from '@hotwired/stimulus'

export default class extends Controller {
static targets = ['form', 'submitter'];
static outlets = ['editable'];

submit(event) {
this.editableOutlet.submit(this.formTarget, this.element);
this.submitterTarget.disabled = true;
event.preventDefault();
}

cancel() {
this.editableOutlet.hide();
}
}
15 changes: 15 additions & 0 deletions assets/js/parse_html.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*!
* This file is part of the Sonata Project package.
*
* (c) Thomas Rabaix <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

export default function parseHTML(html) {
const template = document.createElement('template');
template.innerHTML = html;

return document.importNode(template.content, true);
}
93 changes: 26 additions & 67 deletions src/Action/SetObjectFieldValueAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,16 @@
namespace Sonata\AdminBundle\Action;

use Sonata\AdminBundle\Exception\BadRequestParamHttpException;
use Sonata\AdminBundle\FieldDescription\FieldDescriptionInterface;
use Sonata\AdminBundle\Form\DataTransformerResolverInterface;
use Sonata\AdminBundle\Request\AdminFetcherInterface;
use Sonata\AdminBundle\Twig\RenderElementRuntime;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Form\FormRenderer;
use Twig\Environment;

final class SetObjectFieldValueAction
Expand All @@ -35,9 +33,9 @@
public function __construct(
private Environment $twig,
private AdminFetcherInterface $adminFetcher,
private ValidatorInterface $validator,

Check failure on line 36 in src/Action/SetObjectFieldValueAction.php

View workflow job for this annotation

GitHub Actions / PHPStan

Property Sonata\AdminBundle\Action\SetObjectFieldValueAction::$validator is never read, only written.
private DataTransformerResolverInterface $resolver,

Check failure on line 37 in src/Action/SetObjectFieldValueAction.php

View workflow job for this annotation

GitHub Actions / PHPStan

Property Sonata\AdminBundle\Action\SetObjectFieldValueAction::$resolver is never read, only written.
private PropertyAccessorInterface $propertyAccessor,

Check failure on line 38 in src/Action/SetObjectFieldValueAction.php

View workflow job for this annotation

GitHub Actions / PHPStan

Property Sonata\AdminBundle\Action\SetObjectFieldValueAction::$propertyAccessor is never read, only written.
?RenderElementRuntime $renderElementRuntime = null,
) {
// NEXT_MAJOR: Remove the deprecation and restrict param constructor to RenderElementRuntime.
Expand All @@ -55,27 +53,14 @@
/**
* @throws NotFoundHttpException
*/
public function __invoke(Request $request): JsonResponse
public function __invoke(Request $request): Response
{
try {
$admin = $this->adminFetcher->get($request);
} catch (\InvalidArgumentException $e) {
throw new NotFoundHttpException($e->getMessage());
}

// alter should be done by using a post method
if (!$request->isXmlHttpRequest()) {
return new JsonResponse('Expected an XmlHttpRequest request header', Response::HTTP_METHOD_NOT_ALLOWED);
}

if (Request::METHOD_POST !== $request->getMethod()) {
return new JsonResponse(\sprintf(
'Invalid request method given "%s", %s expected',
$request->getMethod(),
Request::METHOD_POST
), Response::HTTP_METHOD_NOT_ALLOWED);
}

$objectId = $request->get('objectId');
if (!\is_string($objectId) && !\is_int($objectId)) {
throw new BadRequestParamHttpException('objectId', ['string', 'int'], $objectId);
Expand Down Expand Up @@ -106,66 +91,40 @@
}

$fieldDescription = $admin->getListFieldDescription($field);

if (true !== $fieldDescription->getOption('editable')) {
return new JsonResponse('The field cannot be edited, editable option must be set to true', Response::HTTP_BAD_REQUEST);
}

$propertyPath = new PropertyPath($field);
$rootObject = $object;

// If property path has more than 1 element, take the last object in order to validate it
$parent = $propertyPath->getParent();
if (null !== $parent) {
$object = $this->propertyAccessor->getValue($object, $parent);

$elements = $propertyPath->getElements();
$field = end($elements);
\assert(\is_string($field));

$propertyPath = new PropertyPath($field);
}

$value = $request->get('value');

if ('' === $value) {
$this->propertyAccessor->setValue($object, $propertyPath, null);
} else {
$dataTransformer = $this->resolver->resolve($fieldDescription, $admin->getModelManager());

if ($dataTransformer instanceof DataTransformerInterface) {
$value = $dataTransformer->reverseTransform($value);
}

if (null === $value && \in_array($fieldDescription->getType(), [FieldDescriptionInterface::TYPE_CHOICE, FieldDescriptionInterface::TYPE_ENUM], true)) {
return new JsonResponse(\sprintf(
'Edit failed, object with id "%s" not found in association "%s".',
$objectId,
$field
), Response::HTTP_NOT_FOUND);
}

$this->propertyAccessor->setValue($object, $propertyPath, $value);
}

$violations = $this->validator->validate($object);
$admin->setSubject($object);
$formBuilder = $admin->getFormContractor()->getFormBuilder('editable', ['data_class' => $admin->getClass()]);
$formBuilder->add($fieldDescription->getFieldName());

if (\count($violations) > 0) {
$messages = [];
$form = $formBuilder->getForm();
$form->setData($object);
$form->handleRequest($admin->getRequest());

foreach ($violations as $violation) {
$messages[] = $violation->getMessage();
}
if ($form->isSubmitted() && $form->isValid()) {
$admin->update($object);

return new JsonResponse(implode("\n", $messages), Response::HTTP_BAD_REQUEST);
return new Response(
$this->renderElementRuntime->renderListElement($this->twig, $object, $fieldDescription),
Response::HTTP_OK
);
}

\assert(\is_object($object));
$admin->update($object);
$status = $form->isSubmitted() && !$form->isValid()
? Response::HTTP_BAD_REQUEST
: Response::HTTP_OK;

// render the widget
$content = $this->renderElementRuntime->renderListElement($this->twig, $rootObject, $fieldDescription);
$view = $form->createView();
$renderer = $this->twig->getRuntime(FormRenderer::class);
$renderer->setTheme($view, $admin->getFormTheme());

return new JsonResponse($content, Response::HTTP_OK);
return new Response($this->twig->render('@SonataAdmin/Action/set_object_field_value.html.twig', [
'admin' => $admin,
'field_description' => $fieldDescription,
'object' => $object,
'form' => $view,
]), $status);
}
}
25 changes: 25 additions & 0 deletions src/Resources/views/Action/set_object_field_value.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{% set route = app.request.attributes.get('_route') %}
{% set params = app.request.attributes.get('_route_params')|merge(app.request.query.all) %}

<form action="{{ path(route, params) }}" method="POST" class="form-inline"
{{ stimulus_controller('editable-form', {}, {}, { 'editable': '#editable_' ~ field_description.name }) }}

Check failure on line 5 in src/Resources/views/Action/set_object_field_value.html.twig

View workflow job for this annotation

GitHub Actions / Twig files

Unknown "stimulus_controller" function.
{{ stimulus_action('editable-form', 'submit', 'submit') }}
{{ stimulus_target('editable-form', 'form') }}>
{{ form_errors(form) }}
<div class="control-group form-group">
<div>
<div class="editable-input">
{{ form_widget(form[field_description.fieldName], { 'label': false }) }}
{{ form_errors(form[field_description.fieldName]) }}
</div>
<div class="editable-buttons">
<button type="submit" class="btn btn-primary btn-sm" {{ stimulus_target('editable-form', 'submitter') }}>
<i class="glyphicon glyphicon-ok"></i>
</button>
<button type="button" class="btn btn-default btn-sm" {{ stimulus_action('editable-form', 'cancel', 'click') }}>
<i class="glyphicon glyphicon-remove"></i>
</button>
</div>
</div>
</div>
</form>
32 changes: 4 additions & 28 deletions src/Resources/views/CRUD/base_list_field.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,7 @@
</a>
{% else %}
{% set is_editable = field_description.option('editable', false) and admin.hasAccess('edit', object) %}
{% if is_editable and field_description.option('multiple', false) and value is iterable %}
{# multiple editable field should be real multiple #}
{# https://vitalets.github.io/x-editable/docs.html#checklist #}
{% set x_editable_type = 'checklist' %}
{% else %}
{% set x_editable_type = field_description.type|sonata_xeditable_type %}
{% endif %}

{% if is_editable and x_editable_type %}
{% if is_editable %}
{% set url = path(
'sonata_admin_set_object_field_value',
{
Expand All @@ -56,32 +48,16 @@
+ app.request.query.all|default({})
) %}

{% if field_description.type == constant('Sonata\\AdminBundle\\FieldDescription\\FieldDescriptionInterface::TYPE_DATE') and value is not empty %}
{# it is a x-editable format https://vitalets.github.io/x-editable/docs.html#date #}
{% set data_value = value|date('Y-m-d', options.timezone|default(null)) %}
{% elseif field_description.type == constant('Sonata\\AdminBundle\\FieldDescription\\FieldDescriptionInterface::TYPE_BOOLEAN') and value is empty %}
{% set data_value = 0 %}
{% elseif field_description.type == constant('Sonata\\AdminBundle\\FieldDescription\\FieldDescriptionInterface::TYPE_ENUM') and value is not empty %}
{% set data_value = value.value %}
{% elseif value is iterable %}
{% set data_value = value|json_encode %}
{% else %}
{% set data_value = value %}
{% endif %}

<span {% block field_span_attributes %}class="x-editable"
data-type="{{ x_editable_type }}"
data-value="{{ data_value }}"
<span {% block field_span_attributes %}id="editable_{{ field_description.name }}" class="x-editable"
{% if field_description.label is not same as(false) %}
{% if field_description.translationDomain is same as(false) %}
data-title="{{ field_description.label }}"
{% else %}
data-title="{{ field_description.label|trans({}, field_description.translationDomain) }}"
{% endif %}
{% endif %}
{% if field_description.type == constant('Sonata\\AdminBundle\\FieldDescription\\FieldDescriptionInterface::TYPE_DATE') %}
data-format="yyyy-mm-dd"
{% endif %}
{{ stimulus_controller('editable') }}

Check failure on line 59 in src/Resources/views/CRUD/base_list_field.html.twig

View workflow job for this annotation

GitHub Actions / Twig files

Unknown "stimulus_controller" function.
{{ stimulus_action('editable', 'show', 'click') }}
data-pk="{{ admin.id(object) }}"
data-url="{{ url }}" {% endblock %}>
{{ block('field') }}
Expand Down
12 changes: 0 additions & 12 deletions src/Resources/views/CRUD/list_boolean.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,6 @@ file that was distributed with this source code.

{% extends get_admin_template('base_list_field', admin.code) %}

{% set is_editable = field_description.option('editable', false) and admin.hasAccess('edit', object) %}
{% set x_editable_type = field_description.type|sonata_xeditable_type %}

{% block field_span_attributes %}
{% if is_editable and x_editable_type %}
{% apply spaceless %}
{{ parent() }}
data-source="[{value: 0, text: '{%- trans from 'SonataAdminBundle' %}label_type_no{% endtrans -%}'},{value: 1, text: '{%- trans from 'SonataAdminBundle' %}label_type_yes{% endtrans -%}'}]"
{% endapply %}
{% endif %}
{% endblock %}

{% block field %}
{%- include '@SonataAdmin/CRUD/display_boolean.html.twig' with {
value: value,
Expand Down
15 changes: 0 additions & 15 deletions src/Resources/views/CRUD/list_choice.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,6 @@ file that was distributed with this source code.

{% extends get_admin_template('base_list_field', admin.code) %}

{% set is_editable =
field_description.option('editable', false) and
admin.hasAccess('edit', object)
%}
{% set x_editable_type = field_description.type|sonata_xeditable_type %}

{% block field_span_attributes %}
{% if is_editable and x_editable_type %}
{% apply spaceless %}
{{ parent() }}
data-source="{{ field_description|sonata_xeditable_choices|json_encode }}"
{% endapply %}
{% endif %}
{% endblock %}

{# NEXT_MAJOR: Remove the fallback on catalogue #}
{% block field %}
{%- include '@SonataAdmin/CRUD/display_choice.html.twig' with {
Expand Down
15 changes: 0 additions & 15 deletions src/Resources/views/CRUD/list_enum.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,6 @@ file that was distributed with this source code.

{% extends get_admin_template('base_list_field', admin.code) %}

{% set is_editable =
field_description.option('editable', false) and
admin.hasAccess('edit', object)
%}
{% set x_editable_type = field_description.type|sonata_xeditable_type %}

{% block field_span_attributes %}
{% if is_editable and x_editable_type %}
{% apply spaceless %}
{{ parent() }}
data-source="{{ field_description|sonata_xeditable_choices|json_encode }}"
{% endapply %}
{% endif %}
{% endblock %}

{% block field %}
{%- include '@SonataAdmin/CRUD/display_enum.html.twig' with {
value: value,
Expand Down
Loading
Loading