From 64d33fb1d088d0262f0bb10e5d1c6f82ef5b6e4f Mon Sep 17 00:00:00 2001 From: "tao.chen" <93983997+taochen-ct@users.noreply.github.com> Date: Tue, 1 Sep 2026 11:29:07 +0800 Subject: [PATCH] fix: loadChildren + loadScripts + loadDataResources --- .../features/platform/state/scriptsSlice.ts | 42 ++++++++++++++----- .../app/features/platform/state/treeSlice.ts | 26 ++++++++---- frontend/app/features/platform/state/types.ts | 4 ++ .../platform/state/useScriptWorkspaceStore.ts | 1 + 4 files changed, 56 insertions(+), 17 deletions(-) diff --git a/frontend/app/features/platform/state/scriptsSlice.ts b/frontend/app/features/platform/state/scriptsSlice.ts index 08555a8..d0f9ff5 100644 --- a/frontend/app/features/platform/state/scriptsSlice.ts +++ b/frontend/app/features/platform/state/scriptsSlice.ts @@ -283,28 +283,41 @@ export const createScriptsSlice: StateCreator< loadDataResources: async (parentPath = "", ownerUserId) => { const api = requireApi(); + const cacheKey = ownerCacheKey(ownerUserId, parentPath); + // 命中缓存:listResources 是非递归的,同一 (owner, parent_path) 拉过的 + // 内容不会自己变化;省去 toggleExpanded 重复展开同一目录时的网络往返。 + if (get().loadedDataResourcePaths.has(cacheKey)) return; set({ dataResourcesLoading: true }); try { const list = await api.listResources(parentPath, { ownerUserId }); const fresh = Array.isArray(list) ? list : []; set((state) => { - // 按 owner 范围合并:丢弃该 owner 的旧资源再并入 fresh(fresh 覆盖 - // 同 id)。owner 缺省=我,故根加载/刷新我的数据时替换我的一级资源。 + // 按 (owner, parent_path) 局部替换:丢掉该 owner 在 parentPath 下的 + // 旧条目,保留该 owner 在其它路径下的条目,再并入 fresh。 + // 这样 toggleExpanded 在子目录展开时按需拉取不会把根已加载的数据 + // 资源擦掉(修"根加载后子目录展开丢数据 / 子目录数据本来不显示")。 const targetOwner = ownerUserId ?? getCurrentUserId() ?? null; - const kept = state.dataResources.filter( - (r) => r.owner_user_id !== targetOwner, - ); + const kept = state.dataResources.filter((r) => { + if (r.owner_user_id !== targetOwner) return true; + return parentPathOf(r.jupyter_accessible_path) !== parentPath; + }); const byId = new Map(kept.map((r) => [r.resource_id, r])); for (const item of fresh) byId.set(item.resource_id, item); - return { dataResources: Array.from(byId.values()) }; + const nextLoaded = new Set(state.loadedDataResourcePaths); + nextLoaded.add(cacheKey); + return { + dataResources: Array.from(byId.values()), + loadedDataResourcePaths: nextLoaded, + }; }); } catch { set((state) => { const targetOwner = ownerUserId ?? getCurrentUserId() ?? null; return { - dataResources: state.dataResources.filter( - (r) => r.owner_user_id !== targetOwner, - ), + dataResources: state.dataResources.filter((r) => { + if (r.owner_user_id !== targetOwner) return true; + return parentPathOf(r.jupyter_accessible_path) !== parentPath; + }), }; }); } finally { @@ -336,4 +349,13 @@ export const createScriptsSlice: StateCreator< } }, }; -}; \ No newline at end of file +}; + +// 提取 jupyter-accessible 路径的父目录;用于 `loadDataResources` 局部替换时 +// 判断一条缓存资源是否落在目标 parent_path 下(list-resources 按 parent_path +// 精确匹配,非递归)。 +function parentPathOf(path: string): string { + const parts = path.split("/"); + parts.pop(); + return parts.join("/"); +} \ No newline at end of file diff --git a/frontend/app/features/platform/state/treeSlice.ts b/frontend/app/features/platform/state/treeSlice.ts index 2b88c9c..79a5c3a 100644 --- a/frontend/app/features/platform/state/treeSlice.ts +++ b/frontend/app/features/platform/state/treeSlice.ts @@ -1,14 +1,16 @@ // ---- treeSlice ---- // // 拥有 expandedPaths / loadingChildrenPaths / loadedChildPaths / -// loadedScriptPaths / loadingScriptPaths (5 个 directory-tree 缓存集合)。 -// 负责 toggleExpanded 和 loadChildren。 +// loadedScriptPaths / loadingScriptPaths / loadedDataResourcePaths +// (6 个 directory-tree 缓存集合)。负责 toggleExpanded 和 loadChildren。 // // 注意: // - loadedScriptPaths/loadingScriptPaths 也由 scriptsSlice 写 (loadScripts / // loadOwnerGroup),但 ownership 在 treeSlice 里 (因为是 cache set,不是数据) -// - scriptsSlice.load 也会写这俩,所以这里只保留这两个 setter (toggleExpanded -// 也要写 expandedPaths)。 +// - loadedDataResourcePaths 由 scriptsSlice.loadDataResources 写(同样的 cache +// 不放数据原则),toggleExpanded 在真实目录分支按需触发。 +// - scriptsSlice.load 也会写 cached script paths,所以这里只保留 toggleExpanded +// (写 expandedPaths) 和 loadChildren (写目录缓存)。 import type { StateCreator } from "zustand"; @@ -34,6 +36,7 @@ export const createTreeSlice: StateCreator< loadedChildPaths: new Set(), loadedScriptPaths: new Set(), loadingScriptPaths: new Set(), + loadedDataResourcePaths: new Set(), }; return { @@ -110,15 +113,24 @@ export const createTreeSlice: StateCreator< } } else { // 真实目录展开:loadChildren(owner 限定的显式目录行)+ loadScripts - // 并行。两者都 idempotent + 缓存;ownerUserId 缺省=我。他人目录同样 - // 调 loadChildren(owner) 拉取其目录结构,否则嵌套子目录无法被发现 - // (list_scripts 非递归,只能看到直接子脚本)。 + // + loadDataResources 并行。三者都 idempotent + 缓存;ownerUserId + // 缺省=我。他人目录同样调 loadChildren(owner) 拉取其目录结构,否则 + // 嵌套子目录无法被发现(list_scripts 非递归,只能看到直接子脚本)。 + // 数据资源也是非递归的——不按需拉取,子目录里的 csv/xlsx/json 等 + // 都不会出现(修"目录树子目录里的数据文件不显示")。 if (!state.loadedChildPaths.has(ownerCacheKey(ownerUserId, loadPath))) { void get().loadChildren(loadPath, ownerUserId); } if (!state.loadedScriptPaths.has(ownerCacheKey(ownerUserId, loadPath))) { void get().loadScripts(loadPath, ownerUserId); } + if ( + !state.loadedDataResourcePaths.has( + ownerCacheKey(ownerUserId, loadPath), + ) + ) { + void get().loadDataResources(loadPath, ownerUserId); + } } } set({ expandedPaths: next }); diff --git a/frontend/app/features/platform/state/types.ts b/frontend/app/features/platform/state/types.ts index a4fb073..df21320 100644 --- a/frontend/app/features/platform/state/types.ts +++ b/frontend/app/features/platform/state/types.ts @@ -62,6 +62,10 @@ export type TreeSliceState = { // `${owner_user_id}:${parent_path}` (see ownerCacheKey). loadedScriptPaths: Set; loadingScriptPaths: Set; + // 与 loadedScriptPaths 同形:按 (owner, parent_path) 缓存已拉取的数据资源, + // 让 toggleExpanded 在子目录展开时也按需请求 listDataResources,而不是 + // 只在根加载一次(修"子目录下的数据文件不展示")。 + loadedDataResourcePaths: Set; }; export type TreeSliceActions = { diff --git a/frontend/app/features/platform/state/useScriptWorkspaceStore.ts b/frontend/app/features/platform/state/useScriptWorkspaceStore.ts index fc5534b..9dd91ed 100644 --- a/frontend/app/features/platform/state/useScriptWorkspaceStore.ts +++ b/frontend/app/features/platform/state/useScriptWorkspaceStore.ts @@ -112,6 +112,7 @@ const INITIAL: ScriptWorkspaceState = { loadedChildPaths: new Set(), loadedScriptPaths: new Set(), loadingScriptPaths: new Set(), + loadedDataResourcePaths: new Set(), // selectionSlice selectedId: null, openTabIds: [],