From 595c04b3ba690ea999deaaf1ddae1c3884244ee5 Mon Sep 17 00:00:00 2001 From: jerasmus Date: Thu, 14 Aug 2025 09:58:39 +0200 Subject: [PATCH 1/6] Implement pagination in the file tree browser Adds a Show more button to request next pages of the list --- .../components/tree_list.vue | 43 +++++++++++++++---- .../repository/file_tree_browser/utils.js | 14 ++++++ .../vue_shared/components/file_row.vue | 12 +++++- .../components/tree_list_spec.js | 33 ++++++++++++-- .../file_tree_browser/utils_spec.js | 32 +++++++++++++- .../vue_shared/components/file_row_spec.js | 29 ++++++++++++- 6 files changed, 149 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/repository/file_tree_browser/components/tree_list.vue b/app/assets/javascripts/repository/file_tree_browser/components/tree_list.vue index ab7567f0fdd0d7..6a18f9d7bc16e1 100644 --- a/app/assets/javascripts/repository/file_tree_browser/components/tree_list.vue +++ b/app/assets/javascripts/repository/file_tree_browser/components/tree_list.vue @@ -14,7 +14,7 @@ import { FOCUS_FILE_TREE_BROWSER_FILTER_BAR, keysFor } from '~/behaviors/shortcu import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle'; import { Mousetrap } from '~/lib/mousetrap'; import Shortcut from '~/behaviors/shortcuts/shortcut.vue'; -import { normalizePath, dedupeByFlatPathAndId } from '../utils'; +import { normalizePath, dedupeByFlatPathAndId, generateShowMoreItem } from '../utils'; export default { ROW_HEIGHT: 32, @@ -109,8 +109,8 @@ export default { buildList(path, level) { const contents = this.getDirectoryContents(path); return this.processDirectories(contents.trees, path, level) - .concat(this.processFiles(contents.blobs, level)) - .concat(this.processSubmodules(contents.submodules, level)); + .concat(this.processFiles(contents.blobs, path, level)) + .concat(this.processSubmodules(contents.submodules, path, level)); }, processDirectories(trees = [], path, level) { const directoryList = []; @@ -129,6 +129,9 @@ export default { isCurrentPath: this.isCurrentPath(treePath), }); + if (this.shouldRenderShowMore(treePath, path)) + directoryList.push(generateShowMoreItem(tree.id, path, level)); + // Recursively add children for expanded directories if (this.expandedPathsMap[treePath] && !this.isDirectoryLoading(treePath)) { directoryList.push(...this.buildList(treePath, level + 1)); @@ -137,7 +140,7 @@ export default { return directoryList; }, - processFiles(blobs = [], level) { + processFiles(blobs = [], path, level) { const filesList = []; blobs.forEach((blob, index) => { @@ -152,11 +155,14 @@ export default { level, isCurrentPath: this.isCurrentPath(blobPath), }); + + if (this.shouldRenderShowMore(blobPath, path)) + filesList.push(generateShowMoreItem(blob.id, path, level)); }); return filesList; }, - processSubmodules(submodules = [], level) { + processSubmodules(submodules = [], path, level) { const submodulesList = []; submodules.forEach((submodule, index) => { @@ -170,6 +176,9 @@ export default { level, isCurrentPath: this.isCurrentPath(submodulePath), }); + + if (this.shouldRenderShowMore(submodulePath, path)) + submodulesList.push(generateShowMoreItem(submodule.id, path, level)); }); return submodulesList; @@ -177,8 +186,9 @@ export default { async fetchDirectory(dirPath) { const path = normalizePath(dirPath); const apiPath = path === '/' ? path : path.substring(1); + const nextPageCursor = this.directoriesCache[path]?.pageInfo?.endCursor || ''; - if (this.directoriesCache[path] || this.loadingPathsMap[path]) return; + if ((this.directoriesCache[path] && !nextPageCursor) || this.loadingPathsMap[path]) return; this.loadingPathsMap = { ...this.loadingPathsMap, [path]: true }; @@ -191,7 +201,7 @@ export default { ref: currentRef, refType: getRefType(refType), path: apiPath, - nextPageCursor: '', + nextPageCursor, pageSize: TREE_PAGE_SIZE, }, }); @@ -203,10 +213,16 @@ export default { blobs: dedupeByFlatPathAndId(treeData.blobs.nodes), submodules: dedupeByFlatPathAndId(treeData.submodules.nodes), }; + const cached = this.directoriesCache[path] || { trees: [], blobs: [], submodules: [] }; this.directoriesCache = { ...this.directoriesCache, - [path]: directoryContents, + [path]: { + trees: [...cached.trees, ...directoryContents.trees], + blobs: [...cached.blobs, ...directoryContents.blobs], + submodules: [...cached.submodules, ...directoryContents.submodules], + pageInfo: project?.repository?.paginatedTree?.pageInfo, + }, }; } catch (error) { createAlert({ @@ -218,6 +234,8 @@ export default { const newMap = { ...this.loadingPathsMap }; delete newMap[path]; this.loadingPathsMap = newMap; + await this.$nextTick(); + this.$refs.scroller?.handleResize?.(); // ensures the scroller is properly updated with new data } }, @@ -270,6 +288,14 @@ export default { getDirectoryContents(path) { return this.directoriesCache[normalizePath(path)] || { trees: [], blobs: [], submodules: [] }; }, + shouldRenderShowMore(itemPath, parentPath) { + const cached = this.directoriesCache[parentPath]; + if (!cached) return false; + + const { trees, blobs, submodules, pageInfo } = cached; + const lastItemPath = normalizePath([...trees, ...blobs, ...submodules].at(-1)?.path); + return itemPath === lastItemPath && pageInfo?.hasNextPage; + }, triggerFocusFilterBar() { const filterBar = this.$refs.filterInput; if (filterBar && filterBar.$el) { @@ -341,6 +367,7 @@ export default { class="!gl-mx-0" truncate-middle @clickTree="navigateTo(item.path)" + @showMore="fetchDirectory(item.parentPath)" /> diff --git a/app/assets/javascripts/repository/file_tree_browser/utils.js b/app/assets/javascripts/repository/file_tree_browser/utils.js index 540ac4748fe242..5ab550bc9e38d6 100644 --- a/app/assets/javascripts/repository/file_tree_browser/utils.js +++ b/app/assets/javascripts/repository/file_tree_browser/utils.js @@ -28,3 +28,17 @@ export const dedupeByFlatPathAndId = (arr) => { return true; }); }; + +/** + * Generates a show more item for the file-row component. + * @param {string} id - Unique id for the entry + * @param {string} parentPath - The path of the parent directory + * @param {number} level - Level used for indentation in rendering the tree + * @returns {Object} Show more item object with id, level, parentPath, and isShowMore properties + */ +export const generateShowMoreItem = (id, parentPath, level) => ({ + id: `${id}-show-more`, + level, + parentPath, + isShowMore: true, +}); diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue index d28106894671f2..a5d9520ce6693d 100644 --- a/app/assets/javascripts/vue_shared/components/file_row.vue +++ b/app/assets/javascripts/vue_shared/components/file_row.vue @@ -1,5 +1,5 @@