/** * @test */ public function configuredDataProviderClassIsInstantiatedWithTcaConfigurationInConstructor() { $dataProviderMockClassName = TreeDataProviderWithConfigurationFixture::class; $tcaConfiguration = ['treeConfig' => ['dataProvider' => $dataProviderMockClassName], 'internal_type' => 'foo']; $this->setExpectedException(\RuntimeException::class, $this->anything(), 1438875249); $this->subject->getDataProvider($tcaConfiguration, 'foo', 'bar', array('uid' => 1)); }
/** * @test */ public function configuredDataProviderClassIsInstantiatedWithTcaConfigurationInConstructor() { $dataProviderMockClassName = $this->getUniqueId('tx_coretest_tree_data_provider'); $tcaConfiguration = array('treeConfig' => array('dataProvider' => $dataProviderMockClassName), 'internal_type' => 'foo'); $classCode = 'class ' . $dataProviderMockClassName . ' { function __construct($configuration) { if (!is_array($configuration)) throw new Exception(\'Failed asserting that the constructor arguments are an array\'); if ($configuration !== ' . var_export($tcaConfiguration, TRUE) . ') throw new Exception(\'Failed asserting that the constructor arguments are correctly passed\'); } }'; eval($classCode); $dataProvider = $this->subject->getDataProvider($tcaConfiguration, 'foo', 'bar', array('uid' => 1)); $this->assertInstanceOf($dataProviderMockClassName, $dataProvider); }
/** * Renders the Ext JS tree. * * @param array $result The current result array. * @param array $fieldConfig The configuration of the current field. * @param string $fieldName The name of the current field. * @param array $staticItems The static items from the field config. * @return array The tree data configuration */ protected function renderTree(array $result, array $fieldConfig, $fieldName, array $staticItems) { $allowedUids = []; foreach ($fieldConfig['config']['items'] as $item) { if ((int) $item[1] > 0) { $allowedUids[] = $item[1]; } } $treeDataProvider = TreeDataProviderFactory::getDataProvider($fieldConfig['config'], $result['tableName'], $fieldName, $result['databaseRow']); $treeDataProvider->setSelectedList(is_array($result['databaseRow'][$fieldName]) ? implode(',', $result['databaseRow'][$fieldName]) : $result['databaseRow'][$fieldName]); $treeDataProvider->setItemWhiteList($allowedUids); $treeDataProvider->initializeTreeData(); /** @var ExtJsArrayTreeRenderer $treeRenderer */ $treeRenderer = GeneralUtility::makeInstance(ExtJsArrayTreeRenderer::class); /** @var TableConfigurationTree $tree */ $tree = GeneralUtility::makeInstance(TableConfigurationTree::class); $tree->setDataProvider($treeDataProvider); $tree->setNodeRenderer($treeRenderer); $treeItems = $this->prepareAdditionalItems($staticItems, $result['databaseRow'][$fieldName]); $treeItems[] = $tree->render(); $treeConfig = ['items' => $treeItems, 'selectedNodes' => $this->prepareSelectedNodes($fieldConfig['config']['items'], $result['databaseRow'][$fieldName])]; return $treeConfig; }
/** * Render tree widget * * @return array As defined in initializeResultArray() of AbstractNode */ public function render() { $table = $this->data['tableName']; $field = $this->data['fieldName']; $row = $this->data['databaseRow']; $parameterArray = $this->data['parameterArray']; // Field configuration from TCA: $config = $parameterArray['fieldConf']['config']; $possibleSelectboxItems = $config['items']; $selectedNodes = $parameterArray['itemFormElValue']; $selectedNodesForApi = array(); foreach ($selectedNodes as $selectedNode) { // @todo: this is ugly - the "old" pipe based value|label syntax is re-created here at the moment foreach ($possibleSelectboxItems as $possibleSelectboxItem) { if ((string) $possibleSelectboxItem[1] === (string) $selectedNode) { $selectedNodesForApi[] = $selectedNode . '|' . rawurlencode($possibleSelectboxItem[0]); } } } $allowedUids = array(); foreach ($possibleSelectboxItems as $item) { if ((int) $item[1] > 0) { $allowedUids[] = $item[1]; } } $treeDataProvider = TreeDataProviderFactory::getDataProvider($config, $table, $field, $row); $treeDataProvider->setSelectedList(implode(',', $selectedNodes)); $treeDataProvider->setItemWhiteList($allowedUids); $treeDataProvider->initializeTreeData(); $treeRenderer = GeneralUtility::makeInstance(ExtJsArrayTreeRenderer::class); $tree = GeneralUtility::makeInstance(TableConfigurationTree::class); $tree->setDataProvider($treeDataProvider); $tree->setNodeRenderer($treeRenderer); $treeData = $tree->render(); $itemArray = array(); /** * @todo: Small bug here: In the past, this was the "not processed list" of default items, but now it is * @todo: a full list of elements. This needs to be fixed later, so "additional" default items are shown again. if (is_array($config['items'])) { foreach ($config['items'] as $additionalItem) { if ($additionalItem[1] !== '--div--') { $item = new \stdClass(); $item->uid = $additionalItem[1]; $item->text = $this->getLanguageService()->sL($additionalItem[0]); $item->selectable = TRUE; $item->leaf = TRUE; $item->checked = in_array($additionalItem[1], $selectedNodes); if (file_exists(PATH_typo3 . $additionalItem[3])) { $item->icon = $additionalItem[3]; } elseif (trim($additionalItem[3]) !== '') { $item->iconCls = IconUtility::getSpriteIconClasses($additionalItem[3]); } $itemArray[] = $item; } } } */ $itemArray[] = $treeData; $id = md5($parameterArray['itemFormElName']); if (isset($config['size']) && (int) $config['size'] > 0) { $height = (int) $config['size'] * 20; } else { $height = 280; } $autoSizeMax = null; if (isset($config['autoSizeMax']) && (int) $config['autoSizeMax'] > 0) { $autoSizeMax = (int) $config['autoSizeMax'] * 20; } $header = false; $expanded = false; $width = 280; $appearance = $config['treeConfig']['appearance']; if (is_array($appearance)) { $header = (bool) $appearance['showHeader']; $expanded = (bool) $appearance['expandAll']; if (isset($appearance['width'])) { $width = (int) $appearance['width']; } } $onChange = ''; if ($parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged']) { $onChange = $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged']; } // Create a JavaScript code line which will ask the user to save/update the form due to changing the element. // This is used for eg. "type" fields and others configured with "requestUpdate" if (!empty($GLOBALS['TCA'][$table]['ctrl']['type']) && $field === $GLOBALS['TCA'][$table]['ctrl']['type'] || !empty($GLOBALS['TCA'][$table]['ctrl']['requestUpdate']) && GeneralUtility::inList(str_replace(' ', '', $GLOBALS['TCA'][$table]['ctrl']['requestUpdate']), $field)) { if ($this->getBackendUserAuthentication()->jsConfirmation(JsConfirmation::TYPE_CHANGE)) { $onChange = 'top.TYPO3.Modal.confirm(TBE_EDITOR.labels.refreshRequired.title, TBE_EDITOR.labels.refreshRequired.content).on("button.clicked", function(e) { if (e.target.name == "ok" && TBE_EDITOR.checkSubmit(-1)) { TBE_EDITOR.submitForm() } top.TYPO3.Modal.dismiss(); });'; } else { $onChange .= 'if (TBE_EDITOR.checkSubmit(-1)){ TBE_EDITOR.submitForm() };'; } } $html = ' <div class="typo3-tceforms-tree"> <input class="treeRecord" type="hidden" ' . $this->getValidationDataAsDataAttribute($config) . ' data-formengine-input-name="' . htmlspecialchars($parameterArray['itemFormElName']) . '"' . ' data-relatedfieldname="' . htmlspecialchars($parameterArray['itemFormElName']) . '"' . ' name="' . htmlspecialchars($parameterArray['itemFormElName']) . '" id="treeinput' . $id . '" value="' . htmlspecialchars(implode(',', $selectedNodesForApi)) . '" /> </div> <div id="tree_' . $id . '"> </div>'; // Wizards: if (empty($config['readOnly'])) { $html = $this->renderWizards(array($html), $config['wizards'], $table, $row, $field, $parameterArray, $parameterArray['itemFormElName'], BackendUtility::getSpecConfParts($parameterArray['fieldConf']['defaultExtras'])); } $resultArray = $this->initializeResultArray(); $resultArray['extJSCODE'] .= LF . 'Ext.onReady(function() { TYPO3.Components.Tree.StandardTreeItemData["' . $id . '"] = ' . json_encode($itemArray) . '; var tree' . $id . ' = new TYPO3.Components.Tree.StandardTree({ id: "' . $id . '", showHeader: ' . (int) $header . ', onChange: ' . GeneralUtility::quoteJSvalue($onChange) . ', countSelectedNodes: ' . count($selectedNodesForApi) . ', width: ' . (int) $width . ', listeners: { click: function(node, event) { if (typeof(node.attributes.checked) == "boolean") { node.attributes.checked = ! node.attributes.checked; node.getUI().toggleCheck(node.attributes.checked); } }, dblclick: function(node, event) { if (typeof(node.attributes.checked) == "boolean") { node.attributes.checked = ! node.attributes.checked; node.getUI().toggleCheck(node.attributes.checked); } }, checkchange: TYPO3.Components.Tree.TcaCheckChangeHandler, collapsenode: function(node) { if (node.id !== "root") { top.TYPO3.Storage.Persistent.removeFromList("tcaTrees." + this.ucId, node.attributes.uid); } }, expandnode: function(node) { if (node.id !== "root") { top.TYPO3.Storage.Persistent.addToList("tcaTrees." + this.ucId, node.attributes.uid); } }, beforerender: function(treeCmp) { // Check if that tree element is already rendered. It is appended on the first tceforms_inline call. if (Ext.fly(treeCmp.getId())) { return false; } }' . ($expanded ? ', afterrender: function(treeCmp) { treeCmp.expandAll(); }' : '') . ' }, tcaMaxItems: ' . ($config['maxitems'] ? (int) $config['maxitems'] : 99999) . ', tcaSelectRecursiveAllowed: ' . ($appearance['allowRecursiveMode'] ? 'true' : 'false') . ', tcaSelectRecursive: false, tcaExclusiveKeys: "' . ($config['exclusiveKeys'] ? $config['exclusiveKeys'] : '') . '", ucId: "' . md5($table . '|' . $field) . '", selModel: TYPO3.Components.Tree.EmptySelectionModel, disabled: ' . ($config['readOnly'] ? 'true' : 'false') . ' });' . LF . ($autoSizeMax ? 'tree' . $id . '.bodyStyle = "max-height: ' . $autoSizeMax . 'px;min-height: ' . $height . 'px;";' : 'tree' . $id . '.height = ' . $height . ';') . LF . 'window.setTimeout(function() { tree' . $id . '.render("tree_' . $id . '"); }, 200); });'; $resultArray['html'] = $html; return $resultArray; }
/** * renders the tree as replacement for the selector * * @param string $table The table name of the record * @param string $field The field name which this element is supposed to edit * @param array $row The record data array where the value(s) for the field can be found * @param array $PA An array with additional configuration options. * @param array $config (Redundant) content of $PA['fieldConf']['config'] (for convenience) * @param array $possibleSelectboxItems Items available for selection * @param string $noMatchLabel Label for no-matching-value * @return string The HTML code for the TCEform field */ public function renderField($table, $field, $row, &$PA, $config, $possibleSelectboxItems, $noMatchLabel) { $valueArray = array(); $selectedNodes = array(); if (!empty($PA['itemFormElValue'])) { $valueArray = explode(',', $PA['itemFormElValue']); } if (count($valueArray)) { foreach ($valueArray as $selectedValue) { $temp = explode('|', $selectedValue); $selectedNodes[] = $temp[0]; } } $allowedUids = array(); foreach ($possibleSelectboxItems as $item) { if ((int) $item[1] > 0) { $allowedUids[] = $item[1]; } } $treeDataProvider = \TYPO3\CMS\Core\Tree\TableConfiguration\TreeDataProviderFactory::getDataProvider($config, $table, $field, $row); $treeDataProvider->setSelectedList(implode(',', $selectedNodes)); $treeDataProvider->setItemWhiteList($allowedUids); $treeDataProvider->initializeTreeData(); $treeRenderer = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Tree\\TableConfiguration\\ExtJsArrayTreeRenderer'); $tree = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Tree\\TableConfiguration\\TableConfigurationTree'); $tree->setDataProvider($treeDataProvider); $tree->setNodeRenderer($treeRenderer); $treeData = $tree->render(); $itemArray = array(); if (is_array($PA['fieldConf']['config']['items'])) { foreach ($PA['fieldConf']['config']['items'] as $additionalItem) { if ($additionalItem[1] !== '--div--') { $item = new \stdClass(); $item->uid = $additionalItem[1]; $item->text = $GLOBALS['LANG']->sL($additionalItem[0]); $item->selectable = TRUE; $item->leaf = TRUE; $item->checked = in_array($additionalItem[1], $selectedNodes); if (file_exists(PATH_typo3 . $additionalItem[3])) { $item->icon = $additionalItem[3]; } elseif (strlen(trim($additionalItem[3]))) { $item->iconCls = \TYPO3\CMS\Backend\Utility\IconUtility::getSpriteIconClasses($additionalItem[3]); } $itemArray[] = $item; } } } $itemArray[] = $treeData; $treeData = json_encode($itemArray); $id = md5($PA['itemFormElName']); if (isset($PA['fieldConf']['config']['size']) && (int) $PA['fieldConf']['config']['size'] > 0) { $height = (int) $PA['fieldConf']['config']['size'] * 20; } else { $height = 280; } if (isset($PA['fieldConf']['config']['autoSizeMax']) && (int) $PA['fieldConf']['config']['autoSizeMax'] > 0) { $autoSizeMax = (int) $PA['fieldConf']['config']['autoSizeMax'] * 20; } $header = FALSE; $expanded = FALSE; $width = 280; $appearance = $PA['fieldConf']['config']['treeConfig']['appearance']; if (is_array($appearance)) { $header = $appearance['showHeader'] ? TRUE : FALSE; $expanded = $appearance['expandAll'] === TRUE; if (isset($appearance['width'])) { $width = (int) $appearance['width']; } } $onChange = ''; if ($PA['fieldChangeFunc']['TBE_EDITOR_fieldChanged']) { $onChange = $PA['fieldChangeFunc']['TBE_EDITOR_fieldChanged']; } // Create a JavaScript code line which will ask the user to save/update the form due to changing the element. // This is used for eg. "type" fields and others configured with "requestUpdate" if (!empty($GLOBALS['TCA'][$table]['ctrl']['type']) && $field === $GLOBALS['TCA'][$table]['ctrl']['type'] || !empty($GLOBALS['TCA'][$table]['ctrl']['requestUpdate']) && GeneralUtility::inList(str_replace(' ', '', $GLOBALS['TCA'][$table]['ctrl']['requestUpdate']), $field)) { if ($GLOBALS['BE_USER']->jsConfirmation(1)) { $onChange .= 'if (confirm(TBE_EDITOR.labels.onChangeAlert) && ' . 'TBE_EDITOR.checkSubmit(-1)){ TBE_EDITOR.submitForm() };'; } else { $onChange .= 'if (TBE_EDITOR.checkSubmit(-1)){ TBE_EDITOR.submitForm() };'; } } /** @var $pageRenderer \TYPO3\CMS\Core\Page\PageRenderer */ $pageRenderer = $GLOBALS['SOBE']->doc->getPageRenderer(); $pageRenderer->loadExtJs(); $pageRenderer->addJsFile('sysext/backend/Resources/Public/JavaScript/tree.js'); $pageRenderer->addInlineLanguageLabelFile(\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('lang') . 'locallang_csh_corebe.xlf', 'tcatree'); $pageRenderer->addExtOnReadyCode(' TYPO3.Components.Tree.StandardTreeItemData["' . $id . '"] = ' . $treeData . '; var tree' . $id . ' = new TYPO3.Components.Tree.StandardTree({ id: "' . $id . '", showHeader: ' . (int) $header . ', onChange: "' . $onChange . '", countSelectedNodes: ' . count($selectedNodes) . ', width: ' . $width . ', listeners: { click: function(node, event) { if (typeof(node.attributes.checked) == "boolean") { node.attributes.checked = ! node.attributes.checked; node.getUI().toggleCheck(node.attributes.checked); } }, dblclick: function(node, event) { if (typeof(node.attributes.checked) == "boolean") { node.attributes.checked = ! node.attributes.checked; node.getUI().toggleCheck(node.attributes.checked); } }, checkchange: TYPO3.Components.Tree.TcaCheckChangeHandler, collapsenode: function(node) { if (node.id !== "root") { top.TYPO3.BackendUserSettings.ExtDirect.removeFromList("tcaTrees." + this.ucId, node.attributes.uid); } }, expandnode: function(node) { if (node.id !== "root") { top.TYPO3.BackendUserSettings.ExtDirect.addToList("tcaTrees." + this.ucId, node.attributes.uid); } }, beforerender: function(treeCmp) { // Check if that tree element is already rendered. It is appended on the first tceforms_inline call. if (Ext.fly(treeCmp.getId())) { return false; } }' . ($expanded ? ', afterrender: function(treeCmp) { treeCmp.expandAll(); }' : '') . ' }, tcaMaxItems: ' . ($PA['fieldConf']['config']['maxitems'] ? (int) $PA['fieldConf']['config']['maxitems'] : 99999) . ', tcaSelectRecursiveAllowed: ' . ($appearance['allowRecursiveMode'] ? 'true' : 'false') . ', tcaSelectRecursive: false, tcaExclusiveKeys: "' . ($PA['fieldConf']['config']['exclusiveKeys'] ? $PA['fieldConf']['config']['exclusiveKeys'] : '') . '", ucId: "' . md5($table . '|' . $field) . '", selModel: TYPO3.Components.Tree.EmptySelectionModel, disabled: ' . ($PA['fieldConf']['config']['readOnly'] ? 'true' : 'false') . ' });' . LF . ($autoSizeMax ? 'tree' . $id . '.bodyStyle = "max-height: ' . $autoSizeMax . 'px;min-height: ' . $height . 'px;";' : 'tree' . $id . '.height = ' . $height . ';') . LF . '(function() { tree' . $id . '.render("tree_' . $id . '"); }).defer(20); '); $formField = ' <div class="typo3-tceforms-tree"> <input class="treeRecord" type="hidden" name="' . htmlspecialchars($PA['itemFormElName']) . '" id="treeinput' . $id . '" value="' . htmlspecialchars($PA['itemFormElValue']) . '" /> </div> <div id="tree_' . $id . '"> </div>'; return $formField; }
/** * Sanitize config options and resolve select items if requested. * * @param array $result * @return array * @throws \UnexpectedValueException */ public function addData(array $result) { $table = $result['tableName']; foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) { if (empty($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'select') { continue; } // Make sure we are only processing supported renderTypes if (!$this->isTargetRenderType($fieldConfig)) { continue; } $fieldConfig['config']['maxitems'] = $this->sanitizeMaxItems($fieldConfig['config']['maxitems']); // A couple of tree specific config parameters can be overwritten via page TS. // Pick those that influence the data fetching and write them into the config // given to the tree data provider. This is additionally used in SelectTreeElement, so always do that. if (isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['config.']['treeConfig.'])) { $pageTsConfig = $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['config.']['treeConfig.']; // If rootUid is set in pageTsConfig, use it if (isset($pageTsConfig['rootUid'])) { $fieldConfig['config']['treeConfig']['rootUid'] = (int) $pageTsConfig['rootUid']; } if (isset($pageTsConfig['appearance.']['expandAll'])) { $fieldConfig['config']['treeConfig']['appearance']['expandAll'] = (bool) $pageTsConfig['appearance.']['expandAll']; } if (isset($pageTsConfig['appearance.']['maxLevels'])) { $fieldConfig['config']['treeConfig']['appearance']['maxLevels'] = (int) $pageTsConfig['appearance.']['maxLevels']; } if (isset($pageTsConfig['appearance.']['nonSelectableLevels'])) { $fieldConfig['config']['treeConfig']['appearance']['nonSelectableLevels'] = $pageTsConfig['appearance.']['nonSelectableLevels']; } } if ($result['selectTreeCompileItems']) { // Prepare the list of currently selected nodes using RelationHandler $result['databaseRow'][$fieldName] = $this->processDatabaseFieldValue($result['databaseRow'], $fieldName); $result['databaseRow'][$fieldName] = $this->processSelectFieldValue($result, $fieldName, []); $finalItems = []; // Prepare the list of "static" items if there are any. // "static" and "dynamic" is separated since the tree code only copes with "real" existing foreign nodes, // so this "static" stuff allows defining tree items that don't really exist in the tree. $itemsFromTca = $this->sanitizeItemArray($fieldConfig['config']['items'], $table, $fieldName); // List of additional items defined by page ts config "addItems" $itemsFromPageTsConfig = $this->addItemsFromPageTsConfig($result, $fieldName, []); if (!empty($itemsFromTca) || !empty($itemsFromPageTsConfig)) { // First apply "keepItems" to $itemsFromTca, this will restrict the tca item list to only // those items that are defined in page ts "keepItems" if given $itemsFromTca = $this->removeItemsByKeepItemsPageTsConfig($result, $fieldName, $itemsFromTca); // Then, merge the items from page ts "addItems" into item list, since "addItems" should // add additional items even if they are not in the "keepItems" list $staticItems = array_merge($itemsFromTca, $itemsFromPageTsConfig); // Now apply page ts config "removeItems", so this is *after* addItems, so "removeItems" could // possibly remove items again that were added via "addItems" $staticItems = $this->removeItemsByRemoveItemsPageTsConfig($result, $fieldName, $staticItems); // Now, apply user and access right restrictions to this item list $staticItems = $this->removeItemsByUserLanguageFieldRestriction($result, $fieldName, $staticItems); $staticItems = $this->removeItemsByUserAuthMode($result, $fieldName, $staticItems); $staticItems = $this->removeItemsByDoktypeUserRestriction($result, $fieldName, $staticItems); // Call itemsProcFunc if given. Note this function does *not* see the "dynamic" list of items if (!empty($fieldConfig['config']['itemsProcFunc'])) { $staticItems = $this->resolveItemProcessorFunction($result, $fieldName, $staticItems); // itemsProcFunc must not be used anymore unset($fieldConfig['config']['itemsProcFunc']); } // And translate any labels from the static list $staticItems = $this->translateLabels($result, $staticItems, $table, $fieldName); // Now compile the target items using the same array structure as the "dynamic" list below foreach ($staticItems as $item) { if ($item[1] === '--div--') { // Skip divs that may occur here for whatever reason continue; } $finalItems[] = ['identifier' => $item[1], 'name' => $item[0], 'icon' => $item[2] ?? '', 'iconOverlay' => '', 'depth' => 0, 'hasChildren' => false, 'selectable' => true, 'checked' => in_array($item[1], $result['databaseRow'][$fieldName])]; } } // Fetch the list of all possible "related" items (yuk!) and apply a similar processing as with the "static" list $dynamicItems = $this->addItemsFromForeignTable($result, $fieldName, []); $dynamicItems = $this->removeItemsByKeepItemsPageTsConfig($result, $fieldName, $dynamicItems); $dynamicItems = $this->removeItemsByRemoveItemsPageTsConfig($result, $fieldName, $dynamicItems); $dynamicItems = $this->removeItemsByUserLanguageFieldRestriction($result, $fieldName, $dynamicItems); $dynamicItems = $this->removeItemsByUserAuthMode($result, $fieldName, $dynamicItems); $dynamicItems = $this->removeItemsByDoktypeUserRestriction($result, $fieldName, $dynamicItems); // Funnily, the only data needed for the tree code are the uids of the possible records (yuk!) - get them $uidListOfAllDynamicItems = []; foreach ($dynamicItems as $item) { if ((int) $item[1] > 0) { $uidListOfAllDynamicItems[] = (int) $item[1]; } } // Now kick in this tree stuff $treeDataProvider = TreeDataProviderFactory::getDataProvider($fieldConfig['config'], $table, $fieldName, $result['databaseRow']); $treeDataProvider->setSelectedList(implode(',', $result['databaseRow'][$fieldName])); // Basically the tree foo fetches all tree nodes again (aaargs), then verifies if // a given rows uid is within this "list of allowed uids". It then creates an object // tree representing the nested tree, just to collapse all that to a flat array again. Yay ... $treeDataProvider->setItemWhiteList($uidListOfAllDynamicItems); $treeDataProvider->initializeTreeData(); $treeRenderer = GeneralUtility::makeInstance(ExtJsArrayTreeRenderer::class); $tree = GeneralUtility::makeInstance(TableConfigurationTree::class); $tree->setDataProvider($treeDataProvider); $tree->setNodeRenderer($treeRenderer); // Merge tree nodes after calculated nodes from static items $fieldConfig['config']['items'] = array_merge($finalItems, $tree->render()); } $result['processedTca']['columns'][$fieldName] = $fieldConfig; } return $result; }