[TASK] Visualize nested data structures in workspace module 01/28701/5
authorOliver Hader <oliver@typo3.org>
Mon, 24 Mar 2014 13:39:56 +0000 (14:39 +0100)
committerOliver Hader <oliver.hader@typo3.org>
Tue, 25 Mar 2014 15:15:23 +0000 (16:15 +0100)
The current workspace module only has two levels (page and the
accordant records). Nested record sets like tt_content ->
sys_file_reference (any "text with image" content element) are
not recognized as dependent and need to be published separately.

The GridDataService is extended to determine the the accordant
nested record sets and provides additional data for parent and
child scenarios. The ExtJS view components are extended to take
care of nested record sets and to handle expand and collapse
events on these kind of record collections.

Resolves: #55349
Releases: 6.2
Change-Id: I93ca187c3997bf7f4cdadefd741be2541aef5ae4
Reviewed-on: https://review.typo3.org/28701
Reviewed-by: Oliver Hader
Tested-by: Oliver Hader
13 files changed:
typo3/sysext/workspaces/Classes/Controller/ReviewController.php
typo3/sysext/workspaces/Classes/Service/Dependency/CollectionService.php [new file with mode: 0644]
typo3/sysext/workspaces/Classes/Service/GridDataService.php
typo3/sysext/workspaces/Resources/Public/Images/zoom_in.png [new file with mode: 0755]
typo3/sysext/workspaces/Resources/Public/Images/zoom_out.png [new file with mode: 0755]
typo3/sysext/workspaces/Resources/Public/JavaScript/Component/RowDetailTemplate.js [new file with mode: 0644]
typo3/sysext/workspaces/Resources/Public/JavaScript/Component/RowExpander.js [new file with mode: 0644]
typo3/sysext/workspaces/Resources/Public/JavaScript/Store/mainstore.js
typo3/sysext/workspaces/Resources/Public/JavaScript/component.js
typo3/sysext/workspaces/Resources/Public/JavaScript/configuration.js
typo3/sysext/workspaces/Resources/Public/JavaScript/grid.js
typo3/sysext/workspaces/Resources/Public/JavaScript/workspaces.js
typo3/sysext/workspaces/Resources/Public/StyleSheet/module.css

index f5e4c60..63cabc0 100644 (file)
@@ -177,6 +177,8 @@ class ReviewController extends \TYPO3\CMS\Workspaces\Controller\AbstractControll
                $custom = $this->getAdditionalResourceService()->getJavaScriptResources();
 
                $resources = array(
+                       $resourcePath . 'Component/RowDetailTemplate.js',
+                       $resourcePath . 'Component/RowExpander.js',
                        $resourcePath . 'Store/mainstore.js',
                        $resourcePath . 'configuration.js',
                        $resourcePath . 'helpers.js',
diff --git a/typo3/sysext/workspaces/Classes/Service/Dependency/CollectionService.php b/typo3/sysext/workspaces/Classes/Service/Dependency/CollectionService.php
new file mode 100644 (file)
index 0000000..ac7482e
--- /dev/null
@@ -0,0 +1,225 @@
+<?php
+namespace TYPO3\CMS\Workspaces\Service\Dependency;
+
+/***************************************************************
+ * Copyright notice
+ *
+ * (c) 2014 Oliver Hader <oliver.hader@typo3.org>
+ * All rights reserved
+ *
+ * This script is part of the TYPO3 project. The TYPO3 project is
+ * free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * The GNU General Public License can be found at
+ * http://www.gnu.org/copyleft/gpl.html.
+ * A copy is found in the text file GPL.txt and important notices to the license
+ * from the author is found in LICENSE.txt distributed with these scripts.
+ *
+ *
+ * This script is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * This copyright notice MUST APPEAR in all copies of the script!
+ ***************************************************************/
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Workspaces\Service\GridDataService;
+use TYPO3\CMS\Version\Dependency;
+
+/**
+ * Service to collect dependent elements.
+ *
+ * @author Oliver Hader <oliver.hader@typo3.org>
+ */
+class CollectionService implements \TYPO3\CMS\Core\SingletonInterface {
+
+       /**
+        * @var \TYPO3\CMS\Core\DataHandling\DataHandler
+        */
+       protected $dataHandler;
+
+       /**
+        * @var \TYPO3\CMS\Version\Dependency\ElementEntityProcessor
+        */
+       protected $elementEntityProcessor;
+
+       /**
+        * @var Dependency\DependencyResolver
+        */
+       protected $dependencyResolver;
+
+       /**
+        * @var array
+        */
+       protected $dataArray;
+
+       /**
+        * @var array
+        */
+       protected $nestedDataArray;
+
+       /**
+        * @return Dependency\DependencyResolver
+        */
+       public function getDependencyResolver() {
+               if (!isset($this->dependencyResolver)) {
+                       $this->dependencyResolver = GeneralUtility::makeInstance('TYPO3\\CMS\\Version\\Dependency\\DependencyResolver');
+                       $this->dependencyResolver->setOuterMostParentsRequireReferences(TRUE);
+                       $this->dependencyResolver->setWorkspace($this->getWorkspace());
+
+                       $this->dependencyResolver->setEventCallback(
+                               Dependency\ElementEntity::EVENT_CreateChildReference,
+                               $this->getDependencyCallback('createNewDependentElementChildReferenceCallback')
+                       );
+
+                       $this->dependencyResolver->setEventCallback(
+                               Dependency\ElementEntity::EVENT_CreateParentReference,
+                               $this->getDependencyCallback('createNewDependentElementParentReferenceCallback')
+                       );
+               }
+
+               return $this->dependencyResolver;
+       }
+
+       /**
+        * Gets a new callback to be used in the dependency resolver utility.
+        *
+        * @param string $method
+        * @param array $targetArguments
+        * @return Dependency\EventCallback
+        */
+       protected function getDependencyCallback($method, array $targetArguments = array()) {
+               return GeneralUtility::makeInstance(
+                       'TYPO3\\CMS\\Version\\Dependency\\EventCallback',
+                       $this->getElementEntityProcessor(), $method, $targetArguments
+               );
+       }
+
+       /**
+        * Gets the element entity processor.
+        *
+        * @return \TYPO3\CMS\Version\Dependency\ElementEntityProcessor
+        */
+       protected function getElementEntityProcessor() {
+               if (!isset($this->elementEntityProcessor)) {
+                       $this->elementEntityProcessor = GeneralUtility::makeInstance(
+                               'TYPO3\\CMS\\Version\\Dependency\\ElementEntityProcessor'
+                       );
+                       $this->elementEntityProcessor->setWorkspace($this->getWorkspace());
+               }
+               return $this->elementEntityProcessor;
+       }
+
+       /**
+        * Gets the current workspace id.
+        *
+        * @return int
+        */
+       protected function getWorkspace() {
+               return (int)$GLOBALS['BE_USER']->workspace;
+       }
+
+       /**
+        * Processes the data array
+        *
+        * @param array $dataArray
+        * @return array
+        */
+       public function process(array $dataArray) {
+               $collection = 0;
+               $this->dataArray = $dataArray;
+               $this->nestedDataArray = array();
+
+               $outerMostParents = $this->getDependencyResolver()->getOuterMostParents();
+
+               if (empty($outerMostParents)) {
+                       return $this->dataArray;
+               }
+
+               // For each outer most parent, get all nested child elements:
+               foreach ($outerMostParents as $outerMostParent) {
+                       $this->resolveDataArrayChildDependencies(
+                               $outerMostParent,
+                               ++$collection
+                       );
+               }
+
+               $processedDataArray = $this->finalize($this->dataArray);
+
+               unset($this->dataArray);
+               unset($this->nestedDataArray);
+
+               return $processedDataArray;
+       }
+
+       /**
+        * Applies structures to instance data array and
+        * ensures children are added below accordant parent
+        *
+        * @param array $dataArray
+        * @return array
+        */
+       protected function finalize(array $dataArray) {
+               $processedDataArray = array();
+               foreach ($dataArray as $dataElement) {
+                       $dataElementIdentifier = $dataElement['id'];
+                       $processedDataArray[] = $dataElement;
+                       // Insert children (if any)
+                       if (!empty($this->nestedDataArray[$dataElementIdentifier])) {
+                               $processedDataArray = array_merge(
+                                       $processedDataArray,
+                                       $this->finalize($this->nestedDataArray[$dataElementIdentifier])
+                               );
+                               unset($this->nestedDataArray[$dataElementIdentifier]);
+                       }
+               }
+
+               return $processedDataArray;
+       }
+
+       /**
+        * Resolves nested child dependencies.
+        *
+        * @param Dependency\ElementEntity $parent
+        * @param int $collection
+        * @param string $nextParentIdentifier
+        * @param int $collectionLevel
+        */
+       protected function resolveDataArrayChildDependencies(Dependency\ElementEntity $parent, $collection, $nextParentIdentifier = '', $collectionLevel = 0) {
+               $parentIdentifier = $parent->__toString();
+               $parentIsSet = isset($this->dataArray[$parentIdentifier]);
+
+               if ($parentIsSet) {
+                       $this->dataArray[$parentIdentifier][GridDataService::GridColumn_Collection] = $collection;
+                       $this->dataArray[$parentIdentifier][GridDataService::GridColumn_CollectionLevel] = $collectionLevel;
+                       $this->dataArray[$parentIdentifier][GridDataService::GridColumn_CollectionCurrent] = md5($parentIdentifier);
+                       $this->dataArray[$parentIdentifier][GridDataService::GridColumn_CollectionChildren] = count($parent->getChildren());
+                       $nextParentIdentifier = $parentIdentifier;
+                       $collectionLevel++;
+               }
+
+               foreach ($parent->getChildren() as $child) {
+                       $this->resolveDataArrayChildDependencies(
+                               $child->getElement(),
+                               $collection,
+                               $nextParentIdentifier,
+                               $collectionLevel
+                       );
+
+                       $childIdentifier = $child->getElement()->__toString();
+                       if (!empty($nextParentIdentifier) && isset($this->dataArray[$childIdentifier])) {
+                               // Remove from dataArray, but collect to process later
+                               // and add it just next to the accordant parent element
+                               $this->dataArray[$childIdentifier][GridDataService::GridColumn_CollectionParent] = md5($nextParentIdentifier);
+                               $this->nestedDataArray[$nextParentIdentifier][] = $this->dataArray[$childIdentifier];
+                               unset($this->dataArray[$childIdentifier]);
+                       }
+               }
+       }
+
+}
\ No newline at end of file
index aeca0ab..3296664 100644 (file)
@@ -28,6 +28,7 @@ namespace TYPO3\CMS\Workspaces\Service;
  ***************************************************************/
 
 use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
  * Grid data service
@@ -40,6 +41,13 @@ class GridDataService {
        const SIGNAL_GenerateDataArray_PostProcesss = 'generateDataArray.postProcess';
        const SIGNAL_GetDataArray_PostProcesss = 'getDataArray.postProcess';
        const SIGNAL_SortDataArray_PostProcesss = 'sortDataArray.postProcess';
+
+       const GridColumn_Collection = 'Workspaces_Collection';
+       const GridColumn_CollectionLevel = 'Workspaces_CollectionLevel';
+       const GridColumn_CollectionParent = 'Workspaces_CollectionParent';
+       const GridColumn_CollectionCurrent = 'Workspaces_CollectionCurrent';
+       const GridColumn_CollectionChildren = 'Workspaces_CollectionChildren';
+
        /**
         * Id of the current active workspace.
         *
@@ -127,9 +135,15 @@ class GridDataService {
                // check for dataArray in cache
                if ($this->getDataArrayFromCache($versions, $filterTxt) === FALSE) {
                        /** @var $stagesObj \TYPO3\CMS\Workspaces\Service\StagesService */
-                       $stagesObj = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Workspaces\\Service\\StagesService');
+                       $stagesObj = GeneralUtility::makeInstance('TYPO3\\CMS\\Workspaces\\Service\\StagesService');
+                       $defaultGridColumns = array(
+                               self::GridColumn_Collection => 0,
+                               self::GridColumn_CollectionLevel => 0,
+                               self::GridColumn_CollectionParent => '',
+                               self::GridColumn_CollectionCurrent => '',
+                               self::GridColumn_CollectionChildren => 0,
+                       );
                        foreach ($versions as $table => $records) {
-                               $versionArray = array('table' => $table);
                                $hiddenField = $this->getTcaEnableColumnsFieldName($table, 'disabled');
                                $isRecordTypeAllowedToModify = $GLOBALS['BE_USER']->check('tables_modify', $table);
 
@@ -147,9 +161,12 @@ class GridDataService {
 
                                        $isDeletedPage = $table == 'pages' && $recordState == 'deleted';
                                        $viewUrl = \TYPO3\CMS\Workspaces\Service\WorkspaceService::viewSingleRecord($table, $record['uid'], $origRecord, $versionRecord);
+                                       $versionArray = array();
+                                       $versionArray['table'] = $table;
                                        $versionArray['id'] = $table . ':' . $record['uid'];
                                        $versionArray['uid'] = $record['uid'];
                                        $versionArray['workspace'] = $versionRecord['t3ver_id'];
+                                       $versionArray = array_merge($versionArray, $defaultGridColumns);
                                        $versionArray['label_Workspace'] = htmlspecialchars(
                                                BackendUtility::getRecordTitle($table, $versionRecord));
                                        $versionArray['label_Live'] = htmlspecialchars(BackendUtility::getRecordTitle($table, $origRecord));
@@ -199,7 +216,8 @@ class GridDataService {
                                        );
 
                                        if ($filterTxt == '' || $this->isFilterTextInVisibleColumns($filterTxt, $versionArray)) {
-                                               $this->dataArray[] = $versionArray;
+                                               $versionIdentifier = $versionArray['id'];
+                                               $this->dataArray[$versionIdentifier] = $versionArray;
                                        }
                                }
                        }
@@ -220,6 +238,24 @@ class GridDataService {
                // methodName(\TYPO3\CMS\Workspaces\Service\GridDataService $gridData, array &$dataArray, array $versions)
                $this->emitSignal(self::SIGNAL_GenerateDataArray_PostProcesss, $this->dataArray, $versions);
                $this->sortDataArray();
+               $this->resolveDataArrayDependencies();
+       }
+
+       /**
+        * Resolves dependencies of nested structures
+        * and sort data elements considering these dependencies.
+        *
+        * @return void
+        */
+       protected function resolveDataArrayDependencies() {
+               $collectionService = $this->getDependencyCollectionService();
+               $dependencyResolver = $collectionService->getDependencyResolver();
+
+               foreach ($this->dataArray as $dataElement) {
+                       $dependencyResolver->addElement($dataElement['table'], $dataElement['uid']);
+               }
+
+               $this->dataArray = $collectionService->process($this->dataArray);
        }
 
        /**
@@ -231,10 +267,23 @@ class GridDataService {
         */
        protected function getDataArray($start, $limit) {
                $dataArrayPart = array();
-               $end = $start + $limit < count($this->dataArray) ? $start + $limit : count($this->dataArray);
+               $dataArrayCount = count($this->dataArray);
+               $end = ($start + $limit < count($this->dataArray) ? $start + $limit : $dataArrayCount);
+
+               // Ensure that there are numerical indexes
+               $this->dataArray = array_values(($this->dataArray));
                for ($i = $start; $i < $end; $i++) {
                        $dataArrayPart[] = $this->dataArray[$i];
                }
+
+               // Ensure that collections are not cut for the pagination
+               if (!empty($this->dataArray[$i][self::GridColumn_Collection])) {
+                       $collectionIdentifier = $this->dataArray[$i][self::GridColumn_Collection];
+                       for ($i = $i + 1; $i < $dataArrayCount && $collectionIdentifier === $this->dataArray[$i][self::GridColumn_Collection]; $i++) {
+                               $dataArrayPart[] = $this->dataArray[$i];
+                       }
+               }
+
                // Suggested slot method:
                // methodName(\TYPO3\CMS\Workspaces\Service\GridDataService $gridData, array &$dataArray, $start, $limit)
                $this->emitSignal(self::SIGNAL_GetDataArray_PostProcesss, $this->dataArray, $start, $limit);
@@ -247,7 +296,7 @@ class GridDataService {
         * @return void
         */
        protected function initializeWorkspacesCachingFramework() {
-               $this->workspacesCache = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Cache\\CacheManager')->getCache('workspaces_cache');
+               $this->workspacesCache = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Cache\\CacheManager')->getCache('workspaces_cache');
        }
 
        /**
@@ -310,39 +359,27 @@ class GridDataService {
                if (is_array($this->dataArray)) {
                        switch ($this->sort) {
                                case 'uid':
-
                                case 'change':
-
                                case 'workspace_Tstamp':
-
                                case 't3ver_oid':
-
                                case 'liveid':
-
                                case 'livepid':
-
                                case 'languageValue':
-                                       usort($this->dataArray, array($this, 'intSort'));
+                                       uasort($this->dataArray, array($this, 'intSort'));
                                        break;
-
                                case 'label_Workspace':
-
                                case 'label_Live':
-
                                case 'label_Stage':
-
                                case 'workspace_Title':
-
                                case 'path_Live':
                                        // case 'path_Workspace': This is the first sorting attribute
-                                       usort($this->dataArray, array($this, 'stringSort'));
+                                       uasort($this->dataArray, array($this, 'stringSort'));
                                        break;
-
                                default:
                                        // Do nothing
                        }
                } else {
-                       \TYPO3\CMS\Core\Utility\GeneralUtility::sysLog('Try to sort "' . $this->sort . '" in "TYPO3\\CMS\\Workspaces\\Service\\GridDataService::sortDataArray" but $this->dataArray is empty! This might be the Bug #26422 which could not reproduced yet.', 3);
+                       GeneralUtility::sysLog('Try to sort "' . $this->sort . '" in "TYPO3\\CMS\\Workspaces\\Service\\GridDataService::sortDataArray" but $this->dataArray is empty! This might be the Bug #26422 which could not reproduced yet.', 3);
                }
                // Suggested slot method:
                // methodName(\TYPO3\CMS\Workspaces\Service\GridDataService $gridData, array &$dataArray, $sortColumn, $sortDirection)
@@ -357,6 +394,9 @@ class GridDataService {
         * @return integer
         */
        protected function intSort(array $a, array $b) {
+               if (!$this->isSortable($a, $b)) {
+                       return 0;
+               }
                // First sort by using the page-path in current workspace
                $path_cmp = strcasecmp($a['path_Workspace'], $b['path_Workspace']);
                if ($path_cmp < 0) {
@@ -384,6 +424,9 @@ class GridDataService {
         * @return integer
         */
        protected function stringSort($a, $b) {
+               if (!$this->isSortable($a, $b)) {
+                       return 0;
+               }
                $path_cmp = strcasecmp($a['path_Workspace'], $b['path_Workspace']);
                if ($path_cmp < 0) {
                        return $path_cmp;
@@ -403,6 +446,22 @@ class GridDataService {
        }
 
        /**
+        * Determines whether dataArray elements are sortable.
+        * Only elements on the first level (0) or below the same
+        * parent element are directly sortable.
+        *
+        * @param array $a
+        * @param array $b
+        * @return bool
+        */
+       protected function isSortable(array $a, array $b) {
+               return (
+                       $a[self::GridColumn_CollectionLevel] === 0 && $b[self::GridColumn_CollectionLevel] === 0
+                       || $a[self::GridColumn_CollectionParent] === $b[self::GridColumn_CollectionParent]
+               );
+       }
+
+       /**
         * Determines whether the text used to filter the results is part of
         * a column that is visible in the grid view.
         *
@@ -531,7 +590,7 @@ class GridDataService {
        public function getSystemLanguages() {
                if (!isset($this->systemLanguages)) {
                        /** @var $translateTools \TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider */
-                       $translateTools = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Backend\\Configuration\\TranslationConfigurationProvider');
+                       $translateTools = GeneralUtility::makeInstance('TYPO3\\CMS\\Backend\\Configuration\\TranslationConfigurationProvider');
                        $this->systemLanguages = $translateTools->getSystemLanguages();
                }
                return $this->systemLanguages;
@@ -544,7 +603,7 @@ class GridDataService {
         */
        protected function getIntegrityService() {
                if (!isset($this->integrityService)) {
-                       $this->integrityService = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Workspaces\\Service\\IntegrityService');
+                       $this->integrityService = GeneralUtility::makeInstance('TYPO3\\CMS\\Workspaces\\Service\\IntegrityService');
                }
                return $this->integrityService;
        }
@@ -562,6 +621,13 @@ class GridDataService {
        }
 
        /**
+        * @return \TYPO3\CMS\Workspaces\Service\Dependency\CollectionService
+        */
+       protected function getDependencyCollectionService() {
+               return GeneralUtility::makeInstance('TYPO3\\CMS\\Workspaces\\Service\\Dependency\\CollectionService');
+       }
+
+       /**
         * @return \TYPO3\CMS\Workspaces\Service\AdditionalColumnService
         */
        protected function getAdditionalColumnService() {
diff --git a/typo3/sysext/workspaces/Resources/Public/Images/zoom_in.png b/typo3/sysext/workspaces/Resources/Public/Images/zoom_in.png
new file mode 100755 (executable)
index 0000000..cdf0a52
Binary files /dev/null and b/typo3/sysext/workspaces/Resources/Public/Images/zoom_in.png differ
diff --git a/typo3/sysext/workspaces/Resources/Public/Images/zoom_out.png b/typo3/sysext/workspaces/Resources/Public/Images/zoom_out.png
new file mode 100755 (executable)
index 0000000..07bf98a
Binary files /dev/null and b/typo3/sysext/workspaces/Resources/Public/Images/zoom_out.png differ
diff --git a/typo3/sysext/workspaces/Resources/Public/JavaScript/Component/RowDetailTemplate.js b/typo3/sysext/workspaces/Resources/Public/JavaScript/Component/RowDetailTemplate.js
new file mode 100644 (file)
index 0000000..0a33cc6
--- /dev/null
@@ -0,0 +1,10 @@
+Ext.ns('TYPO3.Workspaces.Component');
+
+TYPO3.Workspaces.Component.RowDetailTemplate = Ext.extend(Ext.XTemplate, {
+       exists: function(o, name) {
+               return typeof o != 'undefined' && o != null && o!='';
+       },
+       hasComments: function(comments){
+               return comments.length>0;
+       }
+});
diff --git a/typo3/sysext/workspaces/Resources/Public/JavaScript/Component/RowExpander.js b/typo3/sysext/workspaces/Resources/Public/JavaScript/Component/RowExpander.js
new file mode 100644 (file)
index 0000000..89a5501
--- /dev/null
@@ -0,0 +1,320 @@
+Ext.ns('TYPO3.Workspaces.Component');
+
+TYPO3.Workspaces.Component.RowExpander = Ext.extend(Ext.grid.RowExpander, {
+       menuDisabled: true,
+       hideable: false,
+
+       rowDetailTemplate: [
+               '<div class="t3-workspaces-foldoutWrapper">',
+               '<tpl for=".">',
+                       '<tpl>',
+                               '<table class="char_select_template" width="100%">',
+                                       '<tr class="header">',
+                                               '<th class="char_select_profile_titleLeft">',
+                                                       '{[TYPO3.l10n.localize(\'workspace_version\')]}',
+                                               '</th>',
+                                               '<th class="char_select_profile_titleRight">',
+                                                       '{[TYPO3.l10n.localize(\'live_workspace\')]}',
+                                               '</th>',
+                                       '</tr>',
+                                       '<tr>',
+                                               '<td class="t3-workspaces-foldout-subheaderLeft">',
+                                                       '{[String.format(TYPO3.l10n.localize(\'current_step\'), values.label_Stage, values.stage_position, values.stage_count)]}',
+                                               '</td>',
+                                               '<td class="t3-workspaces-foldout-subheaderRight">',
+                                                       '{[String.format(TYPO3.l10n.localize(\'path\'), values.path_Live)]}',
+                                               '</td>',
+                                       '</tr>',
+                                       '<tr>',
+                                               '<td class="t3-workspaces-foldout-td-contentDiffLeft">',
+                                                       '<div class="t3-workspaces-foldout-contentDiff-container">',
+                                                               '<table class="t3-workspaces-foldout-contentDiff">',
+                                                                       '<tr><th><span class="{icon_Workspace}">&nbsp;</span></th><td>{type_Workspace}</td></tr>',
+                                                                       '<tpl for="diff">',
+                                                                               '<tr><th>{label}</th><td class="content">',
+                                                                                       '<tpl if="this.exists(content)">',
+                                                                                               '{content}',
+                                                                                       '</tpl>',
+                                                                               '</td></tr>',
+                                                                       '</tpl>',
+                                                               '</table>',
+                                                       '</div>',
+                                               '</td>',
+                                               '<td class="t3-workspaces-foldout-td-contentDiffRight">',
+                                                       '<div class="t3-workspaces-foldout-contentDiff-container">',
+                                                               '<table class="t3-workspaces-foldout-contentDiff">',
+                                                                       '<tr><th><span class="{icon_Live}"></span></th><td>{type_Live}</td></tr>',
+                                                                       '<tpl for="live_record">',
+                                                                               '<tr><th>{label}</th><td class="content">',
+                                                                                       '<tpl if="this.exists(content)">',
+                                                                                               '{content}',
+                                                                                       '</tpl>',
+                                                                               '</td></tr>',
+                                                                       '</tpl>',
+                                                               '</table>',
+                                                       '</div>',
+                                               '</td>',
+                                       '</tr>',
+                                       '<tpl if="this.hasComments(comments)">',
+                                       '<tr>',
+                                               '<td class="t3-workspaces-foldout-subheaderLeft">',
+                                                       '<div class="t3-workspaces-foldout-subheader-container">',
+                                                               '{[String.format(TYPO3.l10n.localize(\'comments\'), values.stage_position, values.label_Stage)]}',
+                                                       '</div>',
+                                               '</td>',
+                                               '<td class="t3-workspaces-foldout-subheaderRight">',
+                                                       '&nbsp;',
+                                               '</td>',
+                                       '</tr>',
+                                       '<tr>',
+                                               '<td class="char_select_profile_stats">',
+                                                       '<div class="t3-workspaces-comments">',
+                                                       '<tpl for="comments">',
+                                                               '<div class="t3-workspaces-comments-singleComment">',
+                                                                       '<div class="t3-workspaces-comments-singleComment-author">',
+                                                                               '{user_username}',
+                                                                       '</div>',
+                                                                       '<div class="t3-workspaces-comments-singleComment-content-wrapper"><div class="t3-workspaces-comments-singleComment-content">',
+                                                                               '<span class="t3-workspaces-comments-singleComment-content-date">{tstamp}</span>',
+                                                                               '<div class="t3-workspaces-comments-singleComment-content-title">@ {[String.format(TYPO3.l10n.localize(\'stage\'), values.stage_title)]}</div>',
+                                                                               '<div class="t3-workspaces-comments-singleComment-content-text">{user_comment}</div>',
+                                                                       '</div></div>',
+                                                               '</div>',
+                                                       '</tpl>',
+                                                       '</div>',
+                                               '</td>',
+                                               '<td class="char_select_profile_title">',
+                                                       '&nbsp;',
+                                               '</td>',
+                                               '</tpl>',
+                                       '</tr>',
+                               '</table>',
+                       '</tpl>',
+               '</tpl>',
+               '</div>',
+               '<div class="x-clear"></div>'
+       ],
+
+       detailStoreConfiguration: {
+               xtype : 'directstore',
+               storeId : 'rowDetailService',
+               root : 'data',
+               totalProperty : 'total',
+               idProperty : 'id',
+               fields : [
+                       {name : 'uid'},
+                       {name : 't3ver_oid'},
+                       {name : 'table'},
+                       {name : 'stage'},
+                       {name : 'diff'},
+                       {name : 'path_Live'},
+                       {name : 'label_Stage'},
+                       {name : 'stage_position'},
+                       {name : 'stage_count'},
+                       {name : 'live_record'},
+                       {name : 'comments'},
+                       {name : 'icon_Live'},
+                       {name : 'icon_Workspace'},
+                       {name : 'languageValue'},
+                       {name : 'integrity'}
+               ]
+       },
+
+       detailStore: null,
+
+       init : function(grid) {
+               TYPO3.Workspaces.Component.RowExpander.superclass.init.call(this, grid);
+               this.detailStore = Ext.create(this.detailStoreConfiguration);
+
+               this.addEvents({
+                       beforeExpandCollection: true,
+                       beforeExpandCollectionChild: true,
+                       beforeCollapseCollection: true,
+                       beforeCollapseCollectionChild: true
+               })
+       },
+
+       getRowClass : function(record, rowIndex, p, ds) {
+               var cls = [];
+
+               cls.push(Ext.grid.RowExpander.prototype.getRowClass.call(this, record, rowIndex, p, ds));
+
+               if (record.json.Workspaces_CollectionChildren > 0) {
+                       // @todo Extend by new nodeState check
+                       cls.push('typo3-workspaces-collection-parent-collapsed');
+               }
+               if (record.json.Workspaces_CollectionParent) {
+                       // @todo Extend by new nodeState check
+                       cls.push('typo3-workspaces-collection-child-collapsed');
+               }
+               if (!record.json.allowedAction_nextStage && !record.json.allowedAction_prevStage && !record.json.allowedAction_swap) {
+                       cls.push('typo3-workspaces-row-disabled');
+               }
+
+               return cls.join(' ');
+       },
+       renderer : function(v, p, record) {
+               var html;
+               html = Ext.grid.RowExpander.prototype.renderer.call(this, v, p, record);
+               return html;
+       },
+       remoteDataMethod : function (record, index) {
+               this.detailStore.baseParams = {
+                       uid: record.json.uid,
+                       table: record.json.table,
+                       stage: record.json.stage,
+                       t3ver_oid: record.json.t3ver_oid,
+                       path_Live: record.json.path_Live,
+                       label_Stage: record.json.label_Stage
+               };
+               this.detailStore.load({
+                       callback: function(r, options, success) {
+                               TYPO3.Workspaces.RowExpander.expandRow(index);
+                       }
+               });
+               new Ext.ux.TYPO3.Workspace.RowPanel({
+                       renderTo: 'remData' + index,
+                       items: [{
+                               xtype: 'dataview',
+                               store: this.detailStore,
+                               tpl: new TYPO3.Workspaces.Component.RowDetailTemplate(this.rowDetailTemplate)
+                       }]
+               });
+       },
+       onMouseDown : function(e, t) {
+               tObject = Ext.get(t);
+               if (tObject.hasClass('x-grid3-row-expander')) {
+                       e.stopEvent();
+                       row = e.getTarget('.x-grid3-row');
+                       this.toggleRow(row);
+               } else if (tObject.hasClass('typo3-workspaces-collection-level-node')) {
+                       e.stopEvent();
+                       row = e.getTarget('.x-grid3-row');
+                       this.toggleCollection(row);
+               }
+       },
+       toggleRow : function(row) {
+               this[Ext.fly(row).hasClass('x-grid3-row-collapsed') ? 'beforeExpand' : 'collapseRow'](row);
+       },
+       beforeExpand : function(row) {
+               if (typeof row == 'number') {
+                       row = this.grid.view.getRow(row);
+               }
+               var record = this.grid.store.getAt(row.rowIndex);
+               var body = Ext.DomQuery.selectNode('tr:nth(2) div.x-grid3-row-body', row);
+
+               if (this.fireEvent('beforexpand', this, record, body, row.rowIndex) !== false) {
+                       this.tpl = new Ext.Template("<div id=\"remData" + row.rowIndex + "\" class=\"rem-data-expand\"><\div>");
+                       if (this.tpl && this.lazyRender) {
+                               body.innerHTML = this.getBodyContent(record, row.rowIndex);
+                       }
+               }
+                       // toggle remoteData loading
+               this.remoteDataMethod(record, row.rowIndex);
+               return true;
+       },
+       expandRow : function(row) {
+               if (typeof row == 'number') {
+                       row = this.grid.view.getRow(row);
+               }
+               var record = this.grid.store.getAt(row.rowIndex);
+               var body = Ext.DomQuery.selectNode('tr:nth(2) div.x-grid3-row-body', row);
+               this.state[record.id] = true;
+               Ext.fly(row).replaceClass('x-grid3-row-collapsed', 'x-grid3-row-expanded');
+               this.fireEvent('expand', this, record, body, row.rowIndex);
+               var i;
+               for(i = 0; i < this.grid.store.getCount(); i++) {
+                       if(i != row.rowIndex) {
+                               this.collapseRow(i);
+                       }
+               }
+       },
+       collapseRow : function(row) {
+               if (typeof row == 'number') {
+                       row = this.grid.view.getRow(row);
+               }
+               var record = this.grid.store.getAt(row.rowIndex);
+               var body = Ext.fly(row).child('tr:nth(1) div.x-grid3-row-body', true);
+               if (this.fireEvent('beforcollapse', this, record, body, row.rowIndex) !== false) {
+                       this.state[record.id] = false;
+                       Ext.fly(row).replaceClass('x-grid3-row-expanded', 'x-grid3-row-collapsed');
+                       this.fireEvent('collapse', this, record, body, row.rowIndex);
+               }
+       },
+
+       toggleCollection : function(row) {
+               if (Ext.fly(row).hasClass('typo3-workspaces-collection-parent-collapsed')) {
+                       this.expandCollection(row);
+               } else {
+                       this.collapseCollection(row);
+               }
+       },
+       expandCollection : function(row) {
+               var record, body, child, i;
+
+               if (typeof row === 'number') {
+                       row = this.grid.view.getRow(row);
+               }
+
+               record = this.grid.store.getAt(row.rowIndex);
+               body = Ext.DomQuery.selectNode('tr:nth(2) div.x-grid3-row-body', row);
+               if (this.fireEvent('beforeExpandCollection', this, record, body, row.rowIndex) !== false) {
+                       for(i = 0; i < this.grid.store.getCount(); i++) {
+                               child = this.grid.store.getAt(i);
+                               if (child.json.Workspaces_CollectionParent === record.json.Workspaces_CollectionCurrent) {
+                                       this.expandCollectionChild(i);
+                               }
+                       }
+                       Ext.fly(row).replaceClass('typo3-workspaces-collection-parent-collapsed', 'typo3-workspaces-collection-parent-expanded');
+               }
+       },
+       expandCollectionChild : function(row) {
+               var record, body;
+
+               if (typeof row === 'number') {
+                       row = this.grid.view.getRow(row);
+               }
+
+               record = this.grid.store.getAt(row.rowIndex);
+               body = Ext.DomQuery.selectNode('tr:nth(2) div.x-grid3-row-body', row);
+               if (this.fireEvent('beforeCollapseCollectionChild', this, record, body, row.rowIndex) !== false) {
+                       Ext.fly(row).replaceClass('typo3-workspaces-collection-child-collapsed', 'typo3-workspaces-collection-child-expanded');
+               }
+       },
+       collapseCollection : function(row) {
+               var record, body, child, i;
+
+               if (typeof row === 'number') {
+                       row = this.grid.view.getRow(row);
+               }
+
+               record = this.grid.store.getAt(row.rowIndex);
+               body = Ext.fly(row).child('tr:nth(1) div.x-grid3-row-body', true);
+               if (this.fireEvent('beforeCollapseCollectionChild', this, record, body, row.rowIndex) !== false) {
+                       for(i = 0; i < this.grid.store.getCount(); i++) {
+                               child = this.grid.store.getAt(i);
+                               if (child.json.Workspaces_CollectionParent === record.json.Workspaces_CollectionCurrent) {
+                                       // Delegate collapsing to child if it has children as well
+                                       if (child.json.Workspaces_CollectionChildren > 0) {
+                                               this.collapseCollection(i);
+                                       }
+                                       this.collapseCollectionChild(i);
+                               }
+                       }
+                       Ext.fly(row).replaceClass('typo3-workspaces-collection-parent-expanded', 'typo3-workspaces-collection-parent-collapsed');
+               }
+       },
+       collapseCollectionChild : function(row) {
+               var record, body;
+
+               if (typeof row === 'number') {
+                       row = this.grid.view.getRow(row);
+               }
+
+               record = this.grid.store.getAt(row.rowIndex);
+               body = Ext.fly(row).child('tr:nth(1) div.x-grid3-row-body', true);
+               if (this.fireEvent('beforeCollapseCollection', this, record, body, row.rowIndex) !== false) {
+                       Ext.fly(row).replaceClass('typo3-workspaces-collection-child-expanded', 'typo3-workspaces-collection-child-collapsed');
+               }
+       }
+});
index 2403056..b66d120 100644 (file)
@@ -1,6 +1,11 @@
 Ext.ns('TYPO3.Workspaces.Configuration');
 
 TYPO3.Workspaces.Configuration.StoreFieldArray = [
+       {name : 'Workspaces_Collection', type : 'int'},
+       {name : 'Workspaces_CollectionLevel', type : 'int'},
+       {name : 'Workspaces_CollectionParent'},
+       {name : 'Workspaces_CollectionCurrent'},
+       {name : 'Workspaces_CollectionChildren', type : 'int'},
        {name : 'table'},
        {name : 'uid', type : 'int'},
        {name : 't3ver_oid', type : 'int'},
@@ -59,12 +64,22 @@ TYPO3.Workspaces.MainStore = new Ext.data.GroupingStore({
 
        showAction : false,
        listeners : {
-               beforeload : function() {
-               },
+               beforeload : function() {},
                load : function(store, records) {
+                       var defaultColumn = TYPO3.Workspaces.WorkspaceGrid.colModel.getColumnById('label_Workspace');
+                       if (defaultColumn) {
+                               defaultColumn.width = defaultColumn.defaultWidth + this.getMaximumCollectionLevel() * defaultColumn.levelWidth;
+                       }
                },
-               datachanged : function(store) {
-               },
-               scope : this
+               datachanged : function(store) {}
+       },
+       getMaximumCollectionLevel: function() {
+               var maximumCollectionLevel = 0;
+               Ext.each(this.data.items, function(item) {
+                       if (item.json.Workspaces_CollectionLevel > maximumCollectionLevel) {
+                               maximumCollectionLevel = item.json.Workspaces_CollectionLevel;
+                       }
+               });
+               return maximumCollectionLevel;
        }
 });
\ No newline at end of file
index a04c129..635bdea 100644 (file)
 
 Ext.ns('TYPO3.Workspaces');
 
-TYPO3.Workspaces.Component = {};
-
-TYPO3.Workspaces.RowDetail = {};
-TYPO3.Workspaces.RowDetail.rowDataStore = new Ext.data.DirectStore({
-       storeId : 'rowDetailService',
-       root : 'data',
-       totalProperty : 'total',
-       idProperty : 'id',
-       fields : [
-               {name : 'uid'},
-               {name : 't3ver_oid'},
-               {name : 'table'},
-               {name : 'stage'},
-               {name : 'diff'},
-               {name : 'path_Live'},
-               {name : 'label_Stage'},
-               {name : 'stage_position'},
-               {name : 'stage_count'},
-               {name : 'live_record'},
-               {name : 'comments'},
-               {name : 'icon_Live'},
-               {name : 'icon_Workspace'},
-               {name : 'languageValue'},
-               {name : 'integrity'}
-       ]
-});
-
-Ext.override(Ext.XTemplate, {
-       exists: function(o, name) {
-               return typeof o != 'undefined' && o != null && o!='';
-       }
-});
-
 Ext.override(Ext.grid.GroupingView, {
        constructId : function(value, field, idx) {
                var cfg = this.cm.config[idx],
@@ -72,108 +39,6 @@ Ext.override(Ext.grid.GroupingView, {
        }
 });
 
-
-TYPO3.Workspaces.RowDetail.rowDetailTemplate = new Ext.XTemplate(
-       '<div class="t3-workspaces-foldoutWrapper">',
-       '<tpl for=".">',
-               '<tpl>',
-                       '<table class="char_select_template" width="100%">',
-                               '<tr class="header">',
-                                       '<th class="char_select_profile_titleLeft">',
-                                               '{[TYPO3.l10n.localize(\'workspace_version\')]}',
-                                       '</th>',
-                                       '<th class="char_select_profile_titleRight">',
-                                               '{[TYPO3.l10n.localize(\'live_workspace\')]}',
-                                       '</th>',
-                               '</tr>',
-                               '<tr>',
-                                       '<td class="t3-workspaces-foldout-subheaderLeft">',
-                                               '{[String.format(TYPO3.l10n.localize(\'current_step\'), values.label_Stage, values.stage_position, values.stage_count)]}',
-                                       '</td>',
-                                       '<td class="t3-workspaces-foldout-subheaderRight">',
-                                               '{[String.format(TYPO3.l10n.localize(\'path\'), values.path_Live)]}',
-                                       '</td>',
-                               '</tr>',
-                               '<tr>',
-                                       '<td class="t3-workspaces-foldout-td-contentDiffLeft">',
-                                               '<div class="t3-workspaces-foldout-contentDiff-container">',
-                                                       '<table class="t3-workspaces-foldout-contentDiff">',
-                                                               '<tr><th><span class="{icon_Workspace}">&nbsp;</span></th><td>{type_Workspace}</td></tr>',
-                                                               '<tpl for="diff">',
-                                                                       '<tr><th>{label}</th><td class="content">',
-                                                                               '<tpl if="this.exists(content)">',
-                                                                                       '{content}',
-                                                                               '</tpl>',
-                                                                       '</td></tr>',
-                                                               '</tpl>',
-                                                       '</table>',
-                                               '</div>',
-                                       '</td>',
-                                       '<td class="t3-workspaces-foldout-td-contentDiffRight">',
-                                               '<div class="t3-workspaces-foldout-contentDiff-container">',
-                                                       '<table class="t3-workspaces-foldout-contentDiff">',
-                                                               '<tr><th><span class="{icon_Live}"></span></th><td>{type_Live}</td></tr>',
-                                                               '<tpl for="live_record">',
-                                                                       '<tr><th>{label}</th><td class="content">',
-                                                                               '<tpl if="this.exists(content)">',
-                                                                                       '{content}',
-                                                                               '</tpl>',
-                                                                       '</td></tr>',
-                                                               '</tpl>',
-                                                       '</table>',
-                                               '</div>',
-                                       '</td>',
-                               '</tr>',
-                               '<tpl if="this.hasComments(comments)">',
-                               '<tr>',
-                                       '<td class="t3-workspaces-foldout-subheaderLeft">',
-                                               '<div class="t3-workspaces-foldout-subheader-container">',
-                                                       '{[String.format(TYPO3.l10n.localize(\'comments\'), values.stage_position, values.label_Stage)]}',
-                                               '</div>',
-                                       '</td>',
-                                       '<td class="t3-workspaces-foldout-subheaderRight">',
-                                               '&nbsp;',
-                                       '</td>',
-                               '</tr>',
-                               '<tr>',
-                                       '<td class="char_select_profile_stats">',
-                                               '<div class="t3-workspaces-comments">',
-                                               '<tpl for="comments">',
-                                                       '<div class="t3-workspaces-comments-singleComment">',
-                                                               '<div class="t3-workspaces-comments-singleComment-author">',
-                                                                       '{user_username}',
-                                                               '</div>',
-                                                               '<div class="t3-workspaces-comments-singleComment-content-wrapper"><div class="t3-workspaces-comments-singleComment-content">',
-                                                                       '<span class="t3-workspaces-comments-singleComment-content-date">{tstamp}</span>',
-                                                                       '<div class="t3-workspaces-comments-singleComment-content-title">@ {[String.format(TYPO3.l10n.localize(\'stage\'), values.stage_title)]}</div>',
-                                                                       '<div class="t3-workspaces-comments-singleComment-content-text">{user_comment}</div>',
-                                                               '</div></div>',
-                                                       '</div>',
-                                               '</tpl>',
-                                               '</div>',
-                                       '</td>',
-                                       '<td class="char_select_profile_title">',
-                                               '&nbsp;',
-                                       '</td>',
-                                       '</tpl>',
-                               '</tr>',
-                       '</table>',
-               '</tpl>',
-       '</tpl>',
-       '</div>',
-       '<div class="x-clear"></div>',
-       {
-               hasComments: function(comments){
-                       return comments.length>0;
-               }
-       }
-);
-
-TYPO3.Workspaces.RowDetail.rowDataView = new Ext.DataView({
-       store: TYPO3.Workspaces.RowDetail.rowDataStore,
-       tpl: TYPO3.Workspaces.RowDetail.rowDetailTemplate
-});
-
 Ext.ns('Ext.ux.TYPO3.Workspace');
 Ext.ux.TYPO3.Workspace.RowPanel = Ext.extend(Ext.Panel, {
        constructor: function(config) {
@@ -189,94 +54,4 @@ Ext.ux.TYPO3.Workspace.RowPanel = Ext.extend(Ext.Panel, {
        }
 });
 
-TYPO3.Workspaces.RowExpander = new Ext.grid.RowExpander({
-       menuDisabled: true,
-       hideable: false,
-       getRowClass : function(record, rowIndex, p, ds) {
-               cssClass = '';
-               if (!record.json.allowedAction_nextStage && !record.json.allowedAction_prevStage && !record.json.allowedAction_swap) {
-                       cssClass = 'typo3-workspaces-row-disabled ';
-               }
-               if(this.state[record.id]) {
-                       cssClass += 'x-grid3-row-expanded';
-               } else {
-                       cssClass += 'x-grid3-row-collapsed';
-               }
-               return cssClass;
-       },
-       remoteDataMethod : function (record, index) {
-               TYPO3.Workspaces.RowDetail.rowDataStore.baseParams = {
-                       uid: record.json.uid,
-                       table: record.json.table,
-                       stage: record.json.stage,
-                       t3ver_oid: record.json.t3ver_oid,
-                       path_Live: record.json.path_Live,
-                       label_Stage: record.json.label_Stage
-               };
-               TYPO3.Workspaces.RowDetail.rowDataStore.load({
-                       callback: function(r, options, success) {
-                               TYPO3.Workspaces.RowExpander.expandRow(index);
-                       }
-               });
-               new Ext.ux.TYPO3.Workspace.RowPanel({
-                       renderTo: 'remData' + index,
-                       items: TYPO3.Workspaces.RowDetail.rowDataView
-               });
-       },
-       onMouseDown : function(e, t) {
-               tObject = Ext.get(t);
-               if (tObject.hasClass('x-grid3-row-expander')) {
-                       e.stopEvent();
-                       var row = e.getTarget('.x-grid3-row');
-                       this.toggleRow(row);
-               }
-       },
-       toggleRow : function(row) {
-               this[Ext.fly(row).hasClass('x-grid3-row-collapsed') ? 'beforeExpand' : 'collapseRow'](row);
-       },
-       beforeExpand : function(row) {
-               if (typeof row == 'number') {
-                       row = this.grid.view.getRow(row);
-               }
-               var record = this.grid.store.getAt(row.rowIndex);
-               var body = Ext.DomQuery.selectNode('tr:nth(2) div.x-grid3-row-body', row);
-
-               if (this.fireEvent('beforexpand', this, record, body, row.rowIndex) !== false) {
-                       this.tpl = new Ext.Template("<div id=\"remData" + row.rowIndex + "\" class=\"rem-data-expand\"><\div>");
-                       if (this.tpl && this.lazyRender) {
-                               body.innerHTML = this.getBodyContent(record, row.rowIndex);
-                       }
-               }
-                       // toggle remoteData loading
-               this.remoteDataMethod(record, row.rowIndex);
-               return true;
-       },
-       expandRow : function(row) {
-               if (typeof row == 'number') {
-                       row = this.grid.view.getRow(row);
-               }
-               var record = this.grid.store.getAt(row.rowIndex);
-               var body = Ext.DomQuery.selectNode('tr:nth(2) div.x-grid3-row-body', row);
-               this.state[record.id] = true;
-               Ext.fly(row).replaceClass('x-grid3-row-collapsed', 'x-grid3-row-expanded');
-               this.fireEvent('expand', this, record, body, row.rowIndex);
-               var i;
-               for(i = 0; i < this.grid.store.getCount(); i++) {
-                       if(i != row.rowIndex) {
-                               this.collapseRow(i);
-                       }
-               }
-       },
-       collapseRow : function(row) {
-               if (typeof row == 'number') {
-                       row = this.grid.view.getRow(row);
-               }
-               var record = this.grid.store.getAt(row.rowIndex);
-               var body = Ext.fly(row).child('tr:nth(1) div.x-grid3-row-body', true);
-               if (this.fireEvent('beforcollapse', this, record, body, row.rowIndex) !== false) {
-                       this.state[record.id] = false;
-                       Ext.fly(row).replaceClass('x-grid3-row-expanded', 'x-grid3-row-collapsed');
-                       this.fireEvent('collapse', this, record, body, row.rowIndex);
-               }
-       }
-});
\ No newline at end of file
+TYPO3.Workspaces.RowExpander = new TYPO3.Workspaces.Component.RowExpander();
index 8e52fbf..4293377 100644 (file)
@@ -106,19 +106,33 @@ TYPO3.Workspaces.Configuration.LivePath = {
 TYPO3.Workspaces.Configuration.WsTitleWithIcon = {
        id: 'label_Workspace',
        dataIndex : 'label_Workspace',
+       // basic definition
+       defaultWidth: 120,
+       // width is extended depending on collection levels
+       // the value is set in addition to this.defaultWidth
        width: 120,
-       hideable: true,
+       // additional width used for each collection level
+       levelWidth: 18,
+       hideable: false,
        sortable: true,
        header : TYPO3.l10n.localize('column.wsTitle'),
        renderer: function(value, metaData, record, rowIndex, colIndex, store) {
                var dekoClass = 'item-state-' + record.json.state_Workspace;
                value = "<span class=\"" + dekoClass + "\">" + value + "</span>";
-               if (record.json.icon_Live === record.json.icon_Workspace) {
-                       return value;
+               // Prepend icon
+               if (record.json.icon_Live !== record.json.icon_Workspace) {
+                       valud = "<span class=\"" + record.json.icon_Workspace + "\">&nbsp;</span>&nbsp;" + value;
+               }
+               // Prepend nested collection level
+               var levelStyle = 'margin-left: ' + record.json.Workspaces_CollectionLevel * this.levelWidth + 'px;';
+               if (record.json.Workspaces_CollectionChildren > 0) {
+                       value = '<div class="typo3-workspaces-collection-level-node" style="' + levelStyle + '">&#160;</div>' + value;
+               } else if (record.json.Workspaces_CollectionLevel > 0) {
+                       value = '<div class="typo3-workspaces-collection-level-leaf" style="' + levelStyle + '">&#160;</div>' + value;
                } else {
-                       return "<span class=\"" + record.json.icon_Workspace + "\">&nbsp;</span>&nbsp;" + value;
+                       value = '<div class="typo3-workspaces-collection-level-none" style="' + levelStyle + '">&#160;</div>' + value;
                }
-
+               return value;
        },
        filter : {type: 'string'}
 };
index 68a9ccc..84721fc 100644 (file)
@@ -96,25 +96,20 @@ TYPO3.Workspaces.WorkspaceGrid = new Ext.grid.GridPanel({
                        this.colModel = new Ext.grid.ColumnModel({
                                columns: [
                                        TYPO3.Workspaces.RowExpander,
-                                       TYPO3.Workspaces.Configuration.Integrity,
                                        {id: 'uid', dataIndex : 'uid', width: 40, sortable: true, header : TYPO3.l10n.localize('column.uid'), hidden: true, filterable : true },
                                        {id: 't3ver_oid', dataIndex : 't3ver_oid', width: 40, sortable: true, header : TYPO3.l10n.localize('column.oid'), hidden: true, filterable : true },
                                        {id: 'workspace_Title', dataIndex : 'workspace_Title', width: 120, sortable: true, header : TYPO3.l10n.localize('column.workspaceName'), hidden: true, filter : {type : 'string'}},
                                        TYPO3.Workspaces.Configuration.WsPath,
-                                       TYPO3.Workspaces.Configuration.Language,
                                        TYPO3.Workspaces.Configuration.LivePath,
                                        TYPO3.Workspaces.Configuration.WsTitleWithIcon,
                                        TYPO3.Workspaces.Configuration.TitleWithIcon,
-                                       TYPO3.Workspaces.Configuration.ChangeDate
+                                       TYPO3.Workspaces.Configuration.ChangeDate,
+                                       TYPO3.Workspaces.Configuration.Integrity,
+                                       TYPO3.Workspaces.Configuration.Language
                                ].concat(TYPO3.Workspaces.Helpers.getAdditionalColumnHandler()),
                                listeners: {
-
-                                       columnmoved: function(colModel) {
-                                               TYPO3.Workspaces.Actions.updateColModel(colModel);
-                                       },
-                                       hiddenchange: function(colModel) {
-                                               TYPO3.Workspaces.Actions.updateColModel(colModel);
-                                       }
+                                       columnmoved: TYPO3.Workspaces.Actions.updateColModel,
+                                       hiddenchange: TYPO3.Workspaces.Actions.updateColModel
                                }
                        });
                } else {
@@ -122,32 +117,26 @@ TYPO3.Workspaces.WorkspaceGrid = new Ext.grid.GridPanel({
                                columns: [
                                        TYPO3.Workspaces.SelectionModel,
                                        TYPO3.Workspaces.RowExpander,
-                                       TYPO3.Workspaces.Configuration.Integrity,
                                        {id: 'uid', dataIndex : 'uid', width: 40, sortable: true, header : TYPO3.l10n.localize('column.uid'), hidden: true, filterable : true },
                                        {id: 't3ver_oid', dataIndex : 't3ver_oid', width: 40, sortable: true, header : TYPO3.l10n.localize('column.oid'), hidden: true, filterable : true },
                                        {id: 'workspace_Title', dataIndex : 'workspace_Title', width: 120, sortable: true, header : TYPO3.l10n.localize('column.workspaceName'), hidden: true, filter : {type : 'string'}},
                                        TYPO3.Workspaces.Configuration.WsPath,
-                                       TYPO3.Workspaces.Configuration.Language,
                                        TYPO3.Workspaces.Configuration.LivePath,
                                        TYPO3.Workspaces.Configuration.WsTitleWithIcon,
                                        TYPO3.Workspaces.Configuration.SwapButton,
                                        TYPO3.Workspaces.Configuration.TitleWithIcon,
                                        TYPO3.Workspaces.Configuration.ChangeDate,
                                        TYPO3.Workspaces.Configuration.Stage,
-                                       TYPO3.Workspaces.Configuration.RowButtons
+                                       TYPO3.Workspaces.Configuration.RowButtons,
+                                       TYPO3.Workspaces.Configuration.Integrity,
+                                       TYPO3.Workspaces.Configuration.Language
                                ].concat(TYPO3.Workspaces.Helpers.getAdditionalColumnHandler()),
                                listeners: {
-
-                                       columnmoved: function(colModel) {
-                                               TYPO3.Workspaces.Actions.updateColModel(colModel);
-                                       },
-                                       hiddenchange: function(colModel) {
-                                               TYPO3.Workspaces.Actions.updateColModel(colModel);
-                                       }
+                                       columnmoved: TYPO3.Workspaces.Actions.updateColModel,
+                                       hiddenchange: TYPO3.Workspaces.Actions.updateColModel
                                }
                        });
                }
-
        },
        border : true,
        store : TYPO3.Workspaces.MainStore,
index ad09c3c..2f70e98 100644 (file)
@@ -68,7 +68,7 @@ Ext.onReady(function() {
                directFn : TYPO3.Workspaces.ExtDirect.getSystemLanguages
        });
 
-       TYPO3.Workspaces.RowDetail.rowDataStore.proxy = new Ext.data.DirectProxy({
+       TYPO3.Workspaces.RowExpander.detailStore.proxy = new Ext.data.DirectProxy({
                directFn: TYPO3.Workspaces.ExtDirect.getRowDetails
        });
        // late binding of ExtDirect
index 785b1a0..37e6777 100644 (file)
@@ -214,4 +214,47 @@ img.t3-icon-workspaces-sendtoprevstage {
 
 table.t3-workspaces-foldout-contentDiff td.content {
        word-break: break-all;
-}
\ No newline at end of file
+}
+
+.typo3-workspaces-collection-level-node,
+.typo3-workspaces-collection-level-leaf,
+.typo3-workspaces-collection-level-none {
+       float: left;
+       width: 18px;
+}
+
+.x-grid3-row-expander {
+       background-position: left top;
+       float: left;
+}
+.x-grid3-row-collapsed .x-grid3-row-expander {
+       background-position: left top;
+       background-image: url('../Images/zoom_in.png');
+}
+.x-grid3-row-expanded .x-grid3-row-expander {
+       background-position: left top;
+       background-image: url('../Images/zoom_out.png');
+}
+
+.typo3-workspaces-collection-child-collapsed {
+       display: none;
+}
+
+.typo3-workspaces-collection-child-expanded {
+       display: block;
+}
+
+.typo3-workspaces-collection-level-node {
+       width: 18px;
+       height: 18px;
+       background-position: 4px 2px;
+       background-color: transparent;
+       background-repeat: no-repeat;
+       background-image: url('../../../../t3skin/extjs/images/grid/row-expand-sprite.png');
+}
+.typo3-workspaces-collection-parent-collapsed .typo3-workspaces-collection-level-node {
+       background-position: 4px 2px;
+}
+.typo3-workspaces-collection-parent-expanded .typo3-workspaces-collection-level-node {
+       background-position: -21px 2px;
+}