From cfd73ed36d9205eddc27c542f4fec3ac2ee0197d Mon Sep 17 00:00:00 2001 From: Christian Kuhn <lolli@schwarzbu.ch> Date: Wed, 30 Nov 2016 17:29:10 +0100 Subject: [PATCH] [TASK] TCA tree: Simplify json result The patch changes the ajax result that delivers TCA tree items to the SVG tree from a nested list of items to a sorted flat list having a 'depth' argument to indicate the nesting level. This "flat" list is the native mode of the d3 tree, with this change the JS side can be streamlined quite a bit. Along the way, the item providing on PHP side is streamlined, documented much better and easier to understand now within the data provider of FormEngine. The main tree data backend is still a huge, convoluted, slow and insane mess that will eventually fully substituted with a much straighter and quicker approach later. Changes in this area are kept to a minimum for now. Change-Id: Ib64b7277f671b632be3977218e5465b534618d63 Resolves: #78905 Related: #76108 Releases: master Reviewed-on: https://review.typo3.org/50813 Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl> Tested-by: Wouter Wolters <typo3@wouterwolters.nl> Tested-by: TYPO3com <no-reply@typo3.com> Reviewed-by: Thomas Maroschik <tmaroschik@dfau.de> Tested-by: Thomas Maroschik <tmaroschik@dfau.de> Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch> Tested-by: Christian Kuhn <lolli@schwarzbu.ch> --- .../Controller/SelectTreeController.php | 6 +- .../FormDataProvider/AbstractItemProvider.php | 3 +- .../FormDataProvider/TcaSelectTreeItems.php | 208 ++++++++---------- .../Tree/Renderer/ExtJsJsonTreeRenderer.php | 53 +++-- .../FormEngine/Element/SelectTree.js | 21 +- .../JavaScript/FormEngine/Element/SvgTree.js | 152 +++++-------- .../FormEngine/Element/TreeToolbar.js | 21 +- .../TcaSelectTreeItemsTest.php | 7 +- .../ExtJsArrayTreeRenderer.php | 24 +- .../TreeDataProviderFactory.php | 9 +- 10 files changed, 221 insertions(+), 283 deletions(-) diff --git a/typo3/sysext/backend/Classes/Controller/SelectTreeController.php b/typo3/sysext/backend/Classes/Controller/SelectTreeController.php index a0f6423a6b2b..eee0d931519f 100644 --- a/typo3/sysext/backend/Classes/Controller/SelectTreeController.php +++ b/typo3/sysext/backend/Classes/Controller/SelectTreeController.php @@ -105,12 +105,12 @@ class SelectTreeController if ($formData['processedTca']['columns'][$fieldName]['config']['type'] === 'flex') { $treeData = $formData['processedTca']['columns'][$fieldName]['config']['ds'] - ['sheets'][$flexFormPath[3]]['ROOT']['el'][$flexFormPath[5]]['config']['treeData']; + ['sheets'][$flexFormPath[3]]['ROOT']['el'][$flexFormPath[5]]['config']['items']; } else { - $treeData = $formData['processedTca']['columns'][$fieldName]['config']['treeData']; + $treeData = $formData['processedTca']['columns'][$fieldName]['config']['items']; } - $json = json_encode($treeData['items']); + $json = json_encode($treeData); $response->getBody()->write($json); return $response; } diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/AbstractItemProvider.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/AbstractItemProvider.php index 58f001feb7c2..f388e6f18401 100644 --- a/typo3/sysext/backend/Classes/Form/FormDataProvider/AbstractItemProvider.php +++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/AbstractItemProvider.php @@ -1358,8 +1358,9 @@ abstract class AbstractItemProvider * @param array $itemArray All item records for the select field * @param array $dynamicItemArray Item records from dynamic sources * @return array + * @todo: Check method usage, it's probably bogus in select context and was removed from select tree already. */ - public function getStaticValues($itemArray, $dynamicItemArray) + protected function getStaticValues($itemArray, $dynamicItemArray) { $staticValues = []; foreach ($itemArray as $key => $item) { diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaSelectTreeItems.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaSelectTreeItems.php index 92fed04939be..ebd1b6321e61 100644 --- a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaSelectTreeItems.php +++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaSelectTreeItems.php @@ -21,12 +21,20 @@ use TYPO3\CMS\Core\Tree\TableConfiguration\TreeDataProviderFactory; use TYPO3\CMS\Core\Utility\GeneralUtility; /** - * Resolve select items, set processed item list in processedTca, sanitize and resolve database field + * Data provider for type=select + renderType=selectTree fields. + * + * Used in combination with SelectTreeElement to create the base HTML for trees, + * does a little bit of sanitation and preparation then. + * + * Used in combination with SelectTreeController to fetch the final tree list, this is + * triggered if $result['selectTreeCompileItems'] is set to true. This way the tree item + * calculation is only triggered if needed in this ajax context. Writes the prepared + * item array to ['config']['items'] in this case. */ class TcaSelectTreeItems extends AbstractItemProvider implements FormDataProviderInterface { /** - * Resolve select items + * Sanitize config options and resolve select items if requested. * * @param array $result * @return array @@ -50,7 +58,7 @@ class TcaSelectTreeItems extends AbstractItemProvider implements FormDataProvide // 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 + // 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 @@ -69,42 +77,93 @@ class TcaSelectTreeItems extends AbstractItemProvider implements FormDataProvide } if ($result['selectTreeCompileItems']) { - $fieldConfig['config']['items'] = $this->sanitizeItemArray($fieldConfig['config']['items'], $table, $fieldName); - - $pageTsConfigAddItems = $this->addItemsFromPageTsConfig($result, $fieldName, []); - $fieldConfig['config']['items'] = $this->addItemsFromSpecial($result, $fieldName, $fieldConfig['config']['items']); - $fieldConfig['config']['items'] = $this->addItemsFromFolder($result, $fieldName, $fieldConfig['config']['items']); - $staticItems = $fieldConfig['config']['items'] + $pageTsConfigAddItems; - - $fieldConfig['config']['items'] = $this->addItemsFromForeignTable($result, $fieldName, $fieldConfig['config']['items']); - $dynamicItems = array_diff_key($fieldConfig['config']['items'], $staticItems); - - $fieldConfig['config']['items'] = $this->removeItemsByKeepItemsPageTsConfig($result, $fieldName, $fieldConfig['config']['items']); - $fieldConfig['config']['items'] = $pageTsConfigAddItems + $fieldConfig['config']['items']; - $fieldConfig['config']['items'] = $this->removeItemsByRemoveItemsPageTsConfig($result, $fieldName, $fieldConfig['config']['items']); - - $fieldConfig['config']['items'] = $this->removeItemsByUserLanguageFieldRestriction($result, $fieldName, $fieldConfig['config']['items']); - $fieldConfig['config']['items'] = $this->removeItemsByUserAuthMode($result, $fieldName, $fieldConfig['config']['items']); - $fieldConfig['config']['items'] = $this->removeItemsByDoktypeUserRestriction($result, $fieldName, $fieldConfig['config']['items']); - - // Resolve "itemsProcFunc" - if (!empty($fieldConfig['config']['itemsProcFunc'])) { - $fieldConfig['config']['items'] = $this->resolveItemProcessorFunction($result, $fieldName, $fieldConfig['config']['items']); - // itemsProcFunc must not be used anymore - unset($fieldConfig['config']['itemsProcFunc']); - } - - // Translate labels - $fieldConfig['config']['items'] = $this->translateLabels($result, $fieldConfig['config']['items'], $table, $fieldName); - - $staticValues = $this->getStaticValues($fieldConfig['config']['items'], $dynamicItems); + // Prepare the list of currently selected nodes using RelationHandler $result['databaseRow'][$fieldName] = $this->processDatabaseFieldValue($result['databaseRow'], $fieldName); - $result['databaseRow'][$fieldName] = $this->processSelectFieldValue($result, $fieldName, $staticValues); - - // Keys may contain table names, so a numeric array is created - $fieldConfig['config']['items'] = array_values($fieldConfig['config']['items']); + $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]), + ]; + } + } - $fieldConfig['config']['treeData'] = $this->renderTree($result, $fieldConfig, $fieldName, $staticItems); + // 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; @@ -113,81 +172,6 @@ class TcaSelectTreeItems extends AbstractItemProvider implements FormDataProvide return $result; } - /** - * 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, - ]; - - return $treeConfig; - } - - /** - * Prepare the additional items that get prepended to the tree as leaves - * - * @param array $itemArray - * @param array $selectedNodes - * @return array - */ - protected function prepareAdditionalItems(array $itemArray, array $selectedNodes) - { - $additionalItems = []; - - foreach ($itemArray as $item) { - if ($item[1] === '--div--') { - continue; - } - - $additionalItems[] = [ - 'uid' => $item[1], - 'text' => $item[0], - 'selectable' => true, - 'leaf' => true, - 'checked' => in_array($item[1], $selectedNodes), - 'icon' => $item[2] - ]; - } - - return $additionalItems; - } - /** * Determines whether the current field is a valid target for this DataProvider * diff --git a/typo3/sysext/backend/Classes/Tree/Renderer/ExtJsJsonTreeRenderer.php b/typo3/sysext/backend/Classes/Tree/Renderer/ExtJsJsonTreeRenderer.php index 4ff6365a8e3d..12bd8622a239 100644 --- a/typo3/sysext/backend/Classes/Tree/Renderer/ExtJsJsonTreeRenderer.php +++ b/typo3/sysext/backend/Classes/Tree/Renderer/ExtJsJsonTreeRenderer.php @@ -13,7 +13,9 @@ namespace TYPO3\CMS\Backend\Tree\Renderer; * * The TYPO3 project - inspiring people to share! */ +use TYPO3\CMS\Backend\Tree\TreeNodeCollection; use TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider; +use TYPO3\CMS\Core\Tree\TableConfiguration\DatabaseTreeNode; /** * Renderer for unordered lists @@ -36,11 +38,14 @@ class ExtJsJsonTreeRenderer extends \TYPO3\CMS\Backend\Tree\Renderer\AbstractTre */ public function renderNode(\TYPO3\CMS\Backend\Tree\TreeRepresentationNode $node, $recursive = true) { - $nodeArray = $this->getNodeArray($node); + $nodeArray = []; + $nodeArray[] = $this->getNodeArray($node); if ($recursive && $node->hasChildNodes()) { $this->recursionLevel++; $children = $this->renderNodeCollection($node->getChildNodes()); - $nodeArray['children'] = $children; + foreach ($children as $child) { + $nodeArray[] = $child; + } $this->recursionLevel--; } return $nodeArray; @@ -49,7 +54,7 @@ class ExtJsJsonTreeRenderer extends \TYPO3\CMS\Backend\Tree\Renderer\AbstractTre /** * Get node array * - * @param \TYPO3\CMS\Backend\Tree\TreeRepresentationNode $node + * @param \TYPO3\CMS\Backend\Tree\TreeRepresentationNode|DatabaseTreeNode $node * @return array */ protected function getNodeArray(\TYPO3\CMS\Backend\Tree\TreeRepresentationNode $node) @@ -64,27 +69,29 @@ class ExtJsJsonTreeRenderer extends \TYPO3\CMS\Backend\Tree\Renderer\AbstractTre $iconMarkup = $node->getIcon(); } $nodeArray = [ - 'iconTag' => $iconMarkup, - 'text' => htmlspecialchars($node->getLabel()), - 'leaf' => !$node->hasChildNodes(), - 'id' => htmlspecialchars($node->getId()), - 'uid' => htmlspecialchars($node->getId()), - - //svgtree - 'icon' => $iconMarkup, - 'overlayIcon' => $overlayIconMarkup, 'identifier' => htmlspecialchars($node->getId()), - //no need for htmlspecialhars here as d3 is using 'textContent' property of the HTML DOM node + // No need for htmlspecialchars() here as d3 is using 'textContent' property of the HTML DOM node 'name' => $node->getLabel(), + 'icon' => $iconMarkup, + 'overlayIcon' => $overlayIconMarkup, + 'depth' => $this->recursionLevel, + 'hasChildren' => (bool)$node->hasChildNodes(), + 'selectable' => true, ]; - + if ($node instanceof DatabaseTreeNode) { + $nodeArray['checked'] = (bool)$node->getSelected(); + if (!$node->getSelectable()) { + $nodeArray['checked'] = false; + $nodeArray['selectable'] = false; + } + } return $nodeArray; } /** * Renders a node collection recursive or just a single instance * - * @param \TYPO3\CMS\Backend\Tree\TreeNodeCollection $node + * @param \TYPO3\CMS\Backend\Tree\AbstractTree $tree * @param bool $recursive * @return string */ @@ -98,14 +105,24 @@ class ExtJsJsonTreeRenderer extends \TYPO3\CMS\Backend\Tree\Renderer\AbstractTre /** * Renders an tree recursive or just a single instance * - * @param \TYPO3\CMS\Backend\Tree\AbstractTree $node + * @param TreeNodeCollection $collection * @param bool $recursive * @return array */ - public function renderNodeCollection(\TYPO3\CMS\Backend\Tree\TreeNodeCollection $collection, $recursive = true) + public function renderNodeCollection(TreeNodeCollection $collection, $recursive = true) { + $treeItems = []; foreach ($collection as $node) { - $treeItems[] = $this->renderNode($node, $recursive); + $allNodes = $this->renderNode($node, $recursive); + if ($allNodes[0]) { + $treeItems[] = $allNodes[0]; + } + $nodeCount = count($allNodes); + if ($nodeCount > 1) { + for ($i = 1; $i < $nodeCount; $i++) { + $treeItems[] = $allNodes[$i]; + } + } } return $treeItems; } diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/SelectTree.js b/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/SelectTree.js index a20c7221faf1..e908dbc2f31d 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/SelectTree.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/SelectTree.js @@ -59,7 +59,7 @@ define(['d3', 'TYPO3/CMS/Backend/FormEngine/Element/SvgTree'], function (d3, Svg nodeSelection .selectAll('.tree-check use') .attr('visibility', function (node) { - var checked = Boolean(node.data.checked); + var checked = Boolean(node.checked); if (d3.select(this).classed('icon-checked') && checked) { return 'visible'; } else if (d3.select(this).classed('icon-indeterminate') && node.indeterminate && !checked) { @@ -85,7 +85,7 @@ define(['d3', 'TYPO3/CMS/Backend/FormEngine/Element/SvgTree'], function (d3, Svg //this can be simplified to single "use" element with changing href on click when we drop IE11 on WIN7 support var g = nodeSelection.filter(function (node) { //do not render checkbox if node is not selectable - return me.isNodeSelectable(node) || Boolean(node.data.checked); + return me.isNodeSelectable(node) || Boolean(node.checked); }) .append('g') .attr('class', 'tree-check') @@ -124,7 +124,7 @@ define(['d3', 'TYPO3/CMS/Backend/FormEngine/Element/SvgTree'], function (d3, Svg } return node.children.some(function (child) { - if (child.data.checked || child.indeterminate) { + if (child.checked || child.indeterminate) { return true; } }); @@ -138,8 +138,9 @@ define(['d3', 'TYPO3/CMS/Backend/FormEngine/Element/SvgTree'], function (d3, Svg SelectTree.prototype.updateAncestorsIndetermineState = function (node) { var me = this; //foreach ancestor except node itself - node.ancestors().slice(1).forEach(function (n) { - n.indeterminate = (node.data.checked || node.indeterminate) ? true : me.hasCheckedOrIndeterminateChildren(n); + node.parents.forEach(function (index) { + var n = me.nodes[index]; + n.indeterminate = (node.checked || node.indeterminate) ? true : me.hasCheckedOrIndeterminateChildren(n); }); }; @@ -149,12 +150,12 @@ define(['d3', 'TYPO3/CMS/Backend/FormEngine/Element/SvgTree'], function (d3, Svg * It's done once after loading data. Later indeterminate state is updated just for the subset of nodes */ SelectTree.prototype.loadDataAfter = function () { - this.rootNode.each(function (node) { + this.nodes.forEach(function (node) { node.indeterminate = false; }); - this.calculateIndeterminate(this.rootNode); + this.calculateIndeterminate(this.nodes); // Initialise "value" attribute of input field after load and revalidate form engine fields - this.saveCheckboxes(this.rootNode); + this.saveCheckboxes(this.nodes); if (typeof TYPO3.FormEngine.Validation !== 'undefined' && typeof TYPO3.FormEngine.Validation.validate === 'function') { TYPO3.FormEngine.Validation.validate(); } @@ -172,7 +173,7 @@ define(['d3', 'TYPO3/CMS/Backend/FormEngine/Element/SvgTree'], function (d3, Svg } node.eachAfter(function (n) { - if ((n.data.checked || n.indeterminate) && n.parent) { + if ((n.checked || n.indeterminate) && n.parent) { n.parent.indeterminate = true; } }) @@ -197,7 +198,7 @@ define(['d3', 'TYPO3/CMS/Backend/FormEngine/Element/SvgTree'], function (d3, Svg if (typeof this.settings.input !== 'undefined') { var selectedNodes = this.getSelectedNodes(); this.settings.input.val(selectedNodes.map(function (d) { - return d.data.identifier + return d.identifier })); } }; diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/SvgTree.js b/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/SvgTree.js index 93aab43337cd..f20b71f5bb30 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/SvgTree.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/SvgTree.js @@ -193,72 +193,32 @@ define(['jquery', 'd3'], function ($, d3) { $container.parent().append('<p class="text-danger">' + TYPO3.lang['tcatree.msg_save_first'] + '</p>'); return; } - if (Array.isArray(json)) { - if (json.length > 1) { - // If tree comes with multiple root nodes, add them to a new root - var tmp = { - checked: undefined, - children: [], - expandable: true, - expanded: true, - iconTag: null, - id: '', - identifier: 'root', - leaf: false, - name: '', - overlayIcon: '', - text: '', - uid: '' - }; - for (var i = 0; i < json.length; i++) { - var n = json[i]; - if (typeof n.identifier === 'undefined') { - n.identifier = n.uid; - } - if (typeof n.name === 'undefined') { - n.name = n.text; - } - if (typeof n.expandable === 'undefined') { - n.expandable = true; - } - if (typeof n.expanded === 'undefined') { - n.expanded = true; - } - if (typeof n.icon !== 'undefined') { - n.iconTag = n.icon; - } - tmp.children.push(n); - } - json = tmp; - } else { - json = json[0]; - } - } - var rootNode = d3.hierarchy(json); - d3.tree(rootNode); - - rootNode.each(function (n) { - n.open = (me.settings.expandUpToLevel !== null) ? n.depth < me.settings.expandUpToLevel : Boolean(n.expanded); - n.hasChildren = (n.children || n._children) ? 1 : 0; - n.parents = []; - n._isDragged = false; - if (n.parent) { - var x = n; - while (x && x.parent) { - if (x.parent.data.identifier) { - n.parents.push(x.parent.data.identifier); + + var nodes = Array.isArray(json) ? json : []; + nodes = nodes.map(function (node, index) { + node.open = (me.settings.expandUpToLevel !== null) ? node.depth < me.settings.expandUpToLevel : Boolean(node.expanded); + node.parents = []; + node._isDragged = false; + if (node.depth > 0) { + var currentDepth = node.depth; + for (var i = index; i >= 0; i--) { + var currentNode = nodes[i]; + if (currentNode.depth < currentDepth) { + node.parents.push(i); + currentDepth = currentNode.depth; } - x = x.parent; } } - if (typeof n.data.checked == 'undefined') { - n.data.checked = false; - me.settings.unselectableElements.push(n.data.identifier); + if (typeof node.checked == 'undefined') { + node.checked = false; + me.settings.unselectableElements.push(node.identifier); } //dispatch event - me.dispatch.call('prepareLoadedNode', me, n); + me.dispatch.call('prepareLoadedNode', me, node); + return node; }); - me.rootNode = rootNode; + + me.nodes = nodes; me.dispatch.call('loadDataAfter', me); me.prepareDataForVisibleNodes(); me.update(); @@ -274,17 +234,16 @@ define(['jquery', 'd3'], function ($, d3) { var me = this; var blacklist = {}; - this.rootNode.eachBefore(function (node) { + this.nodes.map(function (node, index) { if (!node.open) { - blacklist[node.data.identifier] = true; + blacklist[index] = true; } - }); - this.data.nodes = this.rootNode.descendantsBefore().filter(function (node) { - return node.hidden != true && !node.parents.some(function (id) { - return Boolean(blacklist[id]); - }); + this.data.nodes = this.nodes.filter(function (node) { + return node.hidden != true && !node.parents.some(function (index) { + return Boolean(blacklist[index]); + }); }); var iconHashes = []; @@ -294,33 +253,33 @@ define(['jquery', 'd3'], function ($, d3) { //delete n.children; n.x = n.depth * me.settings.indentWidth; n.y = i * me.settings.nodeHeight; - if (n.parent) { + if (n.parents[0] !== undefined) { me.data.links.push({ - source: n.parent, + source: me.nodes[n.parents[0]], target: n }); } - if (!n.iconHash && me.settings.showIcons && n.data.icon) { - n.iconHash = Math.abs(me.hashCode(n.data.icon)); + if (!n.iconHash && me.settings.showIcons && n.icon) { + n.iconHash = Math.abs(me.hashCode(n.icon)); if (iconHashes.indexOf(n.iconHash) === -1) { iconHashes.push(n.iconHash); me.data.icons.push({ identifier: n.iconHash, - icon: n.data.icon + icon: n.icon }); } - delete n.data.icon; + delete n.icon; } - if (!n.iconOverlayHash && me.settings.showIcons && n.data.overlayIcon) { - n.iconOverlayHash = Math.abs(me.hashCode(n.data.overlayIcon)); + if (!n.iconOverlayHash && me.settings.showIcons && n.overlayIcon) { + n.iconOverlayHash = Math.abs(me.hashCode(n.overlayIcon)); if (iconHashes.indexOf(n.iconOverlayHash) === -1) { iconHashes.push(n.iconOverlayHash); me.data.icons.push({ identifier: n.iconOverlayHash, - icon: n.data.overlayIcon + icon: n.overlayIcon }); } - delete n.data.overlayIcon; + delete n.overlayIcon; } }); this.svg.attr('height', this.data.nodes.length * this.settings.nodeHeight); @@ -336,7 +295,7 @@ define(['jquery', 'd3'], function ($, d3) { var visibleNodes = this.data.nodes.slice(position, position + visibleRows); var nodes = this.nodesContainer.selectAll('.node').data(visibleNodes, function (d) { - return d.data.identifier; + return d.identifier; }); // delete nodes without corresponding data @@ -493,7 +452,7 @@ define(['jquery', 'd3'], function ($, d3) { * @returns {String} */ getNodeLabel: function (node) { - return node.data.name; + return node.name; }, /** @@ -503,7 +462,7 @@ define(['jquery', 'd3'], function ($, d3) { * @returns {String} */ getNodeClass: function (node) { - return 'node identifier-' + node.data.identifier; + return 'node identifier-' + node.identifier; }, /** @@ -513,7 +472,7 @@ define(['jquery', 'd3'], function ($, d3) { * @returns {String} */ getNodeTitle: function (node) { - return 'uid=' + node.data.identifier; + return 'uid=' + node.identifier; }, /** @@ -611,7 +570,7 @@ define(['jquery', 'd3'], function ($, d3) { if (!this.isNodeSelectable(node)) { return; } - var checked = node.data.checked; + var checked = node.checked; this.handleExclusiveNodeSelection(node); if (this.settings.validation && this.settings.validation.maxItems) { @@ -620,7 +579,7 @@ define(['jquery', 'd3'], function ($, d3) { return; } } - node.data.checked = !checked; + node.checked = !checked; this.dispatch.call('nodeSelectedAfter', this, node); this.update(); @@ -635,19 +594,19 @@ define(['jquery', 'd3'], function ($, d3) { handleExclusiveNodeSelection: function (node) { var exclusiveKeys = this.settings.exclusiveNodesIdentifiers.split(','), me = this; - if (this.settings.exclusiveNodesIdentifiers.length && node.data.checked === false) { - if (exclusiveKeys.indexOf('' + node.data.identifier) > -1) { + if (this.settings.exclusiveNodesIdentifiers.length && node.checked === false) { + if (exclusiveKeys.indexOf('' + node.identifier) > -1) { // this key is exclusive, so uncheck all others this.rootNode.each(function (node) { - if (node.data.checked === true) { - node.data.checked = false; + if (node.checked === true) { + node.checked = false; me.dispatch.call('nodeSelectedAfter', me, node); } }); this.exclusiveSelectedNode = node; - } else if (exclusiveKeys.indexOf('' + node.data.identifier) === -1 && this.exclusiveSelectedNode) { + } else if (exclusiveKeys.indexOf('' + node.identifier) === -1 && this.exclusiveSelectedNode) { //current node is not exclusive, but other exclusive node is already selected - this.exclusiveSelectedNode.data.checked = false; + this.exclusiveSelectedNode.checked = false; this.dispatch.call('nodeSelectedAfter', this, this.exclusiveSelectedNode); this.exclusiveSelectedNode = null; } @@ -662,7 +621,7 @@ define(['jquery', 'd3'], function ($, d3) { * @returns {Boolean} */ isNodeSelectable: function (node) { - return !this.settings.readOnlyMode && this.settings.unselectableElements.indexOf(node.data.identifier) == -1; + return !this.settings.readOnlyMode && this.settings.unselectableElements.indexOf(node.identifier) == -1; }, /** @@ -671,14 +630,9 @@ define(['jquery', 'd3'], function ($, d3) { * @returns {Node[]} */ getSelectedNodes: function () { - var selectedNodes = []; - - this.rootNode.each(function (node) { - if (node.data.checked) { - selectedNodes.push(node) - } + return this.nodes.filter(function (node) { + return node.checked; }); - return selectedNodes; }, /** @@ -743,7 +697,7 @@ define(['jquery', 'd3'], function ($, d3) { * Expand all nodes and refresh view */ expandAll: function () { - this.rootNode.each(this.showChildren.bind(this)); + this.nodes.forEach(this.showChildren.bind(this)); this.prepareDataForVisibleNodes(); this.update(); }, @@ -752,7 +706,7 @@ define(['jquery', 'd3'], function ($, d3) { * Collapse all nodes recursively and refresh view */ collapseAll: function () { - this.rootNode.each(this.hideChildren.bind(this)); + this.nodes.forEach(this.hideChildren.bind(this)); this.prepareDataForVisibleNodes(); this.update(); } diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/TreeToolbar.js b/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/TreeToolbar.js index d6ebb581f0c7..f86d1a8bead9 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/TreeToolbar.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/TreeToolbar.js @@ -146,10 +146,10 @@ define(['jquery', 'TYPO3/CMS/Backend/Icons', 'TYPO3/CMS/Backend/Tooltip', 'TYPO3 var me = this, name = $(input).val(); - this.tree.rootNode.open = false; - this.tree.rootNode.eachBefore(function (node, i) { + this.tree.nodes[0].open = false; + this.tree.nodes.forEach(function (node) { var regex = new RegExp(name, 'i'); - if (regex.test(node.data.name)) { + if (regex.test(node.name)) { me.showParents(node); node.open = true; node.hidden = false; @@ -173,8 +173,8 @@ define(['jquery', 'TYPO3/CMS/Backend/Icons', 'TYPO3/CMS/Backend/Tooltip', 'TYPO3 this._hideUncheckedState = !this._hideUncheckedState; if (this._hideUncheckedState) { - this.tree.rootNode.eachBefore(function (node, i) { - if (node.data.checked) { + this.tree.nodes.forEach(function (node) { + if (node.checked) { me.showParents(node); node.open = true; node.hidden = false; @@ -184,7 +184,7 @@ define(['jquery', 'TYPO3/CMS/Backend/Icons', 'TYPO3/CMS/Backend/Tooltip', 'TYPO3 } }); } else { - this.tree.rootNode.eachBefore(function (node, i) { + this.tree.nodes.forEach(function (node) { node.hidden = false; }); } @@ -199,14 +199,15 @@ define(['jquery', 'TYPO3/CMS/Backend/Icons', 'TYPO3/CMS/Backend/Tooltip', 'TYPO3 * @returns {Boolean} */ TreeToolbar.prototype.showParents = function (node) { - if (!node.parent) { + if (node.parents.length === 0) { return true; } - node.parent.hidden = false; + var parent = this.tree.nodes[node.parents[0]]; + parent.hidden = false; //expand parent node - node.parent.open = true; - this.showParents(node.parent); + parent.open = true; + this.showParents(parent); }; return TreeToolbar; diff --git a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaSelectTreeItemsTest.php b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaSelectTreeItemsTest.php index f3d66dd20a87..d9cb7486ce63 100644 --- a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaSelectTreeItemsTest.php +++ b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaSelectTreeItemsTest.php @@ -180,8 +180,8 @@ class TcaSelectTreeItemsTest extends UnitTestCase $expected = $input; $expected['databaseRow']['aField'] = ['1']; - $expected['processedTca']['columns']['aField']['config']['treeData'] = [ - 'items' => [['fake', 'tree', 'data']], + $expected['processedTca']['columns']['aField']['config']['items'] = [ + 'fake', 'tree', 'data', ]; $this->assertEquals($expected, $this->subject->addData($input)); } @@ -207,6 +207,9 @@ class TcaSelectTreeItemsTest extends UnitTestCase /** @var TableConfigurationTree|ObjectProphecy $treeDataProviderProphecy */ $tableConfigurationTreeProphecy = $this->prophesize(TableConfigurationTree::class); GeneralUtility::addInstance(TableConfigurationTree::class, $tableConfigurationTreeProphecy->reveal()); + $tableConfigurationTreeProphecy->render()->willReturn([]); + $tableConfigurationTreeProphecy->setDataProvider(Argument::cetera())->shouldBeCalled(); + $tableConfigurationTreeProphecy->setNodeRenderer(Argument::cetera())->shouldBeCalled(); $input = [ 'tableName' => 'aTable', diff --git a/typo3/sysext/core/Classes/Tree/TableConfiguration/ExtJsArrayTreeRenderer.php b/typo3/sysext/core/Classes/Tree/TableConfiguration/ExtJsArrayTreeRenderer.php index 326a495358ee..e7eaa351cb5a 100644 --- a/typo3/sysext/core/Classes/Tree/TableConfiguration/ExtJsArrayTreeRenderer.php +++ b/typo3/sysext/core/Classes/Tree/TableConfiguration/ExtJsArrayTreeRenderer.php @@ -19,27 +19,6 @@ namespace TYPO3\CMS\Core\Tree\TableConfiguration; */ class ExtJsArrayTreeRenderer extends \TYPO3\CMS\Backend\Tree\Renderer\ExtJsJsonTreeRenderer { - /** - * Gets the node array. If the TCA configuration has defined items, - * they are added to rootlevel on top of the tree - * - * @param \TYPO3\CMS\Backend\Tree\TreeRepresentationNode|DatabaseTreeNode $node - * @return array - */ - protected function getNodeArray(\TYPO3\CMS\Backend\Tree\TreeRepresentationNode $node) - { - $nodeArray = parent::getNodeArray($node); - $nodeArray = array_merge($nodeArray, [ - 'expanded' => $node->getExpanded(), - 'expandable' => $node->hasChildNodes(), - 'checked' => $node->getSelected() - ]); - if (!$node->getSelectable()) { - unset($nodeArray['checked']); - } - return $nodeArray; - } - /** * Renders a node collection recursive or just a single instance * @@ -50,7 +29,6 @@ class ExtJsArrayTreeRenderer extends \TYPO3\CMS\Backend\Tree\Renderer\ExtJsJsonT public function renderTree(\TYPO3\CMS\Backend\Tree\AbstractTree $tree, $recursive = true) { $this->recursionLevel = 0; - $children = $this->renderNode($tree->getRoot(), $recursive); - return $children; + return $this->renderNode($tree->getRoot(), $recursive); } } diff --git a/typo3/sysext/core/Classes/Tree/TableConfiguration/TreeDataProviderFactory.php b/typo3/sysext/core/Classes/Tree/TableConfiguration/TreeDataProviderFactory.php index c78ce41058a2..bf11ae3d5f10 100644 --- a/typo3/sysext/core/Classes/Tree/TableConfiguration/TreeDataProviderFactory.php +++ b/typo3/sysext/core/Classes/Tree/TableConfiguration/TreeDataProviderFactory.php @@ -26,7 +26,7 @@ class TreeDataProviderFactory * @param array $tcaConfiguration * @param $table * @param $field - * @param $currentValue + * @param array $currentValue The current database row, handing over 'uid' is enough * @return DatabaseTreeDataProvider * @throws \InvalidArgumentException */ @@ -45,7 +45,6 @@ class TreeDataProviderFactory $tcaConfiguration['internal_type'] = 'db'; } if ($tcaConfiguration['internal_type'] === 'db') { - $unselectableUids = []; if ($dataProvider === null) { $dataProvider = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Tree\TableConfiguration\DatabaseTreeDataProvider::class); } @@ -53,7 +52,9 @@ class TreeDataProviderFactory $tableName = $tcaConfiguration['foreign_table']; $dataProvider->setTableName($tableName); if ($tableName == $table) { - $unselectableUids[] = $currentValue['uid']; + // The uid of the currently opened row can not be selected in a table relation to "self" + $unselectableUids = [ $currentValue['uid'] ]; + $dataProvider->setItemUnselectableList($unselectableUids); } } else { throw new \InvalidArgumentException('TCA Tree configuration is invalid: "foreign_table" not set', 1288215888); @@ -64,7 +65,6 @@ class TreeDataProviderFactory $dataProvider->setLabelField($GLOBALS['TCA'][$tableName]['ctrl']['label']); } $dataProvider->setTreeId(md5($table . '|' . $field)); - $dataProvider->setSelectedList($currentValue); $treeConfiguration = $tcaConfiguration['treeConfig']; if (isset($treeConfiguration['rootUid'])) { @@ -90,7 +90,6 @@ class TreeDataProviderFactory } else { throw new \InvalidArgumentException('TCA Tree configuration is invalid: neither "childrenField" nor "parentField" is set', 1288215889); } - $dataProvider->setItemUnselectableList($unselectableUids); } elseif ($tcaConfiguration['internal_type'] === 'file' && $dataProvider === null) { // @todo Not implemented yet throw new \InvalidArgumentException('TCA Tree configuration is invalid: tree for "internal_type=file" not implemented yet', 1288215891); -- GitLab