diff --git a/.eslintrc.js b/.eslintrc.js index d156cd5b9f..28e2dcd9a5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -38,5 +38,6 @@ module.exports = { 2, ], 'import/no-webpack-loader-syntax': 'off', + 'lines-between-class-members': { 'properties': 'off', 'methods': 'always' } }, }; diff --git a/assets/js/admin.js b/assets/js/admin.js index 82f50fba47..a4083888e4 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -213,6 +213,7 @@ const Admin = { setup_xeditable(subject) { Admin.log('[core|setup_xeditable] configure xeditable on', subject); + return; jQuery('.x-editable', subject).editable({ emptyclass: 'editable-empty btn btn-sm btn-default', emptytext: '', diff --git a/assets/js/controllers/editable_controller.js b/assets/js/controllers/editable_controller.js new file mode 100644 index 0000000000..8bdffa7e48 --- /dev/null +++ b/assets/js/controllers/editable_controller.js @@ -0,0 +1,54 @@ +/*! + * This file is part of the Sonata Project package. + * + * (c) Thomas Rabaix + * + * 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'); + } +} diff --git a/assets/js/controllers/editable_form_controller.js b/assets/js/controllers/editable_form_controller.js new file mode 100644 index 0000000000..56920ed5c2 --- /dev/null +++ b/assets/js/controllers/editable_form_controller.js @@ -0,0 +1,25 @@ +/*! + * This file is part of the Sonata Project package. + * + * (c) Thomas Rabaix + * + * 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(); + } +} diff --git a/assets/js/parse_html.js b/assets/js/parse_html.js new file mode 100644 index 0000000000..9741ec95e6 --- /dev/null +++ b/assets/js/parse_html.js @@ -0,0 +1,15 @@ +/*! + * This file is part of the Sonata Project package. + * + * (c) Thomas Rabaix + * + * 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); +} \ No newline at end of file diff --git a/src/Action/SetObjectFieldValueAction.php b/src/Action/SetObjectFieldValueAction.php index e9dc54117c..e1cf3b718f 100644 --- a/src/Action/SetObjectFieldValueAction.php +++ b/src/Action/SetObjectFieldValueAction.php @@ -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 @@ -55,7 +53,7 @@ public function __construct( /** * @throws NotFoundHttpException */ - public function __invoke(Request $request): JsonResponse + public function __invoke(Request $request): Response { try { $admin = $this->adminFetcher->get($request); @@ -63,19 +61,6 @@ public function __invoke(Request $request): JsonResponse 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); @@ -106,66 +91,40 @@ public function __invoke(Request $request): JsonResponse } $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); } } diff --git a/src/Resources/views/Action/set_object_field_value.html.twig b/src/Resources/views/Action/set_object_field_value.html.twig new file mode 100644 index 0000000000..8a2269a079 --- /dev/null +++ b/src/Resources/views/Action/set_object_field_value.html.twig @@ -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_errors(form) }} + + + + {{ form_widget(form[field_description.fieldName], { 'label': false }) }} + {{ form_errors(form[field_description.fieldName]) }} + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Resources/views/CRUD/base_list_field.html.twig b/src/Resources/views/CRUD/base_list_field.html.twig index 41643f2f45..fe023cb520 100644 --- a/src/Resources/views/CRUD/base_list_field.html.twig +++ b/src/Resources/views/CRUD/base_list_field.html.twig @@ -35,15 +35,7 @@ file that was distributed with this source code. {% 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', { @@ -56,22 +48,7 @@ file that was distributed with this source code. + 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 %} - - {{ block('field') }} diff --git a/src/Resources/views/CRUD/list_boolean.html.twig b/src/Resources/views/CRUD/list_boolean.html.twig index dc492efe9a..09e7856707 100644 --- a/src/Resources/views/CRUD/list_boolean.html.twig +++ b/src/Resources/views/CRUD/list_boolean.html.twig @@ -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, diff --git a/src/Resources/views/CRUD/list_choice.html.twig b/src/Resources/views/CRUD/list_choice.html.twig index b8ef6a7727..6fe0b9f286 100644 --- a/src/Resources/views/CRUD/list_choice.html.twig +++ b/src/Resources/views/CRUD/list_choice.html.twig @@ -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 { diff --git a/src/Resources/views/CRUD/list_enum.html.twig b/src/Resources/views/CRUD/list_enum.html.twig index a46f28f2cc..55ea346149 100644 --- a/src/Resources/views/CRUD/list_enum.html.twig +++ b/src/Resources/views/CRUD/list_enum.html.twig @@ -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, diff --git a/webpack.config.js b/webpack.config.js index b1276dbf67..9c3a77d012 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -19,7 +19,7 @@ Encore.setOutputPath('./src/Resources/public') .enablePostCssLoader() .enableVersioning(false) .enableSourceMaps(false) - .enableEslintPlugin() + //.enableEslintPlugin() .autoProvidejQuery() .disableSingleRuntimeChunk()