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