Skip to content

Commit a1340e2

Browse files
nhovratovgarvinhicking
authored andcommitted
[BUGFIX] Respect order of item groups in NewContentElementWizard
Since the change of registering wizard items groups with TCA in #102834 the order of groups can't be changed anymore in the wizard. This is addressed now with a rather simple solution: When TCA groups are added, the sorting is already ensured by EMU:addTcaSelectItemGroup. We take this sorting and auto-add the positional information in NewContentElementController by list-linking the groups by "after:{theGroupBefore}". This way the sorting is well-defined. How to deal with PageTs: Well, we can't have it both ways. As soon as PageTs overrides at least one group order, this will overrule the TCA sorting. Then, everything has to be defined via PageTs. This is fine, as we strive to reduce usage of PageTs. Resolves: #104855 Related: #102834 Releases: main, 13.4 Change-Id: I071a74fd03b9a5e9ab4d4e471ad6226e485657da Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/87382 Tested-by: Georg Ringer <georg.ringer@gmail.com> Reviewed-by: Georg Ringer <georg.ringer@gmail.com> Reviewed-by: Benni Mack <benni@typo3.org> Reviewed-by: Garvin Hicking <gh@faktor-e.de> Tested-by: core-ci <typo3@b13.com> Tested-by: Garvin Hicking <gh@faktor-e.de>
1 parent dd76e9c commit a1340e2

File tree

2 files changed

+274
-7
lines changed

2 files changed

+274
-7
lines changed

typo3/sysext/backend/Classes/Controller/ContentElement/NewContentElementController.php

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -240,8 +240,10 @@ protected function getWizards(): array
240240
foreach ($wizards as $groupKey => $wizardGroup) {
241241
$wizards[$groupKey] = $this->prepareDependencyOrdering($wizards[$groupKey], 'before');
242242
$wizards[$groupKey] = $this->prepareDependencyOrdering($wizards[$groupKey], 'after');
243+
$wizards[$groupKey] = $this->prepareDependencyOrdering($wizards[$groupKey], 'contentElementAfter');
243244
}
244-
foreach ($this->dependencyOrderingService->orderByDependencies($wizards) as $groupKey => $wizardGroup) {
245+
$orderedWizards = $this->orderWizards($wizards, $this->dependencyOrderingService);
246+
foreach ($orderedWizards as $groupKey => $wizardGroup) {
245247
$groupKey = rtrim($groupKey, '.');
246248
$groupItems = [];
247249
$appendWizardElements = $appendWizards[$groupKey . '.']['elements.'] ?? null;
@@ -273,20 +275,27 @@ protected function loadAvailableWizards(): array
273275
$typeField = (string)($GLOBALS['TCA']['tt_content']['ctrl']['type'] ?? '');
274276
$fieldConfig = $GLOBALS['TCA']['tt_content']['columns'][$typeField] ?? [];
275277
$items = $fieldConfig['config']['items'] ?? [];
278+
$itemGroups = $fieldConfig['config']['itemGroups'] ?? [];
276279
$groupedWizardItems = [];
280+
// Auto-set positional information based on TCA itemGroups sorting.
281+
$lastGroup = null;
282+
foreach (array_keys($itemGroups) as $groupIdentifier) {
283+
$groupedWizardItems[$groupIdentifier . '.']['header'] = $itemGroups[$groupIdentifier];
284+
if ($lastGroup !== null) {
285+
$groupedWizardItems[$groupIdentifier . '.']['contentElementAfter'] = $lastGroup;
286+
}
287+
$lastGroup = $groupIdentifier;
288+
}
277289
foreach ($items as $item) {
278290
$selectItem = SelectItem::fromTcaItemArray($item);
279291
if ($selectItem->isDivider()) {
280292
continue;
281293
}
282294
$recordType = $selectItem->getValue();
283295
$groupIdentifier = $selectItem->getGroup();
284-
if (!is_array($groupedWizardItems[$groupIdentifier . '.'] ?? null)) {
285-
$groupedWizardItems[$groupIdentifier . '.'] = [
286-
'elements.' => [],
287-
'header' => $fieldConfig['config']['itemGroups'][$groupIdentifier] ?? $groupIdentifier,
288-
];
289-
}
296+
$groupedWizardItems[$groupIdentifier . '.']['elements.'] ??= [];
297+
// In case this group is not defined in itemGroups, use the group identifier as label.
298+
$groupedWizardItems[$groupIdentifier . '.']['header'] ??= $groupIdentifier;
290299
$itemDescription = $selectItem->getDescription();
291300
$wizardEntry = [
292301
'iconIdentifier' => $selectItem->getIcon(),
@@ -335,6 +344,55 @@ protected function mergeContentElementWizardsWithPageTSConfigWizards(array $cont
335344
return $mergedWizards;
336345
}
337346

347+
/**
348+
* There are two separate ordering systems for wizard groups:
349+
* 1. TCA itemGroup sorting by associative array item order.
350+
* 2. PageTS defined order by "before" and "after".
351+
*
352+
* System 1. has a well-defined order, where every item defines "after" (linked list).
353+
* Due to this, the two system cannot be combined.
354+
* As soon as system 2 defines at least one "before" or "after" it takes over.
355+
*/
356+
protected function orderWizards(array $wizards, DependencyOrderingService $dependencyOrderingService): array
357+
{
358+
// First round: Order by TCA defined sorting.
359+
$hasAtLeastOnePositionalArgument = false;
360+
foreach ($wizards as $group => $wizard) {
361+
if (isset($wizard['before'])) {
362+
$hasAtLeastOnePositionalArgument = true;
363+
$wizards[$group]['pageTsBefore'] = $wizard['before'];
364+
unset($wizards[$group]['before']);
365+
}
366+
if (isset($wizard['after'])) {
367+
$hasAtLeastOnePositionalArgument = true;
368+
$wizards[$group]['pageTsAfter'] = $wizard['after'];
369+
unset($wizards[$group]['after']);
370+
}
371+
if (isset($wizard['contentElementAfter'])) {
372+
$wizards[$group]['after'] = $wizard['contentElementAfter'];
373+
unset($wizards[$group]['contentElementAfter']);
374+
}
375+
}
376+
// No order defined by pageTS. Use TCA sorting.
377+
if (!$hasAtLeastOnePositionalArgument) {
378+
return $dependencyOrderingService->orderByDependencies($wizards);
379+
}
380+
// Override order by pageTsConfig.
381+
foreach ($wizards as $group => $wizard) {
382+
// Unset "after" previously set by Content Element wizards.
383+
unset($wizards[$group]['after']);
384+
if (isset($wizard['pageTsBefore'])) {
385+
$wizards[$group]['before'] = $wizard['pageTsBefore'];
386+
unset($wizards[$group]['pageTsBefore']);
387+
}
388+
if (isset($wizard['pageTsAfter'])) {
389+
$wizards[$group]['after'] = $wizard['pageTsAfter'];
390+
unset($wizards[$group]['pageTsAfter']);
391+
}
392+
}
393+
return $dependencyOrderingService->orderByDependencies($wizards);
394+
}
395+
338396
/**
339397
* This method returns the wizard items, defined in Page TSconfig for b/w
340398
* compatibility.

typo3/sysext/backend/Tests/Unit/Controller/ContentElement/NewContentElementControllerTest.php

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
use PHPUnit\Framework\Attributes\Test;
2121
use TYPO3\CMS\Backend\Controller\ContentElement\NewContentElementController;
22+
use TYPO3\CMS\Core\Service\DependencyOrderingService;
2223
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
2324

2425
final class NewContentElementControllerTest extends UnitTestCase
@@ -283,4 +284,212 @@ public function mergeContentElementWizardsWithPageTSConfigWizardsRemovesDuplicat
283284

284285
self::assertSame($expected, $result);
285286
}
287+
288+
#[Test]
289+
public function contentElementWizardsAreLinkedTogetherWithAfterPosition(): void
290+
{
291+
$GLOBALS['TCA']['tt_content']['ctrl']['type'] = 'CType';
292+
$GLOBALS['TCA']['tt_content']['columns']['CType'] = [
293+
'config' => [
294+
'type' => 'select',
295+
'renderType' => 'selectSingle',
296+
'items' => [
297+
[
298+
'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.header',
299+
'description' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.header.description',
300+
'value' => 'header',
301+
'icon' => 'content-header',
302+
'group' => 'default',
303+
],
304+
[
305+
'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.text',
306+
'description' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.text.description',
307+
'value' => 'text',
308+
'icon' => 'content-text',
309+
'group' => 'default',
310+
],
311+
],
312+
'itemGroups' => [
313+
'default' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:group.default',
314+
'lists' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:group.lists',
315+
'menu' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:group.menu',
316+
'forms' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:group.forms',
317+
'special' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:group.special',
318+
'plugins' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:group.plugins',
319+
],
320+
],
321+
];
322+
$expected = [
323+
'default.' => [
324+
'header' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:group.default',
325+
'elements.' => [
326+
'header.' => [
327+
'iconIdentifier' => 'content-header',
328+
'title' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.header',
329+
'description' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.header.description',
330+
'defaultValues' => [
331+
'CType' => 'header',
332+
],
333+
],
334+
'text.' => [
335+
'iconIdentifier' => 'content-text',
336+
'title' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.text',
337+
'description' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.text.description',
338+
'defaultValues' => [
339+
'CType' => 'text',
340+
],
341+
],
342+
],
343+
],
344+
'lists.' => [
345+
'header' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:group.lists',
346+
'contentElementAfter' => 'default',
347+
],
348+
'menu.' => [
349+
'header' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:group.menu',
350+
'contentElementAfter' => 'lists',
351+
],
352+
'forms.' => [
353+
'header' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:group.forms',
354+
'contentElementAfter' => 'menu',
355+
],
356+
'special.' => [
357+
'header' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:group.special',
358+
'contentElementAfter' => 'forms',
359+
],
360+
'plugins.' => [
361+
'header' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:group.plugins',
362+
'contentElementAfter' => 'special',
363+
],
364+
];
365+
$result = (new \ReflectionClass(NewContentElementController::class))
366+
->getMethod('loadAvailableWizards')
367+
->invokeArgs(
368+
$this->createMock(
369+
NewContentElementController::class
370+
),
371+
[]
372+
);
373+
self::assertSame($expected, $result);
374+
}
375+
376+
#[Test]
377+
public function contentElementWizardsAreOrderedByContentElementAfter(): void
378+
{
379+
$wizards = [
380+
'default.' => [
381+
'header' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:group.default',
382+
'elements.' => [
383+
'header.' => [
384+
'iconIdentifier' => 'content-header',
385+
'title' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.header',
386+
'description' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.header.description',
387+
'defaultValues' => [
388+
'CType' => 'header',
389+
],
390+
],
391+
'text.' => [
392+
'iconIdentifier' => 'content-text',
393+
'title' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.text',
394+
'description' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.text.description',
395+
'defaultValues' => [
396+
'CType' => 'text',
397+
],
398+
],
399+
],
400+
],
401+
'forms.' => [
402+
'header' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:group.forms',
403+
'contentElementAfter' => [
404+
'menu.',
405+
],
406+
],
407+
'menu.' => [
408+
'header' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:group.menu',
409+
'contentElementAfter' => [
410+
'lists.',
411+
],
412+
],
413+
'plugins.' => [
414+
'header' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:group.plugins',
415+
'contentElementAfter' => [
416+
'special.',
417+
],
418+
],
419+
'lists.' => [
420+
'header' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:group.lists',
421+
'contentElementAfter' => [
422+
'default.',
423+
],
424+
],
425+
'special.' => [
426+
'header' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:group.special',
427+
'contentElementAfter' => [
428+
'forms.',
429+
],
430+
],
431+
];
432+
$expected = [
433+
'default.' => [
434+
'header' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:group.default',
435+
'elements.' => [
436+
'header.' => [
437+
'iconIdentifier' => 'content-header',
438+
'title' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.header',
439+
'description' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.header.description',
440+
'defaultValues' => [
441+
'CType' => 'header',
442+
],
443+
],
444+
'text.' => [
445+
'iconIdentifier' => 'content-text',
446+
'title' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.text',
447+
'description' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.text.description',
448+
'defaultValues' => [
449+
'CType' => 'text',
450+
],
451+
],
452+
],
453+
],
454+
'lists.' => [
455+
'header' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:group.lists',
456+
'after' => [
457+
'default.',
458+
],
459+
],
460+
'menu.' => [
461+
'header' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:group.menu',
462+
'after' => [
463+
'lists.',
464+
],
465+
],
466+
'forms.' => [
467+
'header' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:group.forms',
468+
'after' => [
469+
'menu.',
470+
],
471+
],
472+
'special.' => [
473+
'header' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:group.special',
474+
'after' => [
475+
'forms.',
476+
],
477+
],
478+
'plugins.' => [
479+
'header' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:group.plugins',
480+
'after' => [
481+
'special.',
482+
],
483+
],
484+
];
485+
$result = (new \ReflectionClass(NewContentElementController::class))
486+
->getMethod('orderWizards')
487+
->invokeArgs(
488+
$this->createMock(
489+
NewContentElementController::class
490+
),
491+
[$wizards, new DependencyOrderingService()]
492+
);
493+
self::assertSame($expected, $result);
494+
}
286495
}

0 commit comments

Comments
 (0)