Skip to content

Commit

Permalink
[BUGFIX] !!! sorting of content elements
Browse files Browse the repository at this point in the history
behaviour of sorting is changed in two ways:
 * element at first position in container column
  * old: pid was used as target (leeds to broken sorting)
  * new: use -uid of container element for first column
  * new: use -uid of previous column child (if exists), (else -uid of container)
 * element after a container
  * old: -uid of container is used (leeds to broken sorting)
  * new: -uid of last child in containers last column is used

we shift a migration command to fix broken sorting:
dry-run: container:sorting
run: container:sorting 0
must be called multiple for nested containers

Fixes: #149
  • Loading branch information
Achim Fritz committed May 20, 2022
1 parent 625ad7c commit a1cc4de
Show file tree
Hide file tree
Showing 47 changed files with 1,473 additions and 34 deletions.
14 changes: 11 additions & 3 deletions Classes/Backend/Grid/ContainerGridColumn.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,19 @@ class ContainerGridColumn extends GridColumn

protected $allowNewContentElements = true;

public function __construct(PageLayoutContext $context, array $columnDefinition, Container $container, bool $allowNewContentElements = true)
{
protected $newContentElementAtTopTarget;

public function __construct(
PageLayoutContext $context,
array $columnDefinition,
Container $container,
int $newContentElementAtTopTarget,
bool $allowNewContentElements = true
) {
parent::__construct($context, $columnDefinition);
$this->container = $container;
$this->allowNewContentElements = $allowNewContentElements;
$this->newContentElementAtTopTarget = $newContentElementAtTopTarget;
}

public function getContainerUid(): int
Expand Down Expand Up @@ -63,7 +71,7 @@ public function getNewContentUrl(): string
'sys_language_uid' => $this->container->getLanguage(),
'colPos' => $this->getColumnNumber(),
'tx_container_parent' => $this->container->getUidOfLiveWorkspace(),
'uid_pid' => $pageId,
'uid_pid' => $this->newContentElementAtTopTarget,
'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI'),
];
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
Expand Down
20 changes: 16 additions & 4 deletions Classes/Backend/Preview/ContainerPreviewRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use B13\Container\ContentDefender\ContainerColumnConfigurationService;
use B13\Container\Domain\Factory\Exception;
use B13\Container\Domain\Factory\PageView\Backend\ContainerFactory;
use B13\Container\Domain\Service\ContainerService;
use B13\Container\Tca\Registry;
use TYPO3\CMS\Backend\Preview\StandardContentPreviewRenderer;
use TYPO3\CMS\Backend\Utility\BackendUtility;
Expand Down Expand Up @@ -44,11 +45,21 @@ class ContainerPreviewRenderer extends StandardContentPreviewRenderer
*/
protected $containerColumnConfigurationService;

public function __construct(Registry $tcaRegistry = null, ContainerFactory $containerFactory = null, ContainerColumnConfigurationService $containerColumnConfigurationService = null)
{
/**
* @var ContainerService
*/
protected $containerService;

public function __construct(
Registry $tcaRegistry = null,
ContainerFactory $containerFactory = null,
ContainerColumnConfigurationService $containerColumnConfigurationService = null,
ContainerService $containerService = null
) {
$this->tcaRegistry = $tcaRegistry ?? GeneralUtility::makeInstance(Registry::class);
$this->containerFactory = $containerFactory ?? GeneralUtility::makeInstance(ContainerFactory::class);
$this->containerColumnConfigurationService = $containerColumnConfigurationService ?? GeneralUtility::makeInstance(ContainerColumnConfigurationService::class);
$this->containerService = $containerService ?? GeneralUtility::makeInstance(ContainerService::class);
}

public function renderPageModulePreviewContent(GridColumnItem $item): string
Expand All @@ -67,10 +78,11 @@ public function renderPageModulePreviewContent(GridColumnItem $item): string
foreach ($containerGrid as $row => $cols) {
$rowObject = GeneralUtility::makeInstance(GridRow::class, $context);
foreach ($cols as $col) {
$newContentElementAtTopTarget = $this->containerService->getNewContentElementAtTopTargetInColumn($container, $col['colPos']);
if ($this->containerColumnConfigurationService->isMaxitemsReached($container, $col['colPos'])) {
$columnObject = GeneralUtility::makeInstance(ContainerGridColumn::class, $context, $col, $container, false);
$columnObject = GeneralUtility::makeInstance(ContainerGridColumn::class, $context, $col, $container, $newContentElementAtTopTarget, false);
} else {
$columnObject = GeneralUtility::makeInstance(ContainerGridColumn::class, $context, $col, $container);
$columnObject = GeneralUtility::makeInstance(ContainerGridColumn::class, $context, $col, $container, $newContentElementAtTopTarget);
}
$rowObject->addColumn($columnObject);
if (isset($col['colPos'])) {
Expand Down
60 changes: 60 additions & 0 deletions Classes/Command/SortingCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace B13\Container\Command;

/*
* This file is part of TYPO3 CMS-based extension "container" by b13.
*
* 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.
*/

use B13\Container\Integrity\Sorting;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class SortingCommand extends Command
{

/**
* @var Sorting
*/
protected $sorting;

protected function configure()
{
$this->addArgument('dryrun', InputArgument::OPTIONAL, 'do not execute queries', true);
}

public function __construct(string $name = null, Sorting $sorting = null)
{
parent::__construct($name);
$this->sorting = $sorting ?? GeneralUtility::makeInstance(Sorting::class);
}

/**
* @param InputInterface $input
* @param OutputInterface $output
*/
public function execute(InputInterface $input, OutputInterface $output): int
{
$dryrun = (bool)$input->getArgument('dryrun');
Bootstrap::initializeBackendAuthentication();
Bootstrap::initializeLanguageObject();
$errors = $this->sorting->run($dryrun);
foreach ($errors as $error) {
$output->writeln($error);
}
if (empty($errors)) {
$output->writeln('migration finished');
}
return 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ public function setContainerIsCopied($containerId): void

public function getTargetColPosForNew(int $containerId, int $colPos): ?int
{
//var_dump($containerId);
if (isset($this->copyMapping[$containerId . '-' . $colPos])) {
return $this->copyMapping[$containerId . '-' . $colPos]['targetColPos'];
}
Expand Down
6 changes: 6 additions & 0 deletions Classes/ContentDefender/Xclasses/DatamapHook.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ protected function isRecordAllowedByRestriction(array $columnConfiguration, arra
$this->mapping[$record['uid']]['containerId'],
$this->mapping[$record['uid']]['colPos']
);
} elseif ($record['tx_container_parent'] > 0) {
$columnConfiguration = $this->containerColumnConfigurationService->override(
$columnConfiguration,
(int)$record['tx_container_parent'],
(int)$record['colPos']
);
}
return parent::isRecordAllowedByRestriction($columnConfiguration, $record);
}
Expand Down
72 changes: 72 additions & 0 deletions Classes/Domain/Service/ContainerService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace B13\Container\Domain\Service;

/*
* This file is part of TYPO3 CMS-based extension "container" by b13.
*
* 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.
*/

use B13\Container\Domain\Factory\ContainerFactory;
use B13\Container\Domain\Model\Container;
use B13\Container\Tca\Registry;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class ContainerService implements SingletonInterface
{
/**
* @var Registry
*/
protected $tcaRegistry;

/**
* @var ContainerFactory
*/
protected $containerFactory;

public function __construct(Registry $tcaRegistry = null, ContainerFactory $containerFactory = null)
{
$this->tcaRegistry = $tcaRegistry ?? GeneralUtility::makeInstance(Registry::class);
$this->containerFactory = $containerFactory ?? GeneralUtility::makeInstance(ContainerFactory::class);
}

public function getNewContentElementAtTopTargetInColumn(Container $container, int $targetColPos): int
{
$target = -$container->getUid();
$previousRecord = null;
$allColumns = $this->tcaRegistry->getAllAvailableColumnsColPos($container->getCType());
foreach ($allColumns as $colPos) {
if ($colPos === $targetColPos && $previousRecord !== null) {
$target = -$previousRecord['uid'];
}
$children = $container->getChildrenByColPos($colPos);
if (!empty($children)) {
$last = array_pop($children);
$previousRecord = $last;
}
}
return $target;
}

public function getAfterContainerElementTarget(Container $container): int
{
$target = -$container->getUid();
$containerRecord = $container->getContainerRecord();
$childRecords = $container->getChildRecords();
if (empty($childRecords)) {
return $target;
}
$lastChild = array_pop($childRecords);
if (!$this->tcaRegistry->isContainerElement($lastChild['CType'])) {
return -$lastChild['uid'];
}
$container = $this->containerFactory->buildContainer($lastChild['uid']);
return $this->getAfterContainerElementTarget($container);
}
}
2 changes: 1 addition & 1 deletion Classes/Hooks/Datahandler/CommandMapAfterFinishHook.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public function processCmdmap_afterFinish(DataHandler $dataHandler): void
// copied from non default language (connectecd mode) children
if ($copiedFromChild['sys_language_uid'] > 0 && $copiedFromChild['l18n_parent'] > 0) {
// fetch orig container
$origContainer = $this->database->fetchOneTranslatedRecord($copiedFromChild['tx_container_parent'], $copiedFromChild['sys_language_uid']);
$origContainer = $this->database->fetchOneTranslatedRecordByl18nParent($copiedFromChild['tx_container_parent'], $copiedFromChild['sys_language_uid']);
// should never be null
if ($origContainer !== null) {
$freeModeContainer = $this->database->fetchContainerRecordLocalizedFreeMode((int)$origContainer['uid'], $copyToLanguage);
Expand Down
94 changes: 92 additions & 2 deletions Classes/Hooks/Datahandler/CommandMapBeforeStartHook.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
*/

use B13\Container\Domain\Factory\ContainerFactory;
use B13\Container\Domain\Factory\Exception;
use B13\Container\Domain\Service\ContainerService;
use B13\Container\Tca\Registry;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;
Expand All @@ -35,20 +37,28 @@ class CommandMapBeforeStartHook
*/
protected $database;

/**
* @var ContainerService
*/
protected $containerService;

/**
* UsedRecords constructor.
* @param ContainerFactory|null $containerFactory
* @param Registry|null $tcaRegistry
* @param Database|null $database
* @param ContainerService|null $containerService
*/
public function __construct(
ContainerFactory $containerFactory = null,
Registry $tcaRegistry = null,
Database $database = null
Database $database = null,
ContainerService $containerService = null
) {
$this->containerFactory = $containerFactory ?? GeneralUtility::makeInstance(ContainerFactory::class);
$this->tcaRegistry = $tcaRegistry ?? GeneralUtility::makeInstance(Registry::class);
$this->database = $database ?? GeneralUtility::makeInstance(Database::class);
$this->containerService = $containerService ?? GeneralUtility::makeInstance(ContainerService::class);
}
/**
* @param DataHandler $dataHandler
Expand All @@ -58,6 +68,86 @@ public function processCmdmap_beforeStart(DataHandler $dataHandler): void
$this->unsetInconsistentLocalizeCommands($dataHandler);
$dataHandler->cmdmap = $this->rewriteSimpleCommandMap($dataHandler->cmdmap);
$dataHandler->cmdmap = $this->extractContainerIdFromColPosOnUpdate($dataHandler->cmdmap);
// previously page id is used for copy/moving element at top of a container colum
// but this leeds to wrong sorting in page context (e.g. List-Module)
$dataHandler->cmdmap = $this->rewriteCommandMapTargetForTopAtContainer($dataHandler->cmdmap);
$dataHandler->cmdmap = $this->rewriteCommandMapTargetForAfterContainer($dataHandler->cmdmap);
}

protected function rewriteCommandMapTargetForAfterContainer(array $cmdmap): array
{
if (!empty($cmdmap['tt_content'])) {
foreach ($cmdmap['tt_content'] as $id => &$cmd) {
foreach ($cmd as $operation => $value) {
if (in_array($operation, ['copy', 'move'], true) === false) {
continue;
}
if (
(!isset($value['update']['tx_container_parent']) || (int)$value['update']['tx_container_parent'] === 0) &&
((is_array($value) && $value['target'] < 0) || (int)$value < 0)
) {
if (is_array($value)) {
$target = -(int)$value['target'];
} else {
// simple command
$target = -(int)$value;
}
$record = $this->database->fetchOneRecord($target);
if ($record['tx_container_parent'] > 0) {
// elements in container have already correct target
continue;
}
if (!$this->tcaRegistry->isContainerElement($record['CType'])) {
continue;
}
try {
$container = $this->containerFactory->buildContainer($record['uid']);
$target = $this->containerService->getAfterContainerElementTarget($container);
if (is_array($value)) {
$cmd[$operation]['target'] = $target;
} else {
// simple command
$cmd[$operation] = $target;
}
} catch (Exception $e) {
continue;
}
}
}
}
}
return $cmdmap;
}

protected function rewriteCommandMapTargetForTopAtContainer(array $cmdmap): array
{
if (!empty($cmdmap['tt_content'])) {
foreach ($cmdmap['tt_content'] as $id => &$cmd) {
foreach ($cmd as $operation => $value) {
if (in_array($operation, ['copy', 'move'], true) === false) {
continue;
}

if (
isset($value['update']) &&
isset($value['update']['tx_container_parent']) &&
$value['update']['tx_container_parent'] > 0 &&
isset($value['update']['colPos']) &&
$value['update']['colPos'] > 0 &&
$value['target'] > 0
) {
try {
$container = $this->containerFactory->buildContainer((int)$value['update']['tx_container_parent']);
$target = $this->containerService->getNewContentElementAtTopTargetInColumn($container, (int)$value['update']['colPos']);
$cmd[$operation]['target'] = $target;
} catch (Exception $e) {
// not a container
}
}
}
}
}
return $cmdmap;
}

protected function rewriteSimpleCommandMap(array $cmdmap): array
Expand Down Expand Up @@ -111,7 +201,7 @@ protected function unsetInconsistentLocalizeCommands(DataHandler $dataHandler):
// should not happen
continue;
}
$translatedContainer = $this->database->fetchOneTranslatedRecord($container['uid'], (int)$data);
$translatedContainer = $this->database->fetchOneTranslatedRecordByl18nParent($container['uid'], (int)$data);
if ($translatedContainer === null || (int)$translatedContainer['l18n_parent'] === 0) {
$dataHandler->log(
'tt_content',
Expand Down
1 change: 1 addition & 0 deletions Classes/Hooks/Datahandler/CommandMapPostProcessingHook.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ protected function localizeOrCopyToLanguage(int $uid, int $language, string $com
try {
$container = $this->containerFactory->buildContainer($uid);
$children = $container->getChildRecords();
$children = array_reverse($children);

This comment has been minimized.

Copy link
@liayn

liayn Dec 20, 2024

This line seems to be the cause for #500

$cmd = ['tt_content' => []];
foreach ($children as $colPos => $record) {
$cmd['tt_content'][$record['uid']] = [$command => $language];
Expand Down
Loading

0 comments on commit a1cc4de

Please sign in to comment.