[TASK] TCA tree: Simplify json result 13/50813/10
authorChristian Kuhn <lolli@schwarzbu.ch>
Wed, 30 Nov 2016 16:29:10 +0000 (17:29 +0100)
committerChristian Kuhn <lolli@schwarzbu.ch>
Wed, 7 Dec 2016 11:16:57 +0000 (12:16 +0100)
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>
typo3/sysext/backend/Classes/Controller/SelectTreeController.php
typo3/sysext/backend/Classes/Form/FormDataProvider/AbstractItemProvider.php
typo3/sysext/backend/Classes/Form/FormDataProvider/TcaSelectTreeItems.php
typo3/sysext/backend/Classes/Tree/Renderer/ExtJsJsonTreeRenderer.php
typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/SelectTree.js
typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/SvgTree.js
typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/TreeToolbar.js
typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaSelectTreeItemsTest.php
typo3/sysext/core/Classes/Tree/TableConfiguration/ExtJsArrayTreeRenderer.php
typo3/sysext/core/Classes/Tree/TableConfiguration/TreeDataProviderFactory.php

index a0f6423..eee0d93 100644 (file)
@@ -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;
     }
index 58f001f..f388e6f 100644 (file)
@@ -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) {
index 92fed04..ebd1b63 100644 (file)
@@ -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
      *
index 4ff6365..12bd862 100644 (file)
@@ -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;
     }
index a20c722..e908dbc 100644 (file)
@@ -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
             }));
         }
     };
index 93aab43..f20b71f 100644 (file)
@@ -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();
         }
index d6ebb58..f86d1a8 100644 (file)
@@ -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;
index f3d66dd..d9cb748 100644 (file)
@@ -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',
index 326a495..e7eaa35 100644 (file)
@@ -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);
     }
 }
index c78ce41..bf11ae3 100644 (file)
@@ -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);