[BUGFIX] Preserve multivalue property sorting in form fields 23/56723/2
authorRalf Zimmermann <ralf.zimmermann@tritum.de>
Thu, 22 Feb 2018 18:30:18 +0000 (19:30 +0100)
committerFrank Naegler <frank.naegler@typo3.org>
Thu, 19 Apr 2018 10:00:12 +0000 (12:00 +0200)
This ensures e.g. the order of select options is preserved between
form edits.

Background: If you put "{"5":"5","4":"4"}" into JavaScript the order is
arbitrary. Internally this issue is already covered by the form editor
by transforming such values into some meta structure like
"[{'_label': '5', '_value': '5'}, ]{'_label': '4', '_value': '4'}".

This fails if some multivalue properties are already set, and such a
formDefinition is opened by the form editor: "{"5":"5","4":"4"}" becomes
"{"4":"4","5":"5"}".

This fix converts such multivalue properties right before this data is
interpreted by JavaScript.

Resolves: #83911
Releases: master, 8.7
Change-Id: Ife5f6be959958fcf9a2cf5c3942b2549f33287a1
Reviewed-on: https://review.typo3.org/55865
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Tobi Kretschmann <tobi@tobishome.de>
Reviewed-by: Mathias Schreiber <mathias.schreiber@typo3.com>
Tested-by: Mathias Schreiber <mathias.schreiber@typo3.com>
Reviewed-by: Kay Strobach <typo3@kay-strobach.de>
Tested-by: Kay Strobach <typo3@kay-strobach.de>
Reviewed-by: Frank Naegler <frank.naegler@typo3.org>
Tested-by: Frank Naegler <frank.naegler@typo3.org>
Reviewed-on: https://review.typo3.org/56723

typo3/sysext/form/Classes/Controller/FormEditorController.php
typo3/sysext/form/Classes/Mvc/Property/TypeConverter/FormDefinitionArrayConverter.php [new file with mode: 0644]
typo3/sysext/form/Classes/Property/TypeConverter/FormDefinitionArrayConverter.php [deleted file]
typo3/sysext/form/Tests/Unit/Controller/FormEditorControllerTest.php
typo3/sysext/form/Tests/Unit/Mvc/Property/TypeConverter/FormDefinitionArrayConverterTest.php [new file with mode: 0644]
typo3/sysext/form/Tests/Unit/Property/TypeConverter/FormDefinitionArrayConverterTest.php [deleted file]
typo3/sysext/form/ext_localconf.php

index ec4b1ec..91b4413 100644 (file)
@@ -83,6 +83,7 @@ class FormEditorController extends AbstractBackendController
         $configurationService = $this->objectManager->get(ConfigurationService::class);
         $this->prototypeConfiguration = $configurationService->getPrototypeConfiguration($prototypeName);
 
+        $formDefinition = $this->transformFormDefinitionForFormEditor($formDefinition);
         $formEditorDefinitions = $this->getFormEditorDefinitions();
 
         $formEditorAppInitialData = [
@@ -428,6 +429,97 @@ class FormEditorController extends AbstractBackendController
     }
 
     /**
+     * @param array $formDefinition
+     * @return array
+     */
+    protected function transformFormDefinitionForFormEditor(array $formDefinition): array
+    {
+        $multiValueProperties = [];
+        foreach ($this->prototypeConfiguration['formElementsDefinition'] as $type => $configuration) {
+            if (!isset($configuration['formEditor']['editors'])) {
+                continue;
+            }
+            foreach ($configuration['formEditor']['editors'] as $editorConfiguration) {
+                if ($editorConfiguration['templateName'] === 'Inspector-PropertyGridEditor') {
+                    $multiValueProperties[$type][] = $editorConfiguration['propertyPath'];
+                }
+            }
+        }
+
+        return $this->transformMultiValueElementsForFormEditor($formDefinition, $multiValueProperties);
+    }
+
+    /**
+     * Some data needs a transformation before it can be used by the
+     * form editor. This rules for multivalue elements like select
+     * elements. To ensure the right sorting if the data goes into
+     * javascript, we need to do transformations:
+     *
+     * [
+     *   '5' => '5',
+     *   '4' => '4',
+     *   '3' => '3'
+     * ]
+     *
+     *
+     * This method transform this into:
+     *
+     * [
+     *   [
+     *     _label => '5'
+     *     _value => 5
+     *   ],
+     *   [
+     *     _label => '4'
+     *     _value => 4
+     *   ],
+     *   [
+     *     _label => '3'
+     *     _value => 3
+     *   ],
+     * ]
+     *
+     * @param array $formDefinition
+     * @param array $multiValueProperties
+     * @return array
+     */
+    protected function transformMultiValueElementsForFormEditor(
+        array $formDefinition,
+        array $multiValueProperties
+    ): array {
+        $output = $formDefinition;
+        foreach ($formDefinition as $key => $value) {
+            if (isset($value['type']) && array_key_exists($value['type'], $multiValueProperties)) {
+                $multiValuePropertiesForType = $multiValueProperties[$value['type']];
+                foreach ($multiValuePropertiesForType as $multiValueProperty) {
+                    if (!ArrayUtility::isValidPath($value, $multiValueProperty, '.')) {
+                        continue;
+                    }
+                    $multiValuePropertyData = ArrayUtility::getValueByPath($value, $multiValueProperty, '.');
+                    if (!is_array($multiValuePropertyData)) {
+                        continue;
+                    }
+                    $newMultiValuePropertyData = [];
+                    foreach ($multiValuePropertyData as $k => $v) {
+                        $newMultiValuePropertyData[] = [
+                            '_label' => $v,
+                            '_value' => $k
+                        ];
+                    }
+                    $value = ArrayUtility::setValueByPath($value, $multiValueProperty, $newMultiValuePropertyData, '.');
+                }
+            }
+
+            $output[$key] = $value;
+            if (is_array($value)) {
+                $output[$key] = $this->transformMultiValueElementsForFormEditor($value, $multiValueProperties);
+            }
+        }
+
+        return $output;
+    }
+
+    /**
      * Returns the current BE user.
      *
      * @return BackendUserAuthentication
diff --git a/typo3/sysext/form/Classes/Mvc/Property/TypeConverter/FormDefinitionArrayConverter.php b/typo3/sysext/form/Classes/Mvc/Property/TypeConverter/FormDefinitionArrayConverter.php
new file mode 100644 (file)
index 0000000..34cccb2
--- /dev/null
@@ -0,0 +1,110 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Form\Mvc\Property\TypeConverter;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\ArrayUtility;
+use TYPO3\CMS\Extbase\Property\Exception as PropertyException;
+use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
+use TYPO3\CMS\Extbase\Property\TypeConverter\AbstractTypeConverter;
+use TYPO3\CMS\Form\Type\FormDefinitionArray;
+
+/**
+ * Converter for form definition arrays
+ *
+ * @internal
+ */
+class FormDefinitionArrayConverter extends AbstractTypeConverter
+{
+    /**
+     * @var array<string>
+     */
+    protected $sourceTypes = ['string'];
+
+    /**
+     * @var string
+     */
+    protected $targetType = FormDefinitionArray::class;
+
+    /**
+     * @var int
+     */
+    protected $priority = 10;
+
+    /**
+     * Convert from $source to $targetType, a noop if the source is an array.
+     * If it is an empty string it will be converted to an empty array.
+     *
+     * @param string $source
+     * @param string $targetType
+     * @param array $convertedChildProperties
+     * @param PropertyMappingConfigurationInterface $configuration
+     * @return FormDefinitionArray
+     * @throws PropertyException
+     */
+    public function convertFrom($source, $targetType, array $convertedChildProperties = [], PropertyMappingConfigurationInterface $configuration = null)
+    {
+        $rawFormDefinitionArray = json_decode($source, true);
+
+        if (json_last_error() !== JSON_ERROR_NONE) {
+            throw new PropertyException('Unable to decode JSON source: ' . json_last_error_msg(), 1512578002);
+        }
+
+        $rawFormDefinitionArray = ArrayUtility::stripTagsFromValuesRecursive($rawFormDefinitionArray);
+        $rawFormDefinitionArray = $this->transformMultiValueElementsForFormFramework($rawFormDefinitionArray);
+        $formDefinitionArray = new FormDefinitionArray($rawFormDefinitionArray);
+
+        return $formDefinitionArray;
+    }
+
+    /**
+     * Some data which is build by the form editor needs a transformation before
+     * it can be used by the framework.
+     * Multivalue elements like select elements produce data like:
+     *
+     * [
+     *   _label => 'label'
+     *   _value => 'value'
+     * ]
+     *
+     * This method transform this into:
+     *
+     * [
+     *   'value' => 'label'
+     * ]
+     *
+     * @param array $input
+     * @return array
+     */
+    protected function transformMultiValueElementsForFormFramework(array $input): array
+    {
+        $output = [];
+
+        foreach ($input as $key => $value) {
+            if (is_int($key) && is_array($value) && isset($value['_label']) && isset($value['_value'])) {
+                $key = $value['_value'];
+                $value = $value['_label'];
+            }
+
+            if (is_array($value)) {
+                $output[$key] = $this->transformMultiValueElementsForFormFramework($value);
+            } else {
+                $output[$key] = $value;
+            }
+        }
+
+        return $output;
+    }
+}
diff --git a/typo3/sysext/form/Classes/Property/TypeConverter/FormDefinitionArrayConverter.php b/typo3/sysext/form/Classes/Property/TypeConverter/FormDefinitionArrayConverter.php
deleted file mode 100644 (file)
index 4a8089e..0000000
+++ /dev/null
@@ -1,110 +0,0 @@
-<?php
-declare(strict_types=1);
-namespace TYPO3\CMS\Form\Property\TypeConverter;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-use TYPO3\CMS\Core\Utility\ArrayUtility;
-use TYPO3\CMS\Extbase\Property\Exception as PropertyException;
-use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
-use TYPO3\CMS\Extbase\Property\TypeConverter\AbstractTypeConverter;
-use TYPO3\CMS\Form\Type\FormDefinitionArray;
-
-/**
- * Converter for form definition arrays
- *
- * @internal
- */
-class FormDefinitionArrayConverter extends AbstractTypeConverter
-{
-    /**
-     * @var array<string>
-     */
-    protected $sourceTypes = ['string'];
-
-    /**
-     * @var string
-     */
-    protected $targetType = FormDefinitionArray::class;
-
-    /**
-     * @var int
-     */
-    protected $priority = 10;
-
-    /**
-     * Convert from $source to $targetType, a noop if the source is an array.
-     * If it is an empty string it will be converted to an empty array.
-     *
-     * @param string $source
-     * @param string $targetType
-     * @param array $convertedChildProperties
-     * @param PropertyMappingConfigurationInterface $configuration
-     * @return FormDefinitionArray
-     * @throws PropertyException
-     */
-    public function convertFrom($source, $targetType, array $convertedChildProperties = [], PropertyMappingConfigurationInterface $configuration = null)
-    {
-        $rawFormDefinitionArray = json_decode($source, true);
-
-        if (json_last_error() !== JSON_ERROR_NONE) {
-            throw new PropertyException('Unable to decode JSON source: ' . json_last_error_msg(), 1512578002);
-        }
-
-        $rawFormDefinitionArray = ArrayUtility::stripTagsFromValuesRecursive($rawFormDefinitionArray);
-        $rawFormDefinitionArray = $this->convertJsonArrayToAssociativeArray($rawFormDefinitionArray);
-        $formDefinitionArray = new FormDefinitionArray($rawFormDefinitionArray);
-
-        return $formDefinitionArray;
-    }
-
-    /**
-     * Some data which is build by the form editor needs a transformation before
-     * it can be used by the framework.
-     * Multivalue elements like select elements produce data like:
-     *
-     * [
-     *   _label => 'label'
-     *   _value => 'value'
-     * ]
-     *
-     * This method transform this into:
-     *
-     * [
-     *   'value' => 'label'
-     * ]
-     *
-     * @param array $input
-     * @return array
-     */
-    protected function convertJsonArrayToAssociativeArray(array $input): array
-    {
-        $output = [];
-
-        foreach ($input as $key => $value) {
-            if (is_int($key) && is_array($value) && isset($value['_label']) && isset($value['_value'])) {
-                $key = $value['_value'];
-                $value = $value['_label'];
-            }
-
-            if (is_array($value)) {
-                $output[$key] = $this->convertJsonArrayToAssociativeArray($value);
-            } else {
-                $output[$key] = $value;
-            }
-        }
-
-        return $output;
-    }
-}
index a40315f..3088a0e 100644 (file)
@@ -395,4 +395,86 @@ class FormEditorControllerTest extends \TYPO3\TestingFramework\Core\Unit\UnitTes
 
         $mockController->_call('renderFormEditorTemplates', []);
     }
+
+    /**
+     * @test
+     */
+    public function transformMultiValueElementsForFormEditorConvertMultiValueDataIntoMetaData()
+    {
+        $mockController = $this->getAccessibleMock(FormEditorController::class, [
+            'dummy'
+        ], [], '', false);
+
+        $input = [
+            0 => [
+                'bar' => 'baz',
+            ],
+            1 => [
+                'type' => 'SOMEELEMENT',
+                'properties' => [
+                    'options' => [
+                        5 => '5',
+                        4 => '4',
+                        3 => '3',
+                        2 => '2',
+                        1 => '1',
+                    ],
+                ],
+            ],
+            2 => [
+                0 => [
+                    'type' => 'TEST',
+                    'properties' => [
+                        'options' => [
+                            5 => '5',
+                            4 => '4',
+                            3 => '3',
+                            2 => '2',
+                            1 => '1',
+                        ],
+                    ],
+                ],
+            ],
+        ];
+
+        $multiValueProperties = [
+            'TEST' => [
+                0 => 'properties.options',
+            ],
+        ];
+
+        $expected = [
+            0 => [
+                'bar' => 'baz',
+            ],
+            1 => [
+                'type' => 'SOMEELEMENT',
+                'properties' => [
+                    'options' => [
+                        5 => '5',
+                        4 => '4',
+                        3 => '3',
+                        2 => '2',
+                        1 => '1',
+                    ],
+                ],
+            ],
+            2 => [
+                0 => [
+                    'type' => 'TEST',
+                    'properties' => [
+                        'options' => [
+                            ['_label' => '5', '_value' => 5],
+                            ['_label' => '4', '_value' => 4],
+                            ['_label' => '3', '_value' => 3],
+                            ['_label' => '2', '_value' => 2],
+                            ['_label' => '1', '_value' => 1],
+                        ],
+                    ],
+                ],
+            ],
+        ];
+
+        $this->assertSame($expected, $mockController->_call('transformMultiValueElementsForFormEditor', $input, $multiValueProperties));
+    }
 }
diff --git a/typo3/sysext/form/Tests/Unit/Mvc/Property/TypeConverter/FormDefinitionArrayConverterTest.php b/typo3/sysext/form/Tests/Unit/Mvc/Property/TypeConverter/FormDefinitionArrayConverterTest.php
new file mode 100644 (file)
index 0000000..64000e3
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+namespace TYPO3\CMS\Form\Tests\Unit\Mvc\Property\TypeConverter;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Form\Mvc\Property\TypeConverter\FormDefinitionArrayConverter;
+use TYPO3\CMS\Form\Type\FormDefinitionArray;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case for TYPO3\CMS\Form\Mvc\Property\TypeConverter\FormDefinitionArrayConverter
+ */
+class FormDefinitionArrayConverterTest extends UnitTestCase
+{
+    /**
+     * @test
+     */
+    public function convertsJsonStringToFormDefinitionArray()
+    {
+        $typeConverter = new FormDefinitionArrayConverter();
+        $source = '{"francine":"stan","enabled":false,"properties":{"options":[{"_label":"label","_value":"value"}]}}';
+        $expected = [
+            'francine' => 'stan',
+            'enabled' => false,
+            'properties' => [
+                'options' => [
+                    'value' => 'label',
+                ],
+            ],
+        ];
+        $result = $typeConverter->convertFrom($source, FormDefinitionArray::class);
+
+        $this->assertInstanceOf(FormDefinitionArray::class, $result);
+        $this->assertSame($expected, $result->getArrayCopy());
+    }
+}
diff --git a/typo3/sysext/form/Tests/Unit/Property/TypeConverter/FormDefinitionArrayConverterTest.php b/typo3/sysext/form/Tests/Unit/Property/TypeConverter/FormDefinitionArrayConverterTest.php
deleted file mode 100644 (file)
index 39670c5..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-<?php
-namespace TYPO3\CMS\Form\Tests\Unit\Property\TypeConverter;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-use TYPO3\CMS\Form\Property\TypeConverter\FormDefinitionArrayConverter;
-use TYPO3\CMS\Form\Type\FormDefinitionArray;
-use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
-
-/**
- * Test case for TYPO3\CMS\Form\Property\TypeConverter\FormDefinitionArrayConverter
- */
-class FormDefinitionArrayConverterTest extends UnitTestCase
-{
-    /**
-     * @test
-     */
-    public function convertsJsonStringToFormDefinitionArray()
-    {
-        $typeConverter = new FormDefinitionArrayConverter();
-        $source = '{"francine":"stan","enabled":false,"properties":{"options":[{"_label":"label","_value":"value"}]}}';
-        $expected = [
-            'francine' => 'stan',
-            'enabled' => false,
-            'properties' => [
-                'options' => [
-                    'value' => 'label',
-                ],
-            ],
-        ];
-        $result = $typeConverter->convertFrom($source, FormDefinitionArray::class);
-
-        $this->assertInstanceOf(FormDefinitionArray::class, $result);
-        $this->assertSame($expected, $result->getArrayCopy());
-    }
-}
index a23ef83..a2da6d0 100644 (file)
@@ -74,7 +74,7 @@ call_user_func(function () {
     }
 
     \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerTypeConverter(
-        \TYPO3\CMS\Form\Property\TypeConverter\FormDefinitionArrayConverter::class
+        \TYPO3\CMS\Form\Mvc\Property\TypeConverter\FormDefinitionArrayConverter::class
     );
 
     // Register "formvh:" namespace