From f35b145bbaaad49a83265c92e7c9368c65356fe5 Mon Sep 17 00:00:00 2001 From: Oliver Bartsch <bo@cedev.de> Date: Fri, 14 Jun 2024 20:08:27 +0200 Subject: [PATCH] [TASK] Render generator fields as hidden fields in columnsOnly mode When using the "columnsOnly" mode to render just a subset of available fields of a record, while the subset includes a field of TCA type "slug", FormEngine needs to also render configured generator fields. Otherwise the slug fields won't work as expected, e.g. when recalculating. However, since it might be confusing for an editor to get fields rendered, which were not selected, are those fields now rendered as hidden fields. This is done by adding those fields to a hidden palette while omitting duplicates and performing sanitization. Resolves: #104115 Releases: main, 12.4 Change-Id: Ia31571bae9913028e81df910462d76dc4314644c Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/84721 Reviewed-by: Oliver Bartsch <bo@cedev.de> Reviewed-by: Jochen Roth <rothjochen@gmail.com> Reviewed-by: Benni Mack <benni@typo3.org> Tested-by: Benni Mack <benni@typo3.org> Tested-by: Oliver Bartsch <bo@cedev.de> Tested-by: Jochen Roth <rothjochen@gmail.com> Tested-by: core-ci <typo3@b13.com> --- .../Controller/EditDocumentController.php | 15 +++++- .../Form/Container/ListOfFieldsContainer.php | 46 ++++++++++++----- .../Controller/EditDocumentControllerTest.php | 19 ++++--- .../Container/ListOfFieldsContainerTest.php | 50 +++++++++++++++++++ 4 files changed, 107 insertions(+), 23 deletions(-) diff --git a/typo3/sysext/backend/Classes/Controller/EditDocumentController.php b/typo3/sysext/backend/Classes/Controller/EditDocumentController.php index 8ae7bdd3fbc5..08def5278569 100644 --- a/typo3/sysext/backend/Classes/Controller/EditDocumentController.php +++ b/typo3/sysext/backend/Classes/Controller/EditDocumentController.php @@ -496,11 +496,19 @@ class EditDocumentController $fieldGroups = [$fieldGroups]; } foreach ($fieldGroups as $fields) { - $this->columnsOnly[$table] = array_merge($this->columnsOnly[$table], (is_array($fields) ? $fields : GeneralUtility::trimExplode(',', $fields, true))); + $this->columnsOnly['__hiddenGeneratorFields'][$table] = array_merge( + $this->columnsOnly['__hiddenGeneratorFields'][$table] ?? [], + (is_array($fields) ? $fields : GeneralUtility::trimExplode(',', $fields, true)) + ); } } } - $this->columnsOnly[$table] = array_unique($this->columnsOnly[$table]); + if (!empty($this->columnsOnly['__hiddenGeneratorFields'][$table])) { + $this->columnsOnly['__hiddenGeneratorFields'][$table] = array_diff( + array_unique($this->columnsOnly['__hiddenGeneratorFields'][$table]), + $this->columnsOnly[$table] + ); + } } } } @@ -1185,6 +1193,9 @@ class EditDocumentController // ListOfFieldsContainer instead of FullRecordContainer in OuterWrapContainer if (!empty($this->columnsOnly[$table])) { $formData['fieldListToRender'] = implode(',', $this->columnsOnly[$table]); + if (!empty($this->columnsOnly['__hiddenGeneratorFields'][$table])) { + $formData['hiddenFieldListToRender'] = implode(',', $this->columnsOnly['__hiddenGeneratorFields'][$table]); + } } $formData['renderType'] = 'outerWrapContainer'; diff --git a/typo3/sysext/backend/Classes/Form/Container/ListOfFieldsContainer.php b/typo3/sysext/backend/Classes/Form/Container/ListOfFieldsContainer.php index 4c5fe37bf412..afa2165cc422 100644 --- a/typo3/sysext/backend/Classes/Form/Container/ListOfFieldsContainer.php +++ b/typo3/sysext/backend/Classes/Form/Container/ListOfFieldsContainer.php @@ -24,6 +24,9 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; * This is an entry container called from FormEngine to handle a * list of specific fields. Access rights are checked here and globalOption array * is prepared for further processing of single fields by PaletteAndSingleContainer. + * + * Using "hiddenFieldListToRender" it's also possible to render additional fields as + * hidden fields, which is e.g. used for the "generatorFields" of TCA type "slug". */ class ListOfFieldsContainer extends AbstractContainer { @@ -34,20 +37,41 @@ class ListOfFieldsContainer extends AbstractContainer */ public function render(): array { - $fieldListToRender = $this->data['fieldListToRender']; - $recordTypeValue = $this->data['recordTypeValue']; + $options = $this->data; + $options['fieldsArray'] = $this->sanitizeFieldList($this->data['fieldListToRender']); - $fieldListToRender = array_unique(GeneralUtility::trimExplode(',', $fieldListToRender, true)); + if ($this->data['hiddenFieldListToRender'] ?? false) { + $hiddenFieldList = array_diff( + $this->sanitizeFieldList($this->data['hiddenFieldListToRender']), + $options['fieldsArray'] + ); + if ($hiddenFieldList !== []) { + $hiddenFieldList = implode(',', $hiddenFieldList); + $hiddenPaletteName = 'hiddenFieldsPalette' . md5($hiddenFieldList); + $options['processedTca']['palettes'][$hiddenPaletteName] = [ + 'isHiddenPalette' => true, + 'showitem' => $hiddenFieldList, + ]; + $options['fieldsArray'][] = '--palette--;;' . $hiddenPaletteName; + } + } - $fieldsByShowitem = $this->data['processedTca']['types'][$recordTypeValue]['showitem']; + $options['renderType'] = 'paletteAndSingleContainer'; + return $this->nodeFactory->create($options)->render(); + } + + protected function sanitizeFieldList(string $fieldList): array + { + $fields = array_unique(GeneralUtility::trimExplode(',', $fieldList, true)); + $fieldsByShowitem = $this->data['processedTca']['types'][$this->data['recordTypeValue']]['showitem']; $fieldsByShowitem = GeneralUtility::trimExplode(',', $fieldsByShowitem, true); - $finalFieldsList = []; - foreach ($fieldListToRender as $fieldName) { + $allowedFields = []; + foreach ($fields as $fieldName) { foreach ($fieldsByShowitem as $fieldByShowitem) { $fieldByShowitemArray = $this->explodeSingleFieldShowItemConfiguration($fieldByShowitem); if ($fieldByShowitemArray['fieldName'] === $fieldName) { - $finalFieldsList[] = implode(';', $fieldByShowitemArray); + $allowedFields[] = implode(';', $fieldByShowitemArray); break; } if ($fieldByShowitemArray['fieldName'] === '--palette--' @@ -59,18 +83,14 @@ class ListOfFieldsContainer extends AbstractContainer foreach ($paletteFields as $paletteField) { $paletteFieldArray = $this->explodeSingleFieldShowItemConfiguration($paletteField); if ($paletteFieldArray['fieldName'] === $fieldName) { - $finalFieldsList[] = implode(';', $paletteFieldArray); + $allowedFields[] = implode(';', $paletteFieldArray); break; } } } } } - - $options = $this->data; - $options['fieldsArray'] = $finalFieldsList; - $options['renderType'] = 'paletteAndSingleContainer'; - return $this->nodeFactory->create($options)->render(); + return $allowedFields; } protected function getLanguageService(): LanguageService diff --git a/typo3/sysext/backend/Tests/Unit/Controller/EditDocumentControllerTest.php b/typo3/sysext/backend/Tests/Unit/Controller/EditDocumentControllerTest.php index e63b8051d2ca..639485944473 100644 --- a/typo3/sysext/backend/Tests/Unit/Controller/EditDocumentControllerTest.php +++ b/typo3/sysext/backend/Tests/Unit/Controller/EditDocumentControllerTest.php @@ -43,14 +43,15 @@ final class EditDocumentControllerTest extends UnitTestCase ]; $editDocumentControllerMock->_call('addSlugFieldsToColumnsOnly', $queryParams); - self::assertEquals($result, array_values($editDocumentControllerMock->_get('columnsOnly')[$tableName])); + self::assertEquals($selectedFields, array_values($editDocumentControllerMock->_get('columnsOnly')[$tableName] ?? [])); + self::assertEquals($result, array_values($editDocumentControllerMock->_get('columnsOnly')['__hiddenGeneratorFields'][$tableName] ?? [])); } public static function slugDependentFieldsAreAddedToColumnsOnlyDataProvider(): array { return [ 'fields in string' => [ - ['fo', 'bar', 'slug', 'title'], + ['title'], ['fo', 'bar', 'slug'], 'fake', [ @@ -65,7 +66,7 @@ final class EditDocumentControllerTest extends UnitTestCase ], ], 'fields in string and array' => [ - ['slug', 'fo', 'title', 'nav_title', 'other_field'], + ['nav_title', 'other_field'], ['slug', 'fo', 'title'], 'fake', [ @@ -80,7 +81,7 @@ final class EditDocumentControllerTest extends UnitTestCase ], ], 'unique fields' => [ - ['slug', 'fo', 'title', 'some_field'], + ['some_field'], ['slug', 'fo', 'title'], 'fake', [ @@ -95,7 +96,7 @@ final class EditDocumentControllerTest extends UnitTestCase ], ], 'fields as comma-separated list' => [ - ['slug', 'fo', 'title', 'nav_title', 'some_field'], + ['nav_title', 'some_field'], ['slug', 'fo', 'title'], 'fake', [ @@ -110,7 +111,7 @@ final class EditDocumentControllerTest extends UnitTestCase ], ], 'no slug field given' => [ - ['slug', 'fo'], + [], ['slug', 'fo'], 'fake', [ @@ -168,8 +169,10 @@ final class EditDocumentControllerTest extends UnitTestCase ]; $editDocumentControllerMock->_call('addSlugFieldsToColumnsOnly', $queryParams); - self::assertEquals(['aField', 'aTitle'], array_values($editDocumentControllerMock->_get('columnsOnly')['aTable'])); - self::assertEquals(['bField', 'bTitle'], array_values($editDocumentControllerMock->_get('columnsOnly')['bTable'])); + self::assertEquals(['aField'], array_values($editDocumentControllerMock->_get('columnsOnly')['aTable'])); + self::assertEquals(['aTitle'], array_values($editDocumentControllerMock->_get('columnsOnly')['__hiddenGeneratorFields']['aTable'])); + self::assertEquals(['bField'], array_values($editDocumentControllerMock->_get('columnsOnly')['bTable'])); + self::assertEquals(['bTitle'], array_values($editDocumentControllerMock->_get('columnsOnly')['__hiddenGeneratorFields']['bTable'])); } public static function resolvePreviewRecordIdDataProvider(): array diff --git a/typo3/sysext/backend/Tests/Unit/Form/Container/ListOfFieldsContainerTest.php b/typo3/sysext/backend/Tests/Unit/Form/Container/ListOfFieldsContainerTest.php index c133c965293d..c89d337e0f09 100644 --- a/typo3/sysext/backend/Tests/Unit/Form/Container/ListOfFieldsContainerTest.php +++ b/typo3/sysext/backend/Tests/Unit/Form/Container/ListOfFieldsContainerTest.php @@ -180,4 +180,54 @@ final class ListOfFieldsContainerTest extends UnitTestCase $subject->setData($input); $subject->render(); } + + #[Test] + public function renderAddsHiddenFields(): void + { + $nodeFactoryMock = $this->createMock(NodeFactory::class); + $paletteAndSingleContainerMock = $this->createMock(PaletteAndSingleContainer::class); + $paletteAndSingleContainerMock->expects(self::atLeastOnce())->method('render')->withAnyParameters()->willReturn([]); + + $input = [ + 'tableName' => 'aTable', + 'recordTypeValue' => 'aType', + 'processedTca' => [ + 'types' => [ + 'aType' => [ + 'showitem' => '--palette--;;aPalette,uniqueField,hiddenField,--palette--;;bPalette,', + ], + ], + 'palettes' => [ + 'aPalette' => [ + 'showitem' => 'aField', + ], + 'bPalette' => [ + 'showitem' => 'hiddenInPalette', + ], + ], + ], + 'fieldListToRender' => 'aField,uniqueField', + 'hiddenFieldListToRender' => 'hiddenField,uniqueField,iDontExist,hiddenInPalette', + ]; + + $expected = $input; + $expected['renderType'] = 'paletteAndSingleContainer'; + $expected['processedTca']['palettes']['hiddenFieldsPalette' . md5('hiddenField;;,hiddenInPalette;;')] = [ + 'isHiddenPalette' => true, + 'showitem' => 'hiddenField;;,hiddenInPalette;;', + ]; + // "uniqueField" is onl rendered once and "iDontExist" is not rendered at all + $expected['fieldsArray'] = [ + 'aField;;', + 'uniqueField;;', + '--palette--;;' . 'hiddenFieldsPalette' . md5('hiddenField;;,hiddenInPalette;;'), + ]; + + $nodeFactoryMock->method('create')->with($expected)->willReturn($paletteAndSingleContainerMock); + + $subject = new ListOfFieldsContainer(); + $subject->injectNodeFactory($nodeFactoryMock); + $subject->setData($input); + $subject->render(); + } } -- GitLab