Commit 49706612 authored by Christian Kuhn's avatar Christian Kuhn Committed by Morton Jonuschat
Browse files

[TASK] FormEngine: Inline titles and expand / collapse

Fetch the last pieces from InlineRecordContainer record title
preparation to record title data provider.
Refactor InlineRecordContainer to be better readable.
Handle expand / collapse state within data provider to only
calculate all record fields if the record is opened. Not
always adding collapsed records to the calculation has
significant performance advantages especially in nested
inline scenarious.

Change-Id: I83a457bd798dc47cc12a8dfb096132394d6bd357
Resolves: #71353
Releases: master
Reviewed-on: https://review.typo3.org/44557

Reviewed-by: Wouter Wolters's avatarWouter Wolters <typo3@wouterwolters.nl>
Tested-by: Wouter Wolters's avatarWouter Wolters <typo3@wouterwolters.nl>
Reviewed-by: default avatarMorton Jonuschat <m.jonuschat@mojocode.de>
Tested-by: default avatarMorton Jonuschat <m.jonuschat@mojocode.de>
parent 2f1e6d99
......@@ -209,14 +209,27 @@ class InlineControlContainer extends AbstractContainer
$resultArray['inlineData'] = $this->inlineData;
// Render the localization links
// @todo: It might be a good idea to have something like "isLocalizedRecord" or similar set by a data provider
$isLocalizedParent = $language > 0
&& $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']][0] > 0
&& MathUtility::canBeInterpretedAsInteger($row['uid']);
$numberOfFullLocalizedChildren = 0;
$numberOfNotYetLocalizedChildren = 0;
foreach ($this->data['parameterArray']['fieldConf']['children'] as $child) {
if (!$child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
$numberOfFullLocalizedChildren ++;
}
if ($isLocalizedParent && $child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
$numberOfNotYetLocalizedChildren ++;
}
}
// Render the localization links if needed
$localizationLinks = '';
// @todo if isInlineDefaultLanguageRecordInLocalizedParentContext
// @todo: Would be even more cool if the localize button is only shown if there are any not yet localized children
if ($language > 0 && $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']][0] > 0 && MathUtility::canBeInterpretedAsInteger($row['uid'])) {
if ($numberOfNotYetLocalizedChildren) {
// Add the "Localize all records" link before all child records:
if (isset($config['appearance']['showAllLocalizationLink']) && $config['appearance']['showAllLocalizationLink']) {
$localizationLinks .= ' ' . $this->getLevelInteractionLink('localize', $nameObject . '-' . $foreign_table, $config);
$localizationLinks = ' ' . $this->getLevelInteractionLink('localize', $nameObject . '-' . $foreign_table, $config);
}
// Add the "Synchronize with default language" link before all child records:
if (isset($config['appearance']['showSynchronizationLink']) && $config['appearance']['showSynchronizationLink']) {
......@@ -224,14 +237,8 @@ class InlineControlContainer extends AbstractContainer
}
}
$numberOfFullChildren = 0;
foreach ($this->data['parameterArray']['fieldConf']['children'] as $child) {
if (!$child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
$numberOfFullChildren ++;
}
}
// Define how to show the "Create new record" link - if there are more than maxitems, hide it
if ($numberOfFullChildren >= $config['maxitems'] || $uniqueMax > 0 && $numberOfFullChildren >= $uniqueMax) {
if ($numberOfFullLocalizedChildren >= $config['maxitems'] || $uniqueMax > 0 && $numberOfFullLocalizedChildren >= $uniqueMax) {
$config['inline']['inlineNewButtonStyle'] = 'display: none;';
$config['inline']['inlineNewRelationButtonStyle'] = 'display: none;';
}
......
......@@ -205,7 +205,14 @@ class FormDataCompiler
'flexParentDatabaseRow' => [],
// BackendUser->uc['inlineView'] - This array holds status of expand / collapsed inline items
// @todo: better documentation of nesting behaviour and bug fixing in this area
// This array is "flat", an inline structure with parent uid 1 having firstChild uid 2 having secondChild uid 3
// firstChild and secondChild are not nested. If an uid is set it means "record is expanded", example:
// 'parent' => [
// 1 => [
// 'firstChild' => [ 2 ], // record 2 of firstChild table is open in inline context to parent 1
// 'secondChild' => [ 3 ], // record 3 of secondChild table is open in inline context to parent 1
// ],
// ]
'inlineExpandCollapseStateArray' => [],
// The "entry" pid for inline records. Nested inline records can potentially hang around on different
// pid's, but the entry pid is needed for AJAX calls, so that they would know where the action takes
......
......@@ -44,6 +44,16 @@ class TcaColumnsProcessRecordTitle implements FormDataProviderInterface
$result['columnsToProcess'] = array_merge($result['columnsToProcess'], array_filter($labelColumns));
}
// Add foreign_label to process list if exists and the record is an inline child
if ($result['isInlineChild'] && isset($result['inlineParentConfig']['foreign_label'])) {
$result['columnsToProcess'][] = $result['inlineParentConfig']['foreign_label'];
}
// Add symmetric_label to process list if exists and the record is an inline child
if ($result['isInlineChild'] && isset($result['inlineParentConfig']['symmetric_label'])) {
$result['columnsToProcess'][] = $result['inlineParentConfig']['symmetric_label'];
}
return $result;
}
}
......@@ -47,6 +47,26 @@ class TcaColumnsProcessShowitem implements FormDataProviderInterface
return $result;
}
$addShowItemFieldsToColumnsToProcess = true;
if ($result['isInlineChild']) {
// If the record is an inline child that is not expanded, it is not necessary to calculate all fields
$isExistingRecord = $result['command'] === 'edit';
$inlineConfig = $result['inlineParentConfig'];
$collapseAll = isset($inlineConfig['appearance']['collapseAll']) && $inlineConfig['appearance']['collapseAll'];
$expandCollapseStateArray = $result['inlineExpandCollapseStateArray'];
$foreignTable = $result['inlineParentConfig']['foreign_table'];
$isExpandedByUcState = isset($expandCollapseStateArray[$foreignTable])
&& is_array($expandCollapseStateArray[$foreignTable])
&& in_array($result['databaseRow']['uid'], $expandCollapseStateArray[$foreignTable]) !== false;
if ($isExistingRecord && ($collapseAll || !$isExpandedByUcState) && !$result['isInlineAjaxOpeningContext']) {
$addShowItemFieldsToColumnsToProcess = false;
}
}
if (!$addShowItemFieldsToColumnsToProcess) {
return $result;
}
$showItemFieldString = $result['processedTca']['types'][$recordTypeValue]['showitem'];
$showItemFieldArray = GeneralUtility::trimExplode(',', $showItemFieldString, true);
......
......@@ -15,10 +15,9 @@ namespace TYPO3\CMS\Backend\Form\FormDataProvider;
*/
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Remove fields from columns not in showitem or palette list.
* Remove fields from columns not in showitem or palette list or needed otherwise
* This is a relatively effective performance improvement preventing other
* providers from resolving stuff of fields that are not shown later.
* Especially effective for fal related tables.
......
......@@ -164,7 +164,6 @@ class TcaInline extends AbstractDatabaseRecordProvider implements FormDataProvid
);
$showPossible = $result['processedTca']['columns'][$fieldName]['config']['appearance']['showPossibleLocalizationRecords'];
$showRemoved = $result['processedTca']['columns'][$fieldName]['config']['appearance']['showRemovedLocalizationRecords'];
// Find which records are localized, which records are not localized and which are
// localized but miss default language record
......
......@@ -15,8 +15,6 @@ namespace TYPO3\CMS\Backend\Form\FormDataProvider;
*/
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
/**
......@@ -33,29 +31,27 @@ class TcaInlineExpandCollapseState implements FormDataProviderInterface
*/
public function addData(array $result)
{
// Early return if a parent record has already set this
if (!empty($result['inlineExpandCollapseStateArray'])) {
// Early return if a parent record has already set this, happens for existing inline children
// when opening a parent record.
return $result;
} elseif (!empty($result['inlineStructure'])) {
/** @var InlineStackProcessor $inlineStackProcessor */
$inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
$inlineStackProcessor->initializeByGivenStructure($result['inlineStructure']);
// Top parent
$parent = $inlineStackProcessor->getStructureLevel(0);
} elseif (!empty($result['inlineTopMostParentUid']) && !empty($result['inlineTopMostParentTableName'])) {
// Happens in inline ajax context, top parent uid and top parent table are set
$fullInlineState = unserialize($this->getBackendUser()->uc['inlineView']);
if (!is_array($fullInlineState)) {
$fullInlineState = [];
}
$inlineStateForTable = [];
if ($result['command'] !== 'new') {
$table = $parent['table'];
$uid = $parent['uid'];
$table = $result['inlineTopMostParentTableName'];
$uid = $result['inlineTopMostParentUid'];
if (!empty($fullInlineState[$table][$uid])) {
$inlineStateForTable = $fullInlineState[$table][$uid];
}
}
$result['inlineExpandCollapseStateArray'] = $inlineStateForTable;
} else {
// Default case for a single record
$fullInlineState = unserialize($this->getBackendUser()->uc['inlineView']);
if (!is_array($fullInlineState)) {
$fullInlineState = [];
......
......@@ -45,6 +45,7 @@ class TcaRecordTitle implements FormDataProviderInterface
}
if ($result['isInlineChild'] && isset($result['processedTca']['ctrl']['formattedLabel_userFunc'])) {
// inline child with formatted user func is first
$parameters = [
'table' => $result['tableName'],
'row' => $result['databaseRow'],
......@@ -63,7 +64,7 @@ class TcaRecordTitle implements FormDataProviderInterface
GeneralUtility::callUserFunction($result['processedTca']['ctrl']['formattedLabel_userFunc'], $parameters, $null);
$result['recordTitle'] = $parameters['title'];
} elseif (isset($result['processedTca']['ctrl']['label_userFunc'])) {
// userFunc takes precedence over everything
// userFunc takes precedence over everything else
$parameters = [
'table' => $result['tableName'],
'row' => $result['databaseRow'],
......@@ -75,7 +76,19 @@ class TcaRecordTitle implements FormDataProviderInterface
$null = null;
GeneralUtility::callUserFunction($result['processedTca']['ctrl']['label_userFunc'], $parameters, $null);
$result['recordTitle'] = $parameters['title'];
} elseif ($result['isInlineChild'] && isset($result['inlineParentConfig']['foreign_label'])
|| $result['isInlineChild'] && isset($result['inlineParentConfig']['symmetric_label'])
) {
// inline child with foreign label or symmetric inline child with symmetric_label
$fieldName = $result['isOnSymmetricSide']
? $result['inlineParentConfig']['symmetric_label']
: $result['inlineParentConfig']['foreign_label'];
// @todo: this is a mixup here. problem is the prep method cuts the string, but also hsc's the thing.
// @todo: this is uncool for the userfuncs, so it is applied only here. however, the OuterWrapContainer
// @todo: also prep()'s the title created by the else patch below ... find a better separation and clean this up!
$result['recordTitle'] = BackendUtility::getRecordTitlePrep($this->getRecordTitleForField($fieldName, $result));
} else {
// standard record
$result = $this->getRecordTitleByLabelProperties($result);
}
......@@ -166,7 +179,7 @@ class TcaRecordTitle implements FormDataProviderInterface
case 'text':
$recordTitle = $this->getRecordTitleForTextType($rawValue);
case 'flex':
// TODO: Check if and how a label could be generated from flex field data
// @todo: Check if and how a label could be generated from flex field data
default:
}
......
......@@ -72,4 +72,40 @@ class TcaColumnsProcessRecordTitleTest extends UnitTestCase
$expected['columnsToProcess'] = ['uid','aField','anotherField'];
$this->assertSame($expected, $this->subject->addData($input));
}
/**
* @test
*/
public function addDataRegistersForeignLabelInInlineContext()
{
$input = [
'columnsToProcess' => [],
'inlineParentConfig' => [
'foreign_label' => 'aForeignLabelField',
],
'isInlineChild' => true,
];
$expected = $input;
$expected['columnsToProcess'] = [ 'aForeignLabelField' ];
$this->assertSame($expected, $this->subject->addData($input));
}
/**
* @test
*/
public function addDataRegistersSymmetricLabelInInlineContext()
{
$input = [
'columnsToProcess' => [],
'inlineParentConfig' => [
'symmetric_label' => 'aSymmetricLabelField',
],
'isInlineChild' => true,
];
$expected = $input;
$expected['columnsToProcess'] = [ 'aSymmetricLabelField' ];
$this->assertSame($expected, $this->subject->addData($input));
}
}
......@@ -175,4 +175,205 @@ class TcaColumnsProcessShowitemTest extends UnitTestCase
$this->assertSame($expected, $this->subject->addData($input));
}
/**
* @test
*/
public function addDataSkipsColumnsForCollapsedInlineChild()
{
$input = [
'command' => 'edit',
'databaseRow' => [
'uid' => 42,
],
'recordTypeValue' => 'aType',
'processedTca' => [
'types' => [
'aType' => [
'showitem' => 'aField',
],
],
'columns' => [
'aField' => [
'config' => [
'type' => 'input',
]
],
],
],
'inlineParentConfig' => [
'foreign_table' => 'aTable',
],
'isInlineChild' => true,
'isInlineAjaxOpeningContext' => false,
'inlineExpandCollapseStateArray' => [],
];
$expected = $input;
$this->assertSame($expected, $this->subject->addData($input));
}
/**
* @test
*/
public function addDataSkipsColumnsForCollapsedAllInlineChild()
{
$input = [
'command' => 'edit',
'databaseRow' => [
'uid' => 42,
],
'recordTypeValue' => 'aType',
'processedTca' => [
'types' => [
'aType' => [
'showitem' => 'aField',
],
],
'columns' => [
'aField' => [
'config' => [
'type' => 'input',
]
],
],
],
'inlineParentConfig' => [
'foreign_table' => 'aTable',
'appearance' => [
'collapseAll' => true,
],
],
'isInlineChild' => true,
'isInlineAjaxOpeningContext' => false,
'inlineExpandCollapseStateArray' => [
'aTable' => [
42,
],
],
];
$expected = $input;
$this->assertSame($expected, $this->subject->addData($input));
}
/**
* @test
*/
public function addDataAddsColumnsForExpandedInlineChild()
{
$input = [
'command' => 'edit',
'databaseRow' => [
'uid' => 42,
],
'recordTypeValue' => 'aType',
'processedTca' => [
'types' => [
'aType' => [
'showitem' => 'aField',
],
],
'columns' => [
'aField' => [
'config' => [
'type' => 'input',
]
],
],
],
'inlineParentConfig' => [
'foreign_table' => 'aTable',
],
'isInlineChild' => true,
'isInlineAjaxOpeningContext' => false,
'inlineExpandCollapseStateArray' => [
'aTable' => [
42,
],
],
];
$expected = $input;
$expected['columnsToProcess'] = ['aField'];
$this->assertSame($expected, $this->subject->addData($input));
}
/**
* @test
*/
public function addDataAddsColumnsForAjaxOpeningContextInlineChild()
{
$input = [
'command' => 'edit',
'databaseRow' => [
'uid' => 42,
],
'recordTypeValue' => 'aType',
'processedTca' => [
'types' => [
'aType' => [
'showitem' => 'aField',
],
],
'columns' => [
'aField' => [
'config' => [
'type' => 'input',
]
],
],
],
'inlineParentConfig' => [
'foreign_table' => 'aTable',
'appearance' => [
'collapseAll' => true,
],
],
'isInlineChild' => true,
'isInlineAjaxOpeningContext' => true,
'inlineExpandCollapseStateArray' => [],
];
$expected = $input;
$expected['columnsToProcess'] = [ 'aField' ];
$this->assertSame($expected, $this->subject->addData($input));
}
/**
* @test
*/
public function addDataAddsColumnsForNewInlineChild()
{
$input = [
'command' => 'new',
'databaseRow' => [
'uid' => 'NEW1234',
],
'recordTypeValue' => 'aType',
'processedTca' => [
'types' => [
'aType' => [
'showitem' => 'aField',
],
],
'columns' => [
'aField' => [
'config' => [
'type' => 'input',
]
],
],
],
'inlineParentConfig' => [
'foreign_table' => 'aTable',
'appearance' => [
'collapseAll' => true,
],
],
'isInlineChild' => true,
'isInlineAjaxOpeningContext' => false,
'inlineExpandCollapseStateArray' => [],
];
$expected = $input;
$expected['columnsToProcess'] = [ 'aField' ];
$this->assertSame($expected, $this->subject->addData($input));
}
}
......@@ -75,15 +75,8 @@ class TcaInlineExpandCollapseStateTest extends UnitTestCase
'databaseRow' => [
'uid' => 13,
],
'inlineStructure' => [
'stable' => [
[
'table' => 'aParentTable',
'uid' => 5,
'field' => 'inline_2',
],
],
],
'inlineTopMostParentTableName' => 'aParentTable',
'inlineTopMostParentUid' => 5,
];
$inlineState = [
'aParentTable' => [
......
......@@ -114,6 +114,71 @@ class TcaRecordTitleTest extends UnitTestCase
$this->assertSame($expected, $this->subject->addData($input));
}
/**
* @test
*/
public function addDataReturnsRecordTitleForInlineChildWithForeignLabel()
{
$input = [
'tableName' => 'aTable',
'databaseRow' => [
'aField' => 'aValue',
],
'processedTca' => [
'ctrl' => [
'label' => 'foo',
],
'columns' => [
'aField' => [
'config' => [
'type' => 'input',
],
],
],
],
'isInlineChild' => true,
'inlineParentConfig' => [
'foreign_label' => 'aField',
],
];
$expected = $input;
$expected['recordTitle'] = 'aValue';
$this->assertSame($expected, $this->subject->addData($input));
}
/**
* @test
*/
public function addDataReturnsRecordTitleForInlineChildWithSymmetricLabel()
{
$input = [
'tableName' => 'aTable',
'databaseRow' => [
'aField' => 'aValue',
],
'processedTca' => [
'ctrl' => [
'label' => 'foo',
],
'columns' => [
'aField' => [
'config' => [
'type' => 'input',
],
],
],
],
'isInlineChild' => true,
'inlineParentConfig' => [
'symmetric_label' => 'aField',
],
'isOnSymmetricSide' => true,
];
$expected = $input;
$expected['recordTitle'] = 'aValue';
$this->assertSame($expected, $this->subject->addData($input));
}
/**
* @test
*/
......