diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/AbstractItemProvider.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/AbstractItemProvider.php index 10db14be582a6181fe0b8df57cd06d2aba1403c9..5406b1fce1ae5064a8a3ca3e2d4b5aef14a4ee84 100644 --- a/typo3/sysext/backend/Classes/Form/FormDataProvider/AbstractItemProvider.php +++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/AbstractItemProvider.php @@ -186,7 +186,7 @@ abstract class AbstractItemProvider if (!empty($helpTextArray['description'])) { $helpText['description'] = $helpTextArray['description']; } - $items[] = [$label, $currentTable, $icon, $helpText]; + $items[] = [$label, $currentTable, $icon, null, $helpText]; } break; case $special === 'pagetypes': @@ -245,6 +245,7 @@ abstract class AbstractItemProvider rtrim($excludeArray['origin'] === 'flexForm' ? $excludeArray['fieldLabel'] : $languageService->sL($GLOBALS['TCA'][$excludeArray['table']]['columns'][$excludeArray['fieldName']]['label']), ':') . ' (' . $excludeArray['fieldName'] . ')', $excludeArray['table'] . ':' . $excludeArray['fullField'], 'empty-empty', + null, $helpText ]; } @@ -344,6 +345,7 @@ abstract class AbstractItemProvider $languageService->sL($itemCfg[0]), $coKey . ':' . preg_replace('/[:|,]/', '', $itemKey), $icon, + null, $helpText ]; } @@ -384,7 +386,7 @@ abstract class AbstractItemProvider $label .= $languageService->sL($moduleLabels['title']); // Item configuration - $items[] = [$label, $theMod, $icon, $helpText]; + $items[] = [$label, $theMod, $icon, null, $helpText]; } } break; @@ -1327,18 +1329,20 @@ abstract class AbstractItemProvider } $value = strlen((string)$item[1]) > 0 ? $item[1] : ''; $icon = !empty($item[2]) ? $item[2] : null; + $groupId = isset($item[3]) ? $item[3] : null; $helpText = null; - if (!empty($item[3])) { - if (\is_string($item[3])) { - $helpText = $languageService->sL($item[3]); + if (!empty($item[4])) { + if (\is_string($item[4])) { + $helpText = $languageService->sL($item[4]); } else { - $helpText = $item[3]; + $helpText = $item[4]; } } $itemArray[$key] = [ $label, $value, $icon, + $groupId, $helpText ]; } diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaSelectItems.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaSelectItems.php index 3076a94bf57655ce52c874231c3aa085938ce5e2..50b64080c66b3866f491c11d2d4c0f27e76b7dc0 100644 --- a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaSelectItems.php +++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaSelectItems.php @@ -16,6 +16,7 @@ namespace TYPO3\CMS\Backend\Form\FormDataProvider; use TYPO3\CMS\Backend\Form\FormDataProviderInterface; +use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\MathUtility; /** @@ -115,6 +116,12 @@ class TcaSelectItems extends AbstractItemProvider implements FormDataProviderInt // Keys may contain table names, so a numeric array is created $fieldConfig['config']['items'] = array_values($fieldConfig['config']['items']); + $fieldConfig['config']['items'] = $this->groupAndSortItems( + $fieldConfig['config']['items'], + $fieldConfig['config']['itemGroups'] ?? [], + $fieldConfig['config']['sortItems'] ?? [] + ); + $result['processedTca']['columns'][$fieldName] = $fieldConfig; } @@ -158,7 +165,9 @@ class TcaSelectItems extends AbstractItemProvider implements FormDataProviderInt foreach ($unmatchedValues as $unmatchedValue) { $invalidItem = [ @sprintf($noMatchingLabel, $unmatchedValue), - $unmatchedValue + $unmatchedValue, + null, + 'none' // put it in the very first position in the "none" group ]; array_unshift($fieldConf['config']['items'], $invalidItem); } @@ -176,4 +185,138 @@ class TcaSelectItems extends AbstractItemProvider implements FormDataProviderInt { return $fieldConfig['config']['renderType'] !== 'selectTree'; } + + /** + * Is used when --div-- elements in the item list are used, or if groups are defined via "groupItems" config array. + * + * This method takes the --div-- elements out of the list, and adds them to the group lists. + * + * A main "none" group is added, which is always on top, when items are not set to be in a group. + * All items without a groupId - which is defined by the fourth key of an item in the item array - are added + * to the "none" group, or to the last group used previously, to ensure ordering as much as possible as before. + * + * Then the found groups are iterated over the order in the [itemGroups] list, + * and items within a group can be sorted via "sortOrders" configuration. + * + * All grouped items are then "flattened" out and --div-- items are added for each group to keep backwards-compatibility. + * + * @param array $allItems all resolved items including the ones from foreign_table values. The group ID information can be found in fourth key [3] of an item. + * @param array $definedGroups [config][itemGroups] + * @param array $sortOrders [config][sortOrders] + * @return array + */ + protected function groupAndSortItems(array $allItems, array $definedGroups, array $sortOrders): array + { + $groupedItems = []; + // Append defined groups at first, as their order is prioritized + $itemGroups = ['none' => '']; + foreach ($definedGroups as $groupId => $groupLabel) { + $itemGroups[$groupId] = $this->getLanguageService()->sL($groupLabel); + } + $currentGroup = 'none'; + // Extract --div-- into itemGroups + foreach ($allItems as $key => $item) { + if ($item[1] === '--div--') { + // A divider is added as a group (existing groups will get their label overriden) + if (isset($item[3])) { + $currentGroup = $item[3]; + $itemGroups[$currentGroup] = $item[0]; + } else { + $currentGroup = 'none'; + } + continue; + } + // Put the given item in the currentGroup if no group has been given already + if (!isset($item[3])) { + $item[3] = $currentGroup; + } + $groupIdOfItem = !empty($item[3]) ? $item[3] : 'none'; + // It is still possible to have items that have an "unassigned" group, so they are moved to the "none" group + if (!isset($itemGroups[$groupIdOfItem])) { + $itemGroups[$groupIdOfItem] = ''; + } + + // Put the item in its corresponding group (and create it if it does not exist yet) + if (!is_array($groupedItems[$groupIdOfItem] ?? null)) { + $groupedItems[$groupIdOfItem] = []; + } + $groupedItems[$groupIdOfItem][] = $item; + } + // Only "none" = no grouping used explicitly via "itemGroups" or via "--div--" + if (count($itemGroups) === 1) { + if (!empty($sortOrders)) { + $allItems = $this->sortItems($allItems, $sortOrders); + } + return $allItems; + } + + // $groupedItems contains all items per group + // $itemGroups contains all groups in order of each group + + // Let's add the --div-- items again ("unpacking") + // And use the group ordering given by the itemGroups + $finalItems = []; + foreach ($itemGroups as $groupId => $groupLabel) { + $itemsInGroup = $groupedItems[$groupId] ?? []; + if (empty($itemsInGroup)) { + continue; + } + // If sorting is defined, sort within each group now + if (!empty($sortOrders)) { + $itemsInGroup = $this->sortItems($itemsInGroup, $sortOrders); + } + // Add the --div-- if it is not the "none" default item + if ($groupId !== 'none') { + // Fall back to the groupId, if there is no label for it + $groupLabel = $groupLabel ?: $groupId; + $finalItems[] = [$groupLabel, '--div--', null, $groupId, null]; + } + $finalItems = array_merge($finalItems, $itemsInGroup); + } + return $finalItems; + } + + /** + * Sort given items by label or value or a custom user function built like + * "MyVendor\MyExtension\TcaSorter->sortItems" or a callable. + * + * @param array $items + * @param array $sortOrders should be something like like [label => desc] + * @return array the sorted items + */ + protected function sortItems(array $items, array $sortOrders): array + { + foreach ($sortOrders as $order => $direction) { + switch ($order) { + case 'label': + $direction = strtolower($direction); + @usort( + $items, + function ($item1, $item2) use ($direction) { + if ($direction === 'desc') { + return strcasecmp($item1[0], $item2[0]) <= 0; + } + return strcasecmp($item1[0], $item2[0]); + } + ); + break; + case 'value': + $direction = strtolower($direction); + @usort( + $items, + function ($item1, $item2) use ($direction) { + if ($direction === 'desc') { + return strcasecmp($item1[1], $item2[1]) <= 0; + } + return strcasecmp($item1[1], $item2[1]); + } + ); + break; + default: + $reference = null; + GeneralUtility::callUserFunction($direction, $items, $reference); + } + } + return $items; + } } diff --git a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaSelectItemsTest.php b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaSelectItemsTest.php index 10fa53b1edeb76c537e38d21e15bd7b1e454f0ce..30c53d5448b8c4a41ccc785719edab484fee6fcd 100644 --- a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaSelectItemsTest.php +++ b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaSelectItemsTest.php @@ -274,12 +274,90 @@ class TcaSelectItemsTest extends UnitTestCase $expected['processedTca']['columns']['aField']['config']['items'][0][0] = 'translated'; $expected['processedTca']['columns']['aField']['config']['items'][0][2] = null; $expected['processedTca']['columns']['aField']['config']['items'][0][3] = null; + $expected['processedTca']['columns']['aField']['config']['items'][0][4] = null; $expected['databaseRow']['aField'] = ['aValue']; self::assertSame($expected, (new TcaSelectItems())->addData($input)); } + /** + * @test + */ + public function addDataAddsDividersIfItemGroupsAreDefined() + { + $input = [ + 'tableName' => 'aTable', + 'databaseRow' => [ + 'aField' => 'aValue', + ], + 'processedTca' => [ + 'columns' => [ + 'aField' => [ + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'items' => [ + [ + 'aLabel', + 'aValue', + 'an-icon-reference', + 'non-existing-group', + null, + ], + [ + 'anotherLabel', + 'anotherValue', + 'an-icon-reference', + 'example-group', + null, + ], + ], + 'itemGroups' => [ + 'example-group' => 'My Example Group' + ], + 'maxitems' => 99999, + ], + ], + ], + ], + ]; + + $expected = $input; + $expected['databaseRow']['aField'] = ['aValue']; + $expected['processedTca']['columns']['aField']['config']['items'] = [ + [ + 'My Example Group', + '--div--', + null, + 'example-group', + null, + ], + [ + 'anotherLabel', + 'anotherValue', + 'an-icon-reference', + 'example-group', + null, + ], [ + 'non-existing-group', + '--div--', + null, + 'non-existing-group', + null, + ], + [ + 'aLabel', + 'aValue', + 'an-icon-reference', + 'non-existing-group', + null, + ], + ]; + + self::assertSame($expected, (new TcaSelectItems)->addData($input)); + } + /** * @test */ @@ -302,6 +380,7 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 'aValue', 2 => 'an-icon-reference', 3 => null, + 4 => null, ], ], 'maxitems' => 99999, @@ -396,7 +475,8 @@ class TcaSelectItemsTest extends UnitTestCase 0 => 'aTitle', 1 => 'aTable', 2 => null, - 3 => [ + 3 => null, + 4 => [ 'description' => 'aDescription', ], ] @@ -461,6 +541,7 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 'aValue', 2 => null, 3 => null, + 4 => null, ] ]; @@ -498,12 +579,14 @@ class TcaSelectItemsTest extends UnitTestCase 1 => '--div--', 2 => null, 3 => null, + 4 => null, ], 1 => [ 0 => 'barColumnTitle (bar)', 1 => 'fooTable:bar', 2 => 'empty-empty', 3 => null, + 4 => null, ], ], ], @@ -533,12 +616,14 @@ class TcaSelectItemsTest extends UnitTestCase 1 => '--div--', 2 => null, 3 => null, + 4 => null, ], 1 => [ 0 => 'barColumnTitle (bar)', 1 => 'fooTable:bar', 2 => 'empty-empty', 3 => null, + 4 => null, ], ], ], @@ -678,12 +763,14 @@ class TcaSelectItemsTest extends UnitTestCase 1 => '--div--', 2 => null, 3 => null, + 4 => null, ], 1 => [ 0 => 'flexInputLabel (input1)', 1 => 'fooTable:aFlexField;dummy;sDEF;input1', 2 => 'empty-empty', 3 => null, + 4 => null, ], ]; @@ -749,12 +836,14 @@ class TcaSelectItemsTest extends UnitTestCase 1 => '--div--', 2 => null, 3 => null, + 4 => null, ], 1 => [ 0 => '[allowMe] anItemTitle', 1 => 'fooTable:aField:anItemValue:ALLOW', 2 => 'status-status-permission-granted', 3 => null, + 4 => null, ], ]; @@ -820,12 +909,14 @@ class TcaSelectItemsTest extends UnitTestCase 1 => '--div--', 2 => null, 3 => null, + 4 => null, ], 1 => [ 0 => '[denyMe] anItemTitle', 1 => 'fooTable:aField:anItemValue:DENY', 2 => 'status-status-permission-denied', 3 => null, + 4 => null, ], ]; @@ -906,18 +997,21 @@ class TcaSelectItemsTest extends UnitTestCase 1 => '--div--', 2 => null, 3 => null, + 4 => null, ], 1 => [ 0 => '[allowMe] aItemTitle', 1 => 'fooTable:aField:aItemValue:ALLOW', 2 => 'status-status-permission-granted', 3 => null, + 4 => null, ], 2 => [ 0 => '[allowMe] cItemTitle', 1 => 'fooTable:aField:cItemValue:ALLOW', 2 => 'status-status-permission-granted', 3 => null, + 4 => null, ], ]; @@ -998,18 +1092,21 @@ class TcaSelectItemsTest extends UnitTestCase 1 => '--div--', 2 => null, 3 => null, + 4 => null, ], 1 => [ 0 => '[denyMe] aItemTitle', 1 => 'fooTable:aField:aItemValue:DENY', 2 => 'status-status-permission-denied', 3 => null, + 4 => null, ], 2 => [ 0 => '[denyMe] cItemTitle', 1 => 'fooTable:aField:cItemValue:DENY', 2 => 'status-status-permission-denied', 3 => null, + 4 => null, ], ]; @@ -1062,6 +1159,7 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 13, 2 => 'flags-aFlag.gif', 3 => null, + 4 => null, ], ]; @@ -1113,18 +1211,21 @@ class TcaSelectItemsTest extends UnitTestCase 1 => '--div--', null, null, + null, ], 1 => [ 0 => 'anItemTitle', 1 => 'aKey:anItemKey', 2 => 'empty-empty', 3 => null, + 4 => null, ], 2 => [ 0 => 'anotherTitle', 1 => 'aKey:anotherKey', 2 => 'empty-empty', - 3 => [ 'description' => 'aDescription' ], + 3 => null, + 4 => [ 'description' => 'aDescription' ], ], ]; @@ -1179,7 +1280,8 @@ class TcaSelectItemsTest extends UnitTestCase 0 => 'aModuleLabel', 1 => 'aModule', 2 => 'empty-empty', - 3 => [ + 3 => null, + 4 => [ 'title' => 'aModuleTabLabel', 'description' => 'aModuleTabDescription', ], @@ -1230,12 +1332,14 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 'anImage.gif', 2 => Environment::getVarPath() . '/' . $directory . 'anImage.gif', 3 => null, + 4 => null, ], 1 => [ 0 => 'subdir/anotherImage.gif', 1 => 'subdir/anotherImage.gif', 2 => Environment::getVarPath() . '/' . $directory . 'subdir/anotherImage.gif', 3 => null, + 4 => null, ], ]; @@ -1292,6 +1396,7 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 'keep', null, null, + null, ], ], 'maxitems' => 99999, @@ -1319,6 +1424,7 @@ class TcaSelectItemsTest extends UnitTestCase 1 => '1', null, null, + null, ]; self::assertEquals($expected, (new TcaSelectItems())->addData($input)); @@ -1346,6 +1452,7 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 'keep', null, null, + null, ], ], 'maxitems' => 99999, @@ -1373,6 +1480,7 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 'keep', null, null, + null, ]; self::assertEquals($expected, (new TcaSelectItems())->addData($input)); @@ -1818,6 +1926,7 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 'itemValue', 2 => null, 3 => null, + 4 => null, ], ], 'maxitems' => 99999, @@ -1964,12 +2073,14 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 1, 2 => null, 3 => null, + 4 => null, ], 1 => [ 0 => 'aPrefix[LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title]', 1 => 2, 2 => null, 3 => null, + 4 => null, ], ]; @@ -2068,6 +2179,7 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 1, 2 => null, 3 => null, + 4 => null, ] ]; @@ -2155,6 +2267,7 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 1, 2 => null, 3 => null, + 4 => null, ], ]; $expected['databaseRow']['aField'] = []; @@ -2184,6 +2297,7 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 'keep', null, null, + null, ], 1 => [ 0 => 'removeMe', @@ -2327,18 +2441,21 @@ class TcaSelectItemsTest extends UnitTestCase 1 => '1', null, null, + null, ], 1 => [ 0 => 'addItem #1', 1 => '1', null, null, + null, ], 2 => [ 0 => 'addItem #12', 1 => '12', null, null, + null, ], ]; @@ -2367,6 +2484,7 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 'keep', null, null, + null, ], 1 => [ 0 => 'removeMe', @@ -2377,6 +2495,7 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 0, null, null, + null, ], ], 'maxitems' => 99999, @@ -2424,12 +2543,14 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 'keep', null, null, + null, ], 1 => [ 0 => 'keepMe', 1 => 'keepMe2', null, null, + null, ], 2 => [ 0 => 'remove me', @@ -2480,6 +2601,7 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 'keep', null, null, + null, ], 1 => [ 0 => 'removeMe', @@ -2537,6 +2659,7 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 'keep', null, null, + null, ], 1 => [ 0 => 'removeMe', @@ -2565,8 +2688,8 @@ class TcaSelectItemsTest extends UnitTestCase $expected = $input; $expected['databaseRow']['aField'] = []; $expected['processedTca']['columns']['aField']['config']['items'] = [ - [ '[ INVALID VALUE "aValue" ]', 'aValue', null, null ], - [ 'keepMe', 'keep', null, null ], + [ '[ INVALID VALUE "aValue" ]', 'aValue', null, 'none', null ], + [ 'keepMe', 'keep', null, null, null ], ]; self::assertEquals($expected, (new TcaSelectItems())->addData($input)); @@ -2595,6 +2718,7 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 'keep', null, null, + null, ], 1 => [ 0 => 'removeMe', @@ -2643,6 +2767,7 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 'keep', null, null, + null, ], ], 'maxitems' => 99999, @@ -2685,6 +2810,7 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 'keep', null, null, + null, ], 1 => [ 0 => 'removeMe', @@ -2737,6 +2863,7 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 'aValue', 2 => null, 3 => null, + 4 => null, ], ]; }, @@ -2757,6 +2884,7 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 'aValue', 2 => null, 3 => null, + 4 => null, ], ], 'maxitems' => 99999, @@ -2838,12 +2966,14 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 1, 2 => null, 3 => null, + 4 => null, ], 1 => [ 0 => 'aLabel_2', 1 => 2, 2 => null, 3 => null, + 4 => null, ], ]; @@ -2887,6 +3017,7 @@ class TcaSelectItemsTest extends UnitTestCase $item[0], // label $item[1], // uid null, // icon + null, // groupID null // helpText ]; } @@ -2952,6 +3083,7 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 2, 2 => null, 3 => null, + 4 => null, ], ], 'maxitems' => 99999 @@ -2996,6 +3128,7 @@ class TcaSelectItemsTest extends UnitTestCase $item[0], // label $item[1], // uid null, // icon + null, // groupId null // helpText ]; } @@ -3069,6 +3202,7 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 1, 2 => null, 3 => null, + 4 => null, ] ], 'maxitems' => 99999 @@ -3113,6 +3247,7 @@ class TcaSelectItemsTest extends UnitTestCase $item[0], // label $item[1], // uid null, // icon + null, // groupID null // helpText ]; } @@ -3188,12 +3323,14 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 2, 2 => null, 3 => null, + 4 => null, ], 1 => [ 0 => 'Label of the added item', 1 => 12, 2 => null, 3 => null, + 4 => null, ], ], 'maxitems' => 99999 @@ -3356,6 +3493,7 @@ class TcaSelectItemsTest extends UnitTestCase 1 => 'aValue', null, null, + null ], ], 'maxitems' => 99999, @@ -3549,7 +3687,7 @@ class TcaSelectItemsTest extends UnitTestCase 'foreign_table' => 'foreignTable', 'maxitems' => 999, 'items' => [ - ['foo', 'foo', null, null], + ['foo', 'foo', null, null, null], ], ], ], @@ -3581,8 +3719,8 @@ class TcaSelectItemsTest extends UnitTestCase 'renderType' => 'selectSingle', 'maxitems' => 999, 'items' => [ - ['foo', 'foo', null, null], - ['bar', 'bar', null, null], + ['foo', 'foo', null, null, null], + ['bar', 'bar', null, null, null], ], ], ], @@ -3647,9 +3785,9 @@ class TcaSelectItemsTest extends UnitTestCase 'renderType' => 'selectSingle', 'maxitems' => 999, 'items' => [ - ['a', '', null, null], - ['b', 'b', null, null], - ['c', 'c', null, null], + ['a', '', null, null, null], + ['b', 'b', null, null, null], + ['c', 'c', null, null, null], ], ], ], @@ -3689,7 +3827,7 @@ class TcaSelectItemsTest extends UnitTestCase 'renderType' => 'selectSingle', 'maxitems' => 999, 'items' => [ - ['foo', 'foo', null, null], + ['foo', 'foo', null, null, null], ], ], ], @@ -3731,7 +3869,7 @@ class TcaSelectItemsTest extends UnitTestCase 'renderType' => 'selectSingle', 'maxitems' => 99999, 'items' => [ - ['foo', 'foo', null, null], + ['foo', 'foo', null, null, null], ], ], ], @@ -3742,10 +3880,10 @@ class TcaSelectItemsTest extends UnitTestCase $expected = $input; $expected['databaseRow']['aField'] = ['foo']; $expected['processedTca']['columns']['aField']['config']['items'] = [ - ['[ INVALID VALUE "bar" ]', 'bar', null, null], - ['[ INVALID VALUE "2" ]', '2', null, null], - ['[ INVALID VALUE "1" ]', '1', null, null], - ['foo', 'foo', null, null], + ['[ INVALID VALUE "bar" ]', 'bar', null, 'none', null], + ['[ INVALID VALUE "2" ]', '2', null, 'none', null], + ['[ INVALID VALUE "1" ]', '1', null, 'none', null], + ['foo', 'foo', null, null, null], ]; self::assertEquals($expected, (new TcaSelectItems())->addData($input)); } @@ -3769,10 +3907,10 @@ class TcaSelectItemsTest extends UnitTestCase 'multiple' => true, 'maxitems' => 999, 'items' => [ - ['1', '1', null, null], - ['foo', 'foo', null, null], - ['bar', 'bar', null, null], - ['2', '2', null, null], + ['1', '1', null, null, null], + ['foo', 'foo', null, null, null], + ['bar', 'bar', null, null, null], + ['2', '2', null, null, null], ], ], ], @@ -3811,10 +3949,10 @@ class TcaSelectItemsTest extends UnitTestCase 'multiple' => false, 'maxitems' => 999, 'items' => [ - ['1', '1', null, null], - ['foo', 'foo', null, null], - ['bar', 'bar', null, null], - ['2', '2', null, null], + ['1', '1', null, null, null], + ['foo', 'foo', null, null, null], + ['bar', 'bar', null, null, null], + ['2', '2', null, null, null], ], ], ], diff --git a/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php b/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php index 10b95e75293a0b89f29ddb93c9ef15c9b11642fb..972bbdd2464c86213ecc15fbdf5a9a7e77576ca8 100644 --- a/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php +++ b/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php @@ -479,6 +479,66 @@ class ExtensionManagementUtility } } + /** + * Adds an item group to a TCA select field, allows to add a group so addTcaSelectItem() can add a groupId + * with a label and its position within other groups. + * + * @param string $table the table name in TCA - e.g. tt_content + * @param string $field the field name in TCA - e.g. CType + * @param string $groupId the unique identifier for a group, where all items from addTcaSelectItem() with a group ID are connected + * @param string $groupLabel the label e.g. LLL:my_extension/Resources/Private/Language/locallang_tca.xlf:group.mygroupId + * @param string|null $position e.g. "before:special", "after:default" (where the part after the colon is an existing groupId) or "top" or "bottom" + */ + public static function addTcaSelectItemGroup(string $table, string $field, string $groupId, string $groupLabel, ?string $position = 'bottom'): void + { + if (!is_array($GLOBALS['TCA'][$table]['columns'][$field]['config'] ?? null)) { + throw new \RuntimeException('Given select field item list was not found.', 1586728563); + } + $itemGroups = $GLOBALS['TCA'][$table]['columns'][$field]['config']['itemGroups'] ?? []; + // Group has been defined already, nothing to do + if (isset($itemGroups[$groupId])) { + return; + } + $position = (string)$position; + $positionGroupId = ''; + if (strpos($position, ':') !== false) { + [$position, $positionGroupId] = explode(':', $position, 2); + } + // Referenced group was not not found, just append to the bottom + if (!isset($itemGroups[$positionGroupId])) { + $position = 'bottom'; + } + switch ($position) { + case 'after': + $newItemGroups = []; + foreach ($itemGroups as $existingGroupId => $existingGroupLabel) { + $newItemGroups[$existingGroupId] = $existingGroupLabel; + if ($positionGroupId === $existingGroupId) { + $newItemGroups[$groupId] = $groupLabel; + } + } + $itemGroups = $newItemGroups; + break; + case 'before': + $newItemGroups = []; + foreach ($itemGroups as $existingGroupId => $existingGroupLabel) { + if ($positionGroupId === $existingGroupId) { + $newItemGroups[$groupId] = $groupLabel; + } + $newItemGroups[$existingGroupId] = $existingGroupLabel; + } + $itemGroups = $newItemGroups; + break; + case 'top': + $itemGroups = array_merge([$groupId => $groupLabel], $itemGroups); + break; + case 'bottom': + default: + $itemGroups[$groupId] = $groupLabel; + } + $GLOBALS['TCA'][$table]['columns'][$field]['config']['itemGroups'] = $itemGroups; + } + /** * Gets the TCA configuration for a field handling (FAL) files. * @@ -1177,7 +1237,7 @@ class ExtensionManagementUtility * * FOR USE IN files in Configuration/TCA/Overrides/*.php Use in ext_tables.php FILES may break the frontend. * - * @param array $itemArray Numerical array: [0] => Plugin label, [1] => Plugin identifier / plugin key, ideally prefixed with a extension-specific name (e.g. "events2_list"), [2] => Path to plugin icon relative to TYPO3_mainDir + * @param array $itemArray Numerical array: [0] => Plugin label, [1] => Plugin identifier / plugin key, ideally prefixed with an extension-specific name (e.g. "events2_list"), [2] => Path to plugin icon, [3] => an optional "group" ID, falls back to "default" * @param string $type Type (eg. "list_type") - basically a field from "tt_content" table * @param string $extensionKey The extension key * @throws \RuntimeException @@ -1198,6 +1258,9 @@ class ExtensionManagementUtility // @todo do we really set $itemArray[2], even if we cannot find an icon? (as that means it's set to 'EXT:foobar/') $itemArray[2] = 'EXT:' . $extensionKey . '/' . static::getExtensionIcon(static::$packageManager->getPackage($extensionKey)->getPackagePath()); } + if (!isset($itemArray[3])) { + $itemArray[3] = 'default'; + } if (is_array($GLOBALS['TCA']['tt_content']['columns']) && is_array($GLOBALS['TCA']['tt_content']['columns'][$type]['config']['items'])) { foreach ($GLOBALS['TCA']['tt_content']['columns'][$type]['config']['items'] as $k => $v) { if ((string)$v[1] === (string)$itemArray[1]) { diff --git a/typo3/sysext/core/Configuration/TCA/pages.php b/typo3/sysext/core/Configuration/TCA/pages.php index 71b84b43661ede8fa333eb809509ef4a9132c246..edf418e8b5dc250563286bc8bccf70a895c1a43c 100644 --- a/typo3/sysext/core/Configuration/TCA/pages.php +++ b/typo3/sysext/core/Configuration/TCA/pages.php @@ -78,57 +78,76 @@ return [ 'items' => [ [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.doktype.div.page', - '--div--' + '--div--', + null, + 'default' ], [ 'LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:doktype.I.0', (string)\TYPO3\CMS\Core\Domain\Repository\PageRepository::DOKTYPE_DEFAULT, - 'apps-pagetree-page-default' + 'apps-pagetree-page-default', + 'default' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.doktype.I.4', (string)\TYPO3\CMS\Core\Domain\Repository\PageRepository::DOKTYPE_BE_USER_SECTION, - 'apps-pagetree-page-backend-users' + 'apps-pagetree-page-backend-users', + 'default' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.doktype.div.link', - '--div--' + '--div--', + null, + 'link' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.doktype.I.2', (string)\TYPO3\CMS\Core\Domain\Repository\PageRepository::DOKTYPE_SHORTCUT, - 'apps-pagetree-page-shortcut' + 'apps-pagetree-page-shortcut', + 'link' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.doktype.I.5', (string)\TYPO3\CMS\Core\Domain\Repository\PageRepository::DOKTYPE_MOUNTPOINT, - 'apps-pagetree-page-mountpoint' + 'apps-pagetree-page-mountpoint', + 'link' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.doktype.I.8', (string)\TYPO3\CMS\Core\Domain\Repository\PageRepository::DOKTYPE_LINK, - 'apps-pagetree-page-shortcut-external' + 'apps-pagetree-page-shortcut-external', + 'link' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.doktype.div.special', - '--div--' + '--div--', + null, + 'special' ], [ 'LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:doktype.I.folder', (string)\TYPO3\CMS\Core\Domain\Repository\PageRepository::DOKTYPE_SYSFOLDER, - 'apps-pagetree-folder-default' + 'apps-pagetree-folder-default', + 'special' ], [ 'LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:doktype.I.2', (string)\TYPO3\CMS\Core\Domain\Repository\PageRepository::DOKTYPE_RECYCLER, - 'apps-filetree-folder-recycler' + 'apps-filetree-folder-recycler', + 'special' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.doktype.I.7', (string)\TYPO3\CMS\Core\Domain\Repository\PageRepository::DOKTYPE_SPACER, - 'apps-pagetree-spacer' + 'apps-pagetree-spacer', + 'special' ] ], + 'itemGroups' => [ + 'default' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.doktype.div.page', + 'link' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.doktype.div.link', + 'special' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.doktype.div.special', + ], 'default' => (string)\TYPO3\CMS\Core\Domain\Repository\PageRepository::DOKTYPE_DEFAULT, ] ], diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-91008-ItemGroupingForTCASelectItems.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-91008-ItemGroupingForTCASelectItems.rst new file mode 100644 index 0000000000000000000000000000000000000000..c800aedb6c88e63e9a71b4d1d59f379a2282aea7 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-91008-ItemGroupingForTCASelectItems.rst @@ -0,0 +1,150 @@ +.. include:: ../../Includes.txt + +==================================================== +Feature: #91008 - Item grouping for TCA select items +==================================================== + +See :issue:`91008` + +Description +=========== + +The TCA column type "select" now has a clean API to group items for dropdowns +in FormEngine. This was previously handled via placeholder "--div--" items, +which then rendered as `<optgroup>` HTML elements in a dropdown. + +In larger installations or TYPO3 instances with lots of extensions, Plugins +(`tt_content.list_type`), Content Types (`tt_content.CType`) or custom +Page Types (`pages.doktype`), grouping can now be configured on a per-item +basis. Custom groups can be added via an API or when defining TCA for a new table. + +Adding Custom Select Item Groups +-------------------------------- + +Registration of a select item group takes place in :php:`Configuration/TCA/tx_mytable.php` +for new TCA tables, and in :php:`Configuration/TCA/Overrides/a_random_core_table.php` +for modifying an existing TCA definition. + +The following two examples illustrate adding a new group to a field of +type "select": + +.. code-block:: php + + ExtensionManagementUtility:addTcaSelectItemGroup( + 'tt_content', + 'CType', + 'sliders', + 'LLL:EXT:my_slider_mixtape/Resources/Private/Language/locallang_tca.xlf:tt_content.group.sliders', + 'after:lists' + ); + +The TCA for `tt_content.CType` column configuration looks like this now: + +.. code-block:: php + + 'items' => ... + 'itemGroups' => [ + 'default' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.div.standard', + 'lists' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.div.lists', + 'sliders' => 'LLL:EXT:my_slider_mixtape/Resources/Private/Language/locallang_tca.xlf:tt_content.group.sliders', + 'menu' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.div.menu', + 'forms' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.div.forms', + 'special' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.div.special', + ], + +When adding a new select field, itemGroups should be added directly in the +original TCA definition without using the API method. Use the API within +`TCA/Configuration/Overrides/` files to extend an existing TCA select field with +grouping. + +Attaching Select Items to Item Groups +------------------------------------- + +A select item now has a fourth array key to define a "Group ID" to which group it +belongs to. In the example above, the group ID is named "sliders" and used +in the examples below to attach items to this group. + +Grouping for select items can be used via API or in TCA configuration directly. + +This is the example for a custom Content Type "slickslider" belonging to the +group from above: + +.. code-block:: php + + 'items' => [ + ..., + [ + // Label + 'LLL:my_slider_mixtape/Resources/Private/Locallang/locallang_tca.xlf:tt_content.CType.slickslider', + // Value written to the database + 'slickslider', + // Icon for the dropdown + 'EXT:my_slider_mixtape/Resources/Public/Icons/slickslider.png', + // The group ID, if not given, falls back to "none" or the last used --div-- in the item array + 'sliders' + ], + ] + + +The item can be added via API like this: + +.. code-block:: php + + ExtensionManagementUtility::addTcaSelectItem( + 'tt_content', + 'CType', + [ + 'LLL:my_slider_mixtape/Resources/Private/Locallang/locallang_tca.xlf:tt_content.CType.slickslider', + 'slickslider', + 'EXT:my_slider_mixtape/Resources/Public/Icons/slickslider.png', + 'sliders' + ] + ); + +The same approach applies to :php:`ExtensionManagementUtility::addPlugin()` when +adding pi-based plugins. + +When adding Extbase plugins, the API method now allows to specify a group ID +directly as additional parameter. This falls back to the "default" group ID, +which is available in `tt_content.CType` and `tt_content.list_type`. + +.. code-block:: php + + ExtensionUtility::registerPlugin( + // Extension key + 'my_slider_mixtape', + // Plugin value + 'slider_from_records', + // Plugin label + 'LLL:my_slider_mixtape/Resources/Private/Locallang/locallang_tca.xlf:tt_content.plugin.slider_from_records', + // Icon for plugin + 'EXT:my_slider_mixtape/Resources/Public/Icons/slickslider.png', + // Group ID + 'sliders' + ); + + +Impact +====== + +By default, Page Types (`pages.doktype`), Content Types (`tt_content.CType`) and +Plugins (`tt_content.list_type`) now have native grouping enabled. + +The order of the `itemGroups` value is important when using groups, as this +is the order of the groups rendered in the dropdown of FormEngine. + +The API methods can be used to build more groups without juggling with +TCA arrays. + +It is possible now, and encouraged to remove the `--div--` items in custom +selects and use itemGroups instead. TYPO3 Core keeps the `--div--` for +backwards-compatible reasons in TYPO3 v10, but all items of the fields mentioned +above the grouping parameter has been added already. + +Please note that this `--div--` is related to select items, and not the +"showItem" definition which fields should be shown. + +Currently Item Groups are used in FormEngine DropDowns / single-select items +from TYPO3 Core, but can be used in multi-select fields as well. + +.. index:: TCA, ext:core diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-91008-ItemSortingForTCASelectItems.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-91008-ItemSortingForTCASelectItems.rst new file mode 100644 index 0000000000000000000000000000000000000000..f8bb58437abfc2884e8747c938ba8a0da9e12a58 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-91008-ItemSortingForTCASelectItems.rst @@ -0,0 +1,58 @@ +.. include:: ../../Includes.txt + +=================================================== +Feature: #91008 - Item sorting for TCA select items +=================================================== + +See :issue:`91008` + +Description +=========== + +A new option `sortOrders` for TCA-based select fields has been added to allow +sorting of static TCA select items by their values or labels. + +This is now used in TYPO3 Core's `tt_content.list_type` whereas +a previous `itemProcFunc` was used to sort all plugins by label +in the FormEngine dropdown. + +Built-in orderings are to sort items by their labels or values. It is also possible +to define custom `sortOrders` via custom PHP code. + +Examples from tt_contents' `list_type` TCA: + +.. code-block:: php + + // Sort all items by label ("asc" or "desc" is possible) + $GLOBALS['TCA']['tt_content']['columns']['list_type']['config']['sortItems'] = [ + 'label' => 'asc' + ]; + + // Sort all items by value ("asc" or "desc" is possible) + $GLOBALS['TCA']['tt_content']['columns']['list_type']['config']['sortItems'] = [ + 'value' => 'desc' + ]; + + // Sort all items by a custom function + $GLOBALS['TCA']['tt_content']['columns']['list_type']['config']['sortItems'] = [ + 'My_Extension' => 'ksort' + ]; + + $GLOBALS['TCA']['tt_content']['columns']['list_type']['config']['sortItems'] = [ + 'My_Extension' => \VendorName\PackageName\TcaSorter::class . '->sortByMagic' + ]; + +When using grouped select fields with "itemGroups", sorting happens on a +per-group basis - all items within one group are sorted - as the group ordering +is preserved. + + +Impact +====== + +Plugins in FormEngine are now using this option in TYPO3 Core, and other TCA +select fields can benefit from this as well. + +This option is solely built for display purposes in FormEngine. + +.. index:: TCA, ext:core diff --git a/typo3/sysext/core/Tests/Unit/Utility/ExtensionManagementUtilityTest.php b/typo3/sysext/core/Tests/Unit/Utility/ExtensionManagementUtilityTest.php index c09c9e960cc2b3e0ad84f4bec94305cffe443783..db4f02ae2279b9bb2682de411f6988fe579b0deb 100644 --- a/typo3/sysext/core/Tests/Unit/Utility/ExtensionManagementUtilityTest.php +++ b/typo3/sysext/core/Tests/Unit/Utility/ExtensionManagementUtilityTest.php @@ -1835,7 +1835,8 @@ class ExtensionManagementUtilityTest extends UnitTestCase [ 'label', $extKey, - 'EXT:' . $extKey . '/Resources/Public/Icons/Extension.png' + 'EXT:' . $extKey . '/Resources/Public/Icons/Extension.png', + 'default' ] ]; $GLOBALS['TCA']['tt_content']['columns']['list_type']['config']['items'] = []; @@ -1853,4 +1854,119 @@ class ExtensionManagementUtilityTest extends UnitTestCase ExtensionManagementUtility::addPlugin('test'); } + + public function addTcaSelectItemGroupAddsGroupDataProvider() + { + return [ + 'add the first group' => [ + 'my_group', + 'my_group_label', + null, + null, + [ + 'my_group' => 'my_group_label' + ] + ], + 'add a new group at the bottom' => [ + 'my_group', + 'my_group_label', + 'bottom', + [ + 'default' => 'default_label' + ], + [ + 'default' => 'default_label', + 'my_group' => 'my_group_label' + ] + ], + 'add a new group at the top' => [ + 'my_group', + 'my_group_label', + 'top', + [ + 'default' => 'default_label' + ], + [ + 'my_group' => 'my_group_label', + 'default' => 'default_label' + ] + ], + 'add a new group after an existing group' => [ + 'my_group', + 'my_group_label', + 'after:default', + [ + 'default' => 'default_label', + 'special' => 'special_label' + ], + [ + 'default' => 'default_label', + 'my_group' => 'my_group_label', + 'special' => 'special_label' + ] + ], + 'add a new group before an existing group' => [ + 'my_group', + 'my_group_label', + 'before:default', + [ + 'default' => 'default_label', + 'special' => 'special_label' + ], + [ + 'my_group' => 'my_group_label', + 'default' => 'default_label', + 'special' => 'special_label' + ] + ], + 'add a new group after a non-existing group moved to bottom' => [ + 'my_group', + 'my_group_label', + 'after:default2', + [ + 'default' => 'default_label', + 'special' => 'special_label' + ], + [ + 'default' => 'default_label', + 'special' => 'special_label', + 'my_group' => 'my_group_label', + ] + ], + 'add a new group which already exists does nothing' => [ + 'my_group', + 'my_group_label', + 'does-not-matter', + [ + 'default' => 'default_label', + 'my_group' => 'existing_label', + 'special' => 'special_label' + ], + [ + 'default' => 'default_label', + 'my_group' => 'existing_label', + 'special' => 'special_label' + ] + ], + ]; + } + + /** + * @test + * @param string $groupId + * @param string $groupLabel + * @param string $position + * @param array|null $existingGroups + * @param array $expectedGroups + * @dataProvider addTcaSelectItemGroupAddsGroupDataProvider + */ + public function addTcaSelectItemGroupAddsGroup(string $groupId, string $groupLabel, ?string $position, ?array $existingGroups, array $expectedGroups) + { + $GLOBALS['TCA']['tt_content']['columns']['CType']['config'] = []; + if (is_array($existingGroups)) { + $GLOBALS['TCA']['tt_content']['columns']['CType']['config']['itemGroups'] = $existingGroups; + } + ExtensionManagementUtility::addTcaSelectItemGroup('tt_content', 'CType', $groupId, $groupLabel, $position); + self::assertEquals($expectedGroups, $GLOBALS['TCA']['tt_content']['columns']['CType']['config']['itemGroups']); + } } diff --git a/typo3/sysext/extbase/Classes/Utility/ExtensionUtility.php b/typo3/sysext/extbase/Classes/Utility/ExtensionUtility.php index 92f9d7ee790080631baed34067afc6fa7ac99956..46df5a6479375ed6cf583fad7617af62e0410238 100644 --- a/typo3/sysext/extbase/Classes/Utility/ExtensionUtility.php +++ b/typo3/sysext/extbase/Classes/Utility/ExtensionUtility.php @@ -150,9 +150,10 @@ tt_content.' . $pluginSignature . ' { * @param string $pluginName must be a unique id for your plugin in UpperCamelCase (the string length of the extension key added to the length of the plugin name should be less than 32!) * @param string $pluginTitle is a speaking title of the plugin that will be displayed in the drop down menu in the backend * @param string $pluginIcon is an icon identifier or file path prepended with "EXT:", that will be displayed in the drop down menu in the backend (optional) + * @param string $group add this plugin to a plugin group, should be something like "news" or the like, "default" as regular * @throws \InvalidArgumentException */ - public static function registerPlugin($extensionName, $pluginName, $pluginTitle, $pluginIcon = null) + public static function registerPlugin($extensionName, $pluginName, $pluginTitle, $pluginIcon = null, $group = 'default') { self::checkPluginNameFormat($pluginName); self::checkExtensionNameFormat($extensionName); @@ -176,8 +177,12 @@ tt_content.' . $pluginSignature . ' { // pluginType is usually defined by configurePlugin() in the global array. Use this or fall back to default "list_type". $pluginType = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$extensionName]['plugins'][$pluginName]['pluginType'] ?? 'list_type'; + $itemArray = [$pluginTitle, $pluginSignature, $pluginIcon]; + if ($group) { + $itemArray[3] = $group; + } \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPlugin( - [$pluginTitle, $pluginSignature, $pluginIcon], + $itemArray, $pluginType, $extensionKey ); diff --git a/typo3/sysext/felogin/Configuration/TCA/Overrides/tt_content.php b/typo3/sysext/felogin/Configuration/TCA/Overrides/tt_content.php index ec1e51ab73e9ee77affcda8162ba313a283aee57..39a097cbf5b0d0f7a8ed4583df779e2e757355b4 100644 --- a/typo3/sysext/felogin/Configuration/TCA/Overrides/tt_content.php +++ b/typo3/sysext/felogin/Configuration/TCA/Overrides/tt_content.php @@ -12,10 +12,23 @@ call_user_func(static function () { \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin( 'Felogin', 'Login', - 'Login Form' + 'Login Form', + null, + 'forms' ); } else { $contentTypeName = 'login'; + // Add CType=login + \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTcaSelectItem( + 'tt_content', + 'CType', + [ + 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.I.10', + 'login', + 'content-elements-login', + 'forms' + ] + ); } // Add the FlexForm \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue( @@ -25,47 +38,6 @@ call_user_func(static function () { ); $GLOBALS['TCA']['tt_content']['ctrl']['typeicon_classes'][$contentTypeName] = 'mimetypes-x-content-login'; - // check if there is already a forms tab and add the item after that, otherwise - // add the tab item as well - $additionalCTypeItem = [ - 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.I.10', - $contentTypeName, - 'content-elements-login' - ]; - - $existingCTypeItems = $GLOBALS['TCA']['tt_content']['columns']['CType']['config']['items']; - $groupFound = false; - $groupPosition = false; - foreach ($existingCTypeItems as $position => $item) { - if ($item[0] === 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.div.forms') { - $groupFound = true; - $groupPosition = $position; - break; - } - } - - if ($groupFound && $groupPosition) { - // add the new CType item below CType - array_splice( - $GLOBALS['TCA']['tt_content']['columns']['CType']['config']['items'], - $groupPosition, - 0, - [0 => $additionalCTypeItem] - ); - } else { - // nothing found, add two items (group + new CType) at the bottom of the list - \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTcaSelectItem( - 'tt_content', - 'CType', - ['LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.div.forms', '--div--'] - ); - \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTcaSelectItem( - 'tt_content', - 'CType', - $additionalCTypeItem - ); - } - $GLOBALS['TCA']['tt_content']['types'][$contentTypeName]['showitem'] = ' --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general, --palette--;;general, diff --git a/typo3/sysext/form/Configuration/TCA/Overrides/tt_content.php b/typo3/sysext/form/Configuration/TCA/Overrides/tt_content.php index b5d6584c3015c376d0506d0f7bac0b347b63330a..6d6a5b96ee683eaa66b206645fb80e505c585196 100644 --- a/typo3/sysext/form/Configuration/TCA/Overrides/tt_content.php +++ b/typo3/sysext/form/Configuration/TCA/Overrides/tt_content.php @@ -37,6 +37,7 @@ call_user_func(function () { 'Form', 'Formframework', 'Form', - 'content-form' + 'content-form', + 'forms' ); }); diff --git a/typo3/sysext/frontend/Classes/Hooks/TableColumnHooks.php b/typo3/sysext/frontend/Classes/Hooks/TableColumnHooks.php deleted file mode 100644 index b2bc344d9664a292da55d20f382ddda99a4b22e5..0000000000000000000000000000000000000000 --- a/typo3/sysext/frontend/Classes/Hooks/TableColumnHooks.php +++ /dev/null @@ -1,47 +0,0 @@ -<?php - -/* - * 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! - */ - -namespace TYPO3\CMS\Frontend\Hooks; - -/** - * Hooks / manipulation data for TCA columns e.g. to sort items within itemsProcFunc - * @internal this is a concrete TYPO3 hook implementation and solely used for EXT:frontend and not part of TYPO3's Core API. - */ -class TableColumnHooks -{ - /** - * sort list items (used for plugins, list_type) by name - * @param array $parameters - */ - public function sortPluginList(array &$parameters) - { - @usort( - $parameters['items'], - function ($item1, $item2) { - return strcasecmp($this->getLanguageService()->sL($item1[0]), $this->getLanguageService()->sL($item2[0])); - } - ); - } - - /** - * Returns LanguageService - * - * @return \TYPO3\CMS\Core\Localization\LanguageService - */ - protected function getLanguageService() - { - return $GLOBALS['LANG']; - } -} diff --git a/typo3/sysext/frontend/Configuration/TCA/tt_content.php b/typo3/sysext/frontend/Configuration/TCA/tt_content.php index 1dcb7909617c1db48d62261959fc25dcb6f978d6..1d67b9b31292cdf1c1e288488666a8b0db5447fa 100644 --- a/typo3/sysext/frontend/Configuration/TCA/tt_content.php +++ b/typo3/sysext/frontend/Configuration/TCA/tt_content.php @@ -73,136 +73,174 @@ return [ 'items' => [ [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.div.standard', - '--div--' + '--div--', + null, + 'default' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.I.0', 'header', - 'content-header' + 'content-header', + 'default' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.I.1', 'text', - 'content-text' + 'content-text', + 'default' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.I.2', 'textpic', - 'content-textpic' + 'content-textpic', + 'default' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.I.3', 'image', - 'content-image' + 'content-image', + 'default' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.textmedia', 'textmedia', - 'content-textmedia' + 'content-textmedia', + 'default' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.div.lists', - '--div--' + '--div--', + null, + 'lists' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.I.4', 'bullets', - 'content-bullets' + 'content-bullets', + 'lists' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.I.5', 'table', - 'content-table' + 'content-table', + 'lists' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.I.6', 'uploads', - 'content-special-uploads' + 'content-special-uploads', + 'lists' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.div.menu', - '--div--' + '--div--', + null, + 'menu' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.menu_abstract', 'menu_abstract', - 'content-menu-abstract' + 'content-menu-abstract', + 'menu' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.menu_categorized_content', 'menu_categorized_content', - 'content-menu-categorized' + 'content-menu-categorized', + 'menu' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.menu_categorized_pages', 'menu_categorized_pages', - 'content-menu-categorized' + 'content-menu-categorized', + 'menu' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.menu_pages', 'menu_pages', - 'content-menu-pages' + 'content-menu-pages', + 'menu' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.menu_subpages', 'menu_subpages', - 'content-menu-pages' + 'content-menu-pages', + 'menu' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.menu_recently_updated', 'menu_recently_updated', - 'content-menu-recently-updated' + 'content-menu-recently-updated', + 'menu' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.menu_related_pages', 'menu_related_pages', - 'content-menu-related' + 'content-menu-related', + 'menu' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.menu_section', 'menu_section', - 'content-menu-section' + 'content-menu-section', + 'menu' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.menu_section_pages', 'menu_section_pages', - 'content-menu-section' + 'content-menu-section', + 'menu' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.menu_sitemap', 'menu_sitemap', - 'content-menu-sitemap' + 'content-menu-sitemap', + 'menu' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.menu_sitemap_pages', 'menu_sitemap_pages', - 'content-menu-sitemap-pages' + 'content-menu-sitemap-pages', + 'menu' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.div.special', - '--div--' + '--div--', + null, + 'special' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.I.13', 'shortcut', - 'content-special-shortcut' + 'content-special-shortcut', + 'special' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.I.14', 'list', - 'content-plugin' + 'content-plugin', + 'special' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.I.16', 'div', - 'content-special-div' + 'content-special-div', + 'special' ], [ 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.I.17', 'html', - 'content-special-html' + 'content-special-html', + 'special' ] ], + 'itemGroups' => [ + 'default' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.div.standard', + 'lists' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.div.lists', + 'menu' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.div.menu', + 'forms' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.div.forms', + 'special' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.div.special', + ], 'default' => 'text', 'authMode' => $GLOBALS['TYPO3_CONF_VARS']['BE']['explicitADmode'], 'authMode_enforce' => 'strict', @@ -952,10 +990,16 @@ return [ [ '', '', - '' + '', + 'none' ] ], - 'itemsProcFunc' => \TYPO3\CMS\Frontend\Hooks\TableColumnHooks::class . '->sortPluginList', + 'itemGroups' => [ + 'default' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.default_value' + ], + 'sortItems' => [ + 'label' => 'asc' + ], 'default' => '', 'authMode' => $GLOBALS['TYPO3_CONF_VARS']['BE']['explicitADmode'], 'authMode_enforce' => 'strict'