diff --git a/vendor/magento/module-media-gallery-ui/Controller/Adminhtml/Directories/GetTree.php b/vendor/magento/module-media-gallery-ui/Controller/Adminhtml/Directories/GetTree.php index 079b25cdc76..6084c244859 100644 --- a/vendor/magento/module-media-gallery-ui/Controller/Adminhtml/Directories/GetTree.php +++ b/vendor/magento/module-media-gallery-ui/Controller/Adminhtml/Directories/GetTree.php @@ -59,9 +59,19 @@ class GetTree extends Action implements HttpGetActionInterface public function execute() { try { - $responseContent = - $this->getDirectoryTree->execute() - ; + $path = $this->getRequest()->getParam('path'); + $loadWholeTree = filter_var( + $this->getRequest()->getParam('loadWholeTree', true), + FILTER_VALIDATE_BOOLEAN + ); + if ($path === '#' || $path === '') { + $path = null; + } + + $responseContent = $this->getDirectoryTree->execute( + is_string($path) ? $path : null, + $loadWholeTree + ); $responseCode = self::HTTP_OK; } catch (\Exception $exception) { $this->logger->critical($exception); diff --git a/vendor/magento/module-media-gallery-ui/Model/Directories/GetDirectoryTree.php b/vendor/magento/module-media-gallery-ui/Model/Directories/GetDirectoryTree.php index bff9d9867dd..592e3001f2d 100644 --- a/vendor/magento/module-media-gallery-ui/Model/Directories/GetDirectoryTree.php +++ b/vendor/magento/module-media-gallery-ui/Model/Directories/GetDirectoryTree.php @@ -12,7 +12,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\ValidatorException; use Magento\Framework\Filesystem; -use Magento\Framework\Filesystem\Directory\Read; +use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; /** @@ -53,27 +53,20 @@ class GetDirectoryTree } /** - * Return directory folder structure in array + * Return the directory folder structure in an array * + * @param string|null $path + * @param bool $loadWholeTree * @return array * @throws ValidatorException */ - public function execute(): array + public function execute(?string $path = null, bool $loadWholeTree = true): array { - $tree = [ - 'name' => 'root', - 'path' => '/', - 'children' => [] - ]; - $directories = $this->getDirectories(); - foreach ($directories as $idx => &$node) { - $node['children'] = []; - $result = $this->findParent($node, $tree); - $parent = &$result['treeNode']; - - $parent['children'][] = &$directories[$idx]; + if ($loadWholeTree) { + return $this->getDirectories(); } - return $tree['children']; + + return $this->getDirectoryNodesForPath($path); } /** @@ -86,36 +79,109 @@ class GetDirectoryTree { $directories = []; - /** @var Read $mediaDirectory */ + /** @var ReadInterface $mediaDirectory */ $mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); if ($mediaDirectory->isDirectory()) { - $imageFolderPaths = $this->coreConfig->getValue( - self::XML_PATH_MEDIA_GALLERY_IMAGE_FOLDERS, - ScopeConfigInterface::SCOPE_TYPE_DEFAULT - ); - sort($imageFolderPaths); - - foreach ($imageFolderPaths as $imageFolderPath) { - $imageDirectory = $this->filesystem->getDirectoryReadByPath( - $mediaDirectory->getAbsolutePath($imageFolderPath) - ); - if ($imageDirectory->isDirectory()) { - $directories[] = $this->getDirectoryData($imageFolderPath); - foreach ($imageDirectory->readRecursively() as $path) { - if ($imageDirectory->isDirectory($path)) { - $directories[] = $this->getDirectoryData( - $mediaDirectory->getRelativePath($imageDirectory->getAbsolutePath($path)) - ); - } - } - } + foreach ($this->getAllowedDirectoryPaths($mediaDirectory) as $imageFolderPath) { + $directories[] = $this->buildDirectoryNode($mediaDirectory, $imageFolderPath); } } return $directories; } + /** + * Return directory nodes for a specific path (or roots when path is empty). + * + * @param string|null $path + * @return array + * @throws ValidatorException + */ + private function getDirectoryNodesForPath(?string $path): array + { + $nodes = []; + + /** @var ReadInterface $mediaDirectory */ + $mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + if (!$mediaDirectory->isDirectory()) { + return $nodes; + } + + if ($path === null || $path === '') { + foreach ($this->getAllowedDirectoryPaths($mediaDirectory) as $rootPath) { + $nodes[] = $this->getDirectoryDataForLazyLoad($mediaDirectory, $rootPath); + } + + return $nodes; + } + + $normalizedPath = trim($path, '/'); + if (!$this->isPathWithinAllowedRoots($normalizedPath, $mediaDirectory)) { + return $nodes; + } + + if (!$mediaDirectory->isDirectory($normalizedPath)) { + return $nodes; + } + + foreach ($this->getSubdirectoryPaths($mediaDirectory, $normalizedPath) as $subdirectoryPath) { + $nodes[] = $this->getDirectoryDataForLazyLoad($mediaDirectory, $subdirectoryPath); + } + + return $nodes; + } + /** + * Check whether a path is inside the configured allowed roots. + * + * @param string $path + * @param ReadInterface $mediaDirectory + * @return bool + */ + private function isPathWithinAllowedRoots(string $path, ReadInterface $mediaDirectory): bool + { + foreach ($this->getAllowedDirectoryPaths($mediaDirectory) as $allowedRoot) { + if ($path === $allowedRoot || strpos($path, $allowedRoot . '/') === 0) { + return true; + } + } + + return false; + } + + /** + * Return sorted and existing top-level media gallery paths. + * + * @param ReadInterface $mediaDirectory + * @return string[] + */ + private function getAllowedDirectoryPaths(ReadInterface $mediaDirectory): array + { + $imageFolderPaths = $this->coreConfig->getValue( + self::XML_PATH_MEDIA_GALLERY_IMAGE_FOLDERS, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT + ); + + if (!is_array($imageFolderPaths)) { + return []; + } + + sort($imageFolderPaths); + + $allowedDirectoryPaths = []; + foreach ($imageFolderPaths as $imageFolderPath) { + if (is_string($imageFolderPath) + && $imageFolderPath !== '' + && !$this->isPathExcluded->execute($imageFolderPath) + && $mediaDirectory->isDirectory($imageFolderPath) + ) { + $allowedDirectoryPaths[] = $imageFolderPath; + } + } + + return $allowedDirectoryPaths; + } + /** * Return jstree data for given path * @@ -135,39 +201,78 @@ class GetDirectoryTree } /** - * Find parent directory + * Build directory tree recursively for a root folder. * - * @param array $node - * @param array $treeNode - * @param int $level + * @param ReadInterface $mediaDirectory + * @param string $path * @return array */ - private function findParent(array &$node, array &$treeNode, int $level = 0): array + private function buildDirectoryNode(ReadInterface $mediaDirectory, string $path): array { - $nodePathLength = count($node['path_array']); - $treeNodeParentLevel = $nodePathLength - 1; + $node = $this->getDirectoryData($path); + $node['children'] = []; - $result = ['treeNode' => &$treeNode]; - - if ($nodePathLength <= 1 || $level > $treeNodeParentLevel) { - return $result; + foreach ($this->getSubdirectoryPaths($mediaDirectory, $path) as $subdirectoryPath) { + $node['children'][] = $this->buildDirectoryNode($mediaDirectory, $subdirectoryPath); } - foreach ($treeNode['children'] as &$tnode) { - $tNodePathLength = count($tnode['path_array']); - $found = false; - while ($level < $tNodePathLength) { - $found = $node['path_array'][$level] === $tnode['path_array'][$level]; - if ($found) { - $level ++; - } else { - break; - } + return $node; + } + + /** + * Build jstree node for on-demand loading. + * + * @param ReadInterface $mediaDirectory + * @param string $path + * @return array + */ + private function getDirectoryDataForLazyLoad(ReadInterface $mediaDirectory, string $path): array + { + $node = $this->getDirectoryData($path); + $node['children'] = $this->hasSubdirectories($mediaDirectory, $path); + + return $node; + } + + /** + * Return sorted subdirectories for a given folder. + * + * @param ReadInterface $mediaDirectory + * @param string $path + * @return string[] + * @throws ValidatorException + */ + private function getSubdirectoryPaths(ReadInterface $mediaDirectory, string $path): array + { + $subdirectories = []; + + foreach ($mediaDirectory->read($path) as $entryPath) { + if ($mediaDirectory->isDirectory($entryPath) && !$this->isPathExcluded->execute($entryPath)) { + $subdirectories[] = $entryPath; } - if ($found) { - return $this->findParent($node, $tnode, $level); + } + + sort($subdirectories); + + return $subdirectories; + } + + /** + * Check whether path has at least one visible subdirectory. + * + * @param ReadInterface $mediaDirectory + * @param string $path + * @return bool + * @throws ValidatorException + */ + private function hasSubdirectories(ReadInterface $mediaDirectory, string $path): bool + { + foreach ($mediaDirectory->read($path) as $entryPath) { + if ($mediaDirectory->isDirectory($entryPath) && !$this->isPathExcluded->execute($entryPath)) { + return true; } } - return $result; + + return false; } } diff --git a/vendor/magento/module-media-gallery-ui/view/adminhtml/web/js/directory/directories.js b/vendor/magento/module-media-gallery-ui/view/adminhtml/web/js/directory/directories.js index b3f62fc6673..0a63eb8ceab 100644 --- a/vendor/magento/module-media-gallery-ui/view/adminhtml/web/js/directory/directories.js +++ b/vendor/magento/module-media-gallery-ui/view/adminhtml/web/js/directory/directories.js @@ -74,7 +74,7 @@ define([ this.directoryTree().createDirectoryUrl, [this.getNewFolderPath(folderName)] ).then(function () { - this.directoryTree().reloadJsTree().then(function () { + this.directoryTree().reloadJsTree(true).then(function () { this.directoryTree().locateNode(this.getNewFolderPath(folderName)); }.bind(this)); }.bind(this)).fail(function (error) { diff --git a/vendor/magento/module-media-gallery-ui/view/adminhtml/web/js/directory/directoryTree.js b/vendor/magento/module-media-gallery-ui/view/adminhtml/web/js/directory/directoryTree.js index a4d2dff4abf..3f5f2f84c85 100644 --- a/vendor/magento/module-media-gallery-ui/view/adminhtml/web/js/directory/directoryTree.js +++ b/vendor/magento/module-media-gallery-ui/view/adminhtml/web/js/directory/directoryTree.js @@ -26,6 +26,7 @@ define([ createDirectoryUrl: 'media_gallery/directories/create', deleteDirectoryUrl: 'media_gallery/directories/delete', jsTreeReloaded: null, + restoringDirectorySelection: false, modules: { bookmarks: '${ $.bookmarkProvider }', directories: '${ $.name }_directories', @@ -67,10 +68,16 @@ define([ * Render directory tree component. */ renderDirectoryTree: function () { - return this.getJsonTree().then(function (data) { + if (this.isLazyTreeMode()) { + this.createTree(); + + return $.Deferred().resolve().promise(); + } + + return this.getJsonTree(true).then(function (data) { this.createFolderIfNotExists(data).then(function (isFolderCreated) { if (isFolderCreated) { - this.getJsonTree().then(function (newData) { + this.getJsonTree(true).then(function (newData) { this.createTree(newData); }.bind(this)); } else { @@ -129,6 +136,15 @@ define([ return deferred.promise(); }, + /** + * Check if directory tree should be loaded on demand. + * + * @returns {Boolean} + */ + isLazyTreeMode: function () { + return _.isNull(this.getRequestedDirectory()); + }, + /** * Verify if directory exists in array * @@ -218,7 +234,7 @@ define([ this.disableMultiselectBehavior(); $(window).on('reload.MediaGallery', function () { - this.getJsonTree().then(function (data) { + this.getJsonTree(!this.isLazyTreeMode()).then(function (data) { this.createFolderIfNotExists(data).then(function (isCreated) { if (isCreated) { this.renderDirectoryTree().then(function () { @@ -251,10 +267,15 @@ define([ * Verify directory filter on init event, select folder per directory filter state */ updateSelectedDirectory: function () { - var currentFilterPath = this.filterChips().filters.path, + var appliedFilters = this.filterChips().get('applied') || {}, + currentFilterPath = appliedFilters.path || this.filterChips().filters.path, requestedDirectory = this.getRequestedDirectory(), currentTreePath; + if (this.restoringDirectorySelection) { + return; + } + if (_.isUndefined(currentFilterPath)) { this.clearFiltersHandle(); @@ -275,9 +296,128 @@ define([ if (this.folderExistsInTree(currentTreePath)) { this.locateNode(currentTreePath); - } else { - this.selectStorageRoot(); + } else if (_.isString(currentTreePath) && currentTreePath !== '') { + if (!this.isLazyTreeMode()) { + this.selectStorageRoot(); + return; + } + + this.restoringDirectorySelection = true; + this.ensurePathLoaded(currentTreePath) + .done(function (isLoaded) { + if (isLoaded && this.folderExistsInTree(currentTreePath)) { + this.locateNode(currentTreePath); + } else { + this.selectStorageRoot(); + } + }.bind(this)) + .always(function () { + this.restoringDirectorySelection = false; + }.bind(this)); + } + }, + + /** + * Ensure all directory ancestors are loaded in lazy tree mode. + * + * @param {String} path + * @returns {jQuery.Promise} + */ + ensurePathLoaded: function (path) { + var deferred = $.Deferred(), + pathChain = this.getPathChain(path), + index; + + if (!_.isString(path) || path === '') { + deferred.resolve(false); + return deferred.promise(); + } + + if (!this.isLazyTreeMode()) { + deferred.resolve(this.folderExistsInTree(path)); + return deferred.promise(); + } + + index = _.findIndex(pathChain, function (segmentPath) { + return !!this.folderExistsInTree(segmentPath); + }.bind(this)); + + if (index === -1) { + deferred.resolve(false); + return deferred.promise(); + } + + /** + * Open each path segment sequentially so jstree lazy-loads children. + */ + function processNextSegment() { + var segmentPath; + + if (index >= pathChain.length) { + deferred.resolve(this.folderExistsInTree(path)); + return; + } + + segmentPath = pathChain[index]; + + if (!this.folderExistsInTree(segmentPath)) { + deferred.resolve(false); + return; + } + + this.openNodeAsync(segmentPath) + .always(function () { + index++; + processNextSegment.call(this); + }.bind(this)); } + + processNextSegment.call(this); + + return deferred.promise(); + }, + + /** + * Open node and resolve once jstree processes lazy children. + * + * @param {String} path + * @returns {jQuery.Promise} + */ + openNodeAsync: function (path) { + var deferred = $.Deferred(), + tree = $(this.directoryTreeSelector).jstree(true); + + if (!tree || !tree.get_node(path)) { + deferred.resolve(false); + + return deferred.promise(); + } + + tree.open_node(path, function (node, status) { + deferred.resolve(status !== false); + }); + + return deferred.promise(); + }, + + /** + * Convert path string into cumulative segments: + * 'a/b/c' -> ['a', 'a/b', 'a/b/c']. + * + * @param {String} path + * @returns {Array} + */ + getPathChain: function (path) { + var segments = _.filter(path.split('/'), function (segment) { + return segment !== ''; + }), + pathChain = []; + + $.each(segments, function (index) { + pathChain.push(segments.slice(0, index + 1).join('/')); + }); + + return pathChain; }, /** @@ -415,15 +555,27 @@ define([ filters = $.extend(true, filters, applied); filters.path = path; this.filterChips().set('applied', filters); + + if (!_.isUndefined(this.bookmarks()) && _.isFunction(this.bookmarks().store)) { + this.bookmarks().store('current'); + } }, /** * Reload jstree and update jstree events */ - reloadJsTree: function () { + reloadJsTree: function (loadWholeTree) { var deferred = $.Deferred(); - this.getJsonTree().then(function (data) { + if (this.isLazyTreeMode() && !loadWholeTree) { + $(this.directoryTreeSelector).jstree(true).refresh(false, true); + this.setJsTreeReloaded(true); + deferred.resolve(); + + return deferred.promise(); + } + + this.getJsonTree(true).then(function (data) { $(this.directoryTreeSelector).jstree(true).settings.core.data = data; $(this.directoryTreeSelector).jstree(true).refresh(false, true); this.setJsTreeReloaded(true); @@ -436,13 +588,17 @@ define([ /** * Get json data for jstree */ - getJsonTree: function () { - var deferred = $.Deferred(); + getJsonTree: function (loadWholeTree) { + var deferred = $.Deferred(), + requestData = { + loadWholeTree: loadWholeTree ? 1 : 0 + }; $.ajax({ url: this.getDirectoryTreeUrl, type: 'GET', dataType: 'json', + data: requestData, /** * Success handler for request @@ -474,6 +630,28 @@ define([ * @param {Array} data */ createTree: function (data) { + var treeData = data; + + if (this.isLazyTreeMode()) { + treeData = { + url: this.getDirectoryTreeUrl, + type: 'GET', + + /** + * Return data payload for on-demand loading. + * + * @param {Object} node + * @returns {Object} + */ + data: function (node) { + return { + path: node.id, + loadWholeTree: 0 + }; + } + }; + } + // jscs:disable requireCamelCaseOrUpperCaseIdentifiers $(this.directoryTreeSelector).jstree({ plugins: [], @@ -482,7 +660,7 @@ define([ cascade: '' }, core: { - data: data, + data: treeData, check_callback: true, themes: { dots: false