[BUGFIX] Inline children need additional state flags 79/44379/4
authorSebastian Fischer <typo3@evoweb.de>
Thu, 29 Oct 2015 20:43:17 +0000 (21:43 +0100)
committerChristian Kuhn <lolli@schwarzbu.ch>
Fri, 30 Oct 2015 14:30:02 +0000 (15:30 +0100)
Each inline child gets flags added that reflect the state they
are in while being processed. Another patch will later
use more of these flags.

Resolves: #71136
Releases: master
Change-Id: Id86c5efdc6660caba8836691967f09cd69ae1ca7
Reviewed-on: https://review.typo3.org/44379
Reviewed-by: Morton Jonuschat <m.jonuschat@mojocode.de>
Tested-by: Morton Jonuschat <m.jonuschat@mojocode.de>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
typo3/sysext/backend/Classes/Controller/FormInlineAjaxController.php
typo3/sysext/backend/Classes/Form/Container/InlineControlContainer.php
typo3/sysext/backend/Classes/Form/Container/InlineRecordContainer.php
typo3/sysext/backend/Classes/Form/FormDataCompiler.php
typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInline.php
typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInlineExpandCollapseState.php
typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaInlineExpandCollapseStateTest.php

index f33c4b1..f685c3f 100644 (file)
@@ -81,8 +81,6 @@ class FormInlineAjaxController
             'columnsToProcess' => [
                 $parentFieldName
             ],
-            // @todo: still needed?
-            'inlineStructure' => $inlineStackProcessor->getStructure(),
             // Do not resolve existing children, we don't need them now
             'inlineResolveExistingChildren' => false,
         ];
@@ -112,6 +110,8 @@ class FormInlineAjaxController
             'command' => 'new',
             'tableName' => $childTableName,
             'vanillaUid' => $childVanillaUid,
+            'isInlineChild' => true,
+            'inlineStructure' => $inlineStackProcessor->getStructure(),
             'inlineFirstPid' => $inlineFirstPid,
             'inlineParentConfig' => $parentConfig,
         ];
@@ -162,10 +162,6 @@ class FormInlineAjaxController
         }
 
         $childData['inlineParentUid'] = (int)$parent['uid'];
-        // @todo: needed?
-        $childData['inlineStructure'] = $inlineStackProcessor->getStructure();
-        // @todo: needed?
-        $childData['inlineExpandCollapseStateArray'] = $parentData['inlineExpandCollapseStateArray'];
         $childData['renderType'] = 'inlineRecordContainer';
         $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
         $childResult = $nodeFactory->create($childData)->render();
@@ -258,13 +254,9 @@ class FormInlineAjaxController
         // Child, a record from this table should be rendered
         $child = $inlineStackProcessor->getUnstableStructure();
 
-        $childData = $this->compileChild($parentData, $parentFieldName, (int)$child['uid']);
+        $childData = $this->compileChild($parentData, $parentFieldName, (int)$child['uid'], $inlineStackProcessor->getStructure());
 
         $childData['inlineParentUid'] = (int)$parent['uid'];
-        // @todo: needed?
-        $childData['inlineStructure'] = $inlineStackProcessor->getStructure();
-        // @todo: needed?
-        $childData['inlineExpandCollapseStateArray'] = $parentData['inlineExpandCollapseStateArray'];
         $childData['renderType'] = 'inlineRecordContainer';
         $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
         $childResult = $nodeFactory->create($childData)->render();
@@ -339,7 +331,7 @@ class FormInlineAjaxController
                 'columnsToProcess' => [
                     $parentFieldName
                 ],
-                // @todo: still needed?
+                // @todo: still needed? NO!
                 'inlineStructure' => $inlineStackProcessor->getStructure(),
                 // Do not compile existing children, we don't need them now
                 'inlineCompileExistingChildren' => false,
@@ -382,13 +374,9 @@ class FormInlineAjaxController
 
             $localizedItems = array_diff($newItems, $oldItems);
             foreach ($localizedItems as $childUid) {
-                $childData = $this->compileChild($parentData, $parentFieldName, (int)$childUid);
+                $childData = $this->compileChild($parentData, $parentFieldName, (int)$childUid, $inlineStackProcessor->getStructure());
 
                 $childData['inlineParentUid'] = (int)$parent['uid'];
-                // @todo: needed?
-                $childData['inlineStructure'] = $inlineStackProcessor->getStructure();
-                // @todo: needed?
-                $childData['inlineExpandCollapseStateArray'] = $parentData['inlineExpandCollapseStateArray'];
                 $childData['renderType'] = 'inlineRecordContainer';
                 $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
                 $childResult = $nodeFactory->create($childData)->render();
@@ -487,13 +475,14 @@ class FormInlineAjaxController
      * @param array $parentData Result array of parent
      * @param string $parentFieldName Name of parent field
      * @param int $childUid Uid of child to compile
+     * @param array $inlineStructure Current inline structure
      * @return array Full result array
      *
      * @todo: This clones methods compileChild and compileCombinationChild from TcaInline Provider.
      * @todo: Find something around that, eg. some option to force TcaInline provider to calculate a
      * @todo: specific forced-open element only :)
      */
-    protected function compileChild(array $parentData, $parentFieldName, $childUid)
+    protected function compileChild(array $parentData, $parentFieldName, $childUid, array $inlineStructure)
     {
         $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
         $childTableName = $parentConfig['foreign_table'];
@@ -505,15 +494,18 @@ class FormInlineAjaxController
             'command' => 'edit',
             'tableName' => $childTableName,
             'vanillaUid' => (int)$childUid,
+            'isInlineChild' => true,
+            'inlineStructure' => $inlineStructure,
             'inlineFirstPid' => $parentData['inlineFirstPid'],
             'inlineParentConfig' => $parentConfig,
+            'isInlineAjaxOpeningContext' => true,
         ];
         // For foreign_selector with useCombination $mainChild is the mm record
         // and $combinationChild is the child-child. For "normal" relations, $mainChild
         // is just the normal child record and $combinationChild is empty.
         $mainChild = $formDataCompiler->compile($formDataCompilerInput);
         if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
-            $mainChild['combinationChild'] = $this->compileCombinationChild($mainChild, $parentConfig);
+            $mainChild['combinationChild'] = $this->compileCombinationChild($mainChild, $parentConfig, $inlineStructure);
         }
         return $mainChild;
     }
@@ -524,13 +516,14 @@ class FormInlineAjaxController
      *
      * @param array $intermediate Full data array of "mm" record
      * @param array $parentConfig TCA configuration of "parent"
+     * @param array $inlineStructure Current inline structure
      * @return array Full data array of child
      */
-    protected function compileCombinationChild(array $intermediate, array $parentConfig)
+    protected function compileCombinationChild(array $intermediate, array $parentConfig, array $inlineStructure)
     {
         // foreign_selector on intermediate is probably type=select, so data provider of this table resolved that to the uid already
         $intermediateUid = $intermediate['databaseRow'][$parentConfig['foreign_selector']][0];
-        $combinationChild = $this->compileChild($intermediate, $parentConfig['foreign_selector'], $intermediateUid);
+        $combinationChild = $this->compileChild($intermediate, $parentConfig['foreign_selector'], $intermediateUid, $inlineStructure);
         return $combinationChild;
     }
 
index a505579..8c17fdb 100644 (file)
@@ -170,7 +170,7 @@ class InlineControlContainer extends AbstractContainer
             $type = $config['selectorOrUniqueConfiguration']['config']['type'] === 'select' ? 'select' : 'groupdb';
             foreach ($parameterArray['fieldConf']['children'] as $child) {
                 // Determine used unique ids, skip not localized records
-                if (!$child['inlineIsDefaultLanguage']) {
+                if (!$child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
                     $value = $child['databaseRow'][$config['foreign_unique']];
                     // We're assuming there is only one connected value here for both select and group
                     if ($type === 'select') {
@@ -226,7 +226,7 @@ class InlineControlContainer extends AbstractContainer
 
         $numberOfFullChildren = 0;
         foreach ($this->data['parameterArray']['fieldConf']['children'] as $child) {
-            if (!$child['inlineIsDefaultLanguage']) {
+            if (!$child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
                 $numberOfFullChildren ++;
             }
         }
@@ -272,7 +272,7 @@ class InlineControlContainer extends AbstractContainer
             $html .= $childResult['html'];
             $childArray['html'] = '';
             $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childResult);
-            if (!$options['inlineIsDefaultLanguage']) {
+            if (!$options['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
                 // Don't add record to list of "valid" uids if it is only the default
                 // language record of a not yet localized child
                 $sortableRecordUids[] = $options['databaseRow']['uid'];
index 2d538e4..de9d66f 100644 (file)
@@ -117,7 +117,7 @@ class InlineRecordContainer extends AbstractContainer
         // Set this variable if we handle a brand new unsaved record:
         $isNewRecord = !MathUtility::canBeInterpretedAsInteger($record['uid']);
         // Set this variable if the only the default language record and inline child has not been localized yet
-        $isDefaultLanguageRecord = $this->data['inlineIsDefaultLanguage'];
+        $isDefaultLanguageRecord = $this->data['isInlineDefaultLanguageRecordInLocalizedParentContext'];
         // If there is a selector field, normalize it:
         if ($foreign_selector) {
             $valueToNormalize = $record[$foreign_selector];
@@ -452,7 +452,7 @@ class InlineRecordContainer extends AbstractContainer
             /** @var InlineElementHookInterface $hookObj */
             $hookObj->renderForeignRecordHeaderControl_preProcess($parentUid, $foreign_table, $rec, $config, $isVirtualRecord, $enabledControls);
         }
-        if ($data['inlineIsDefaultLanguage']) {
+        if ($data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
             $cells['localize.isLocalizable'] = '<span title="' . $languageService->sL('LLL:EXT:lang/locallang_misc.xlf:localize.isLocalizable', true) . '">'
                 . $this->iconFactory->getIcon('actions-edit-localize-status-low', Icon::SIZE_SMALL)->render()
                 . '</span>';
@@ -571,7 +571,7 @@ class InlineRecordContainer extends AbstractContainer
                                        </span>';
             }
         } elseif ($isVirtualRecord && $isParentExisting) {
-            if ($enabledControls['localize'] && $data['inlineIsDefaultLanguage']) {
+            if ($enabledControls['localize'] && $data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
                 $onClick = 'inline.synchronizeLocalizeRecords(' . GeneralUtility::quoteJSvalue($nameObjectFt) . ', ' . GeneralUtility::quoteJSvalue($rec['uid']) . ');';
                 $cells['localize'] = '
                                        <a class="btn btn-default" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . $languageService->sL('LLL:EXT:lang/locallang_misc.xlf:localize', true) . '">
index 7167c93..01fa832 100644 (file)
@@ -191,18 +191,22 @@ class FormDataCompiler
             'columnsToProcess' => [],
             // If set to TRUE, no wizards are calculated and rendered later
             'disabledWizards' => false,
+
             // BackendUser->uc['inlineView'] - This array holds status of expand / collapsed inline items
             // @todo: better documentation of nesting behaviour and bug fixing in this area
             '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 place on the page structure.
+            // pid's, but the entry pid is needed for AJAX calls, so that they would know where the action takes
+            // place on the page structure.
             'inlineFirstPid' => null,
-            // This array of fields will be set as hidden-fields instead of rendered normally!
-            // This is used by EditDocumentController to force some field values if set as "overrideVals" in _GP
-            'overrideValues' => [],
-
             // The "config" section of an inline parent, prepared and sanitized by TcaInlineConfiguration provider
             'inlineParentConfig' => [],
+            // Flag that is enabled if a records is child of an inline parent
+            'isInlineChild' => false,
+            // Flag if an inline child is expanded so that additional fields need to be rendered
+            'isInlineChildExpanded' => false,
+            // Flag if the inline is in an ajax context that wants to expand the element
+            'isInlineAjaxOpeningContext' => false,
             // Uid of a "child-child" if a new record of an intermediate table is compiled to an existing child. This
             // happens if foreign_selector in parent inline config is set. It will be used by default database row
             // data providers to set this as value for the foreign_selector field on the intermediate table. One use
@@ -214,7 +218,7 @@ class FormDataCompiler
             // Inline scenario: A localized parent record is handled and localizationMode is set to "select", so inline
             // parents can have localized childen. This value is set to TRUE if this array represents a default language
             // child record that was not yet localized.
-            'inlineIsDefaultLanguage' => false,
+            'isInlineDefaultLanguageRecordInLocalizedParentContext' => false,
             // If set, inline children will be resolved. This is set to FALSE in inline ajax context where new children
             // are created and existing children don't matter much.
             'inlineResolveExistingChildren' => true,
@@ -222,13 +226,15 @@ class FormDataCompiler
             // @todo compilation of certain fields is possible
             'inlineCompileExistingChildren' => true,
 
-            // @todo: must be handled / further defined
+            // @todo: keys below must be handled / further defined
             'elementBaseName' => '',
             'flexFormFieldIdentifierPrefix' => 'ID',
             'tabAndInlineStack' => [],
             'inlineData' => [],
             'inlineStructure' => [],
-
+            // This array of fields will be set as hidden-fields instead of rendered normally!
+            // This is used by EditDocumentController to force some field values if set as "overrideVals" in _GP
+            'overrideValues' => [],
         ];
     }
 }
index 5e2839f..0f3b39a 100644 (file)
@@ -184,7 +184,7 @@ class TcaInline extends AbstractDatabaseRecordProvider implements FormDataProvid
                         // If there are still uids in $connectedUidsOfDefaultLanguageRecord, these are records that
                         // exist in default language, but are not localized yet. Compile and mark those
                         $compiledChild = $this->compileChild($result, $fieldName, $defaultLanguageUid);
-                        $compiledChild['inlineIsDefaultLanguage'] = true;
+                        $compiledChild['isInlineDefaultLanguageRecordInLocalizedParentContext'] = true;
                         $result['processedTca']['columns'][$fieldName]['children'][] = $compiledChild;
                     }
                 }
@@ -230,6 +230,7 @@ class TcaInline extends AbstractDatabaseRecordProvider implements FormDataProvid
                         ],
                     ],
                 ],
+                'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'],
             ];
             /** @var OnTheFly $formDataGroup */
             $formDataGroup = GeneralUtility::makeInstance(OnTheFly::class);
@@ -265,6 +266,9 @@ class TcaInline extends AbstractDatabaseRecordProvider implements FormDataProvid
             'command' => 'edit',
             'tableName' => $childTableName,
             'vanillaUid' => (int)$childUid,
+            'isInlineChild' => true,
+            'inlineStructure' => $result['inlineStructure'],
+            'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'],
             'inlineFirstPid' => $result['inlineFirstPid'],
             'inlineParentConfig' => $parentConfig,
         ];
index 8fc55a9..556685a 100644 (file)
@@ -15,6 +15,8 @@ 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;
 
 /**
@@ -31,19 +33,44 @@ class TcaInlineExpandCollapseState implements FormDataProviderInterface
      */
     public function addData(array $result)
     {
-        $fullInlineState = unserialize($this->getBackendUser()->uc['inlineView']);
-        if (!is_array($fullInlineState)) {
-            $fullInlineState = [];
-        }
-        $inlineStateForTable = [];
-        if ($result['command'] !== 'new') {
-            $table = $result['tableName'];
-            $uid = $result['databaseRow']['uid'];
-            if (!empty($fullInlineState[$table][$uid])) {
-                $inlineStateForTable = $fullInlineState[$table][$uid];
+        // Early return if a parent record has already set this
+        if (!empty($result['inlineExpandCollapseStateArray'])) {
+            return $result;
+        } elseif (!empty($result['inlineStructure'])) {
+            /** @var InlineStackProcessor $inlineStackProcessor */
+            $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
+            $inlineStackProcessor->initializeByGivenStructure($result['inlineStructure']);
+            // Top parent
+            $parent = $inlineStackProcessor->getStructureLevel(0);
+            $fullInlineState = unserialize($this->getBackendUser()->uc['inlineView']);
+            if (!is_array($fullInlineState)) {
+                $fullInlineState = [];
+            }
+            $inlineStateForTable = [];
+            if ($result['command'] !== 'new') {
+                $table = $parent['table'];
+                $uid = $parent['uid'];
+                if (!empty($fullInlineState[$table][$uid])) {
+                    $inlineStateForTable = $fullInlineState[$table][$uid];
+                }
+            }
+            $result['inlineExpandCollapseStateArray'] = $inlineStateForTable;
+        } else {
+            $fullInlineState = unserialize($this->getBackendUser()->uc['inlineView']);
+            if (!is_array($fullInlineState)) {
+                $fullInlineState = [];
             }
+            $inlineStateForTable = [];
+            if ($result['command'] !== 'new') {
+                $table = $result['tableName'];
+                $uid = $result['databaseRow']['uid'];
+                if (!empty($fullInlineState[$table][$uid])) {
+                    $inlineStateForTable = $fullInlineState[$table][$uid];
+                }
+            }
+            $result['inlineExpandCollapseStateArray'] = $inlineStateForTable;
         }
-        $result['inlineExpandCollapseStateArray'] = $inlineStateForTable;
+
         return $result;
     }
 
index d2134ff..ed6c322 100644 (file)
@@ -63,4 +63,50 @@ class TcaInlineExpandCollapseStateTest extends UnitTestCase
         $expected['inlineExpandCollapseStateArray'] = $inlineState['aParentTable'][5];
         $this->assertSame($expected, $this->subject->addData($input));
     }
+
+    /**
+     * @test
+     */
+    public function addDataAddsInlineStatusForSecondLevelChild()
+    {
+        $input = [
+            'command' => 'edit',
+            'tableName' => 'bChildTable',
+            'databaseRow' => [
+                'uid' => 13,
+            ],
+            'inlineStructure' =>  [
+                'stable' => [
+                    [
+                        'table' => 'aParentTable',
+                        'uid' => 5,
+                        'field' => 'inline_2',
+                    ],
+                ],
+            ],
+        ];
+        $inlineState = [
+            'aParentTable' => [
+                5 => [
+                    'aChildTable' => [
+                        // Records 23 and 42 are expanded
+                        23,
+                        42,
+                    ],
+                    'bChildTable' => [
+                        // Records 13 and 66 are expanded
+                        13,
+                        66,
+                    ],
+                ],
+            ],
+        ];
+        $GLOBALS['BE_USER'] = new \stdClass();
+        $GLOBALS['BE_USER']->uc = [
+            'inlineView' => serialize($inlineState),
+        ];
+        $expected = $input;
+        $expected['inlineExpandCollapseStateArray'] = $inlineState['aParentTable'][5];
+        $this->assertSame($expected, $this->subject->addData($input));
+    }
 }