139 lines
5.6 KiB
TypeScript
139 lines
5.6 KiB
TypeScript
// ---- treeSlice ----
|
||
//
|
||
// 拥有 expandedPaths / loadingChildrenPaths / loadedChildPaths /
|
||
// loadedScriptPaths / loadingScriptPaths / loadedDataResourcePaths
|
||
// (6 个 directory-tree 缓存集合)。负责 toggleExpanded 和 loadChildren。
|
||
//
|
||
// 注意:
|
||
// - loadedScriptPaths/loadingScriptPaths 也由 scriptsSlice 写 (loadScripts /
|
||
// loadOwnerGroup),但 ownership 在 treeSlice 里 (因为是 cache set,不是数据)
|
||
// - loadedDataResourcePaths 由 scriptsSlice.loadDataResources 写(同样的 cache
|
||
// 不放数据原则),toggleExpanded 在真实目录分支按需触发。
|
||
// - scriptsSlice.load 也会写 cached script paths,所以这里只保留 toggleExpanded
|
||
// (写 expandedPaths) 和 loadChildren (写目录缓存)。
|
||
|
||
import type { StateCreator } from "zustand";
|
||
|
||
import {
|
||
getCurrentUserId,
|
||
ownerCacheKey,
|
||
pushToast,
|
||
requireApi,
|
||
} from "./helpers";
|
||
|
||
import type { TreeSliceActions, TreeSliceState } from "./types";
|
||
import type { ScriptWorkspaceStore } from "./useScriptWorkspaceStore";
|
||
|
||
export const createTreeSlice: StateCreator<
|
||
ScriptWorkspaceStore,
|
||
[],
|
||
[],
|
||
TreeSliceState & TreeSliceActions
|
||
> = (set, get) => {
|
||
const initial: TreeSliceState = {
|
||
expandedPaths: new Set<string>(),
|
||
loadingChildrenPaths: new Set<string>(),
|
||
loadedChildPaths: new Set<string>(),
|
||
loadedScriptPaths: new Set<string>(),
|
||
loadingScriptPaths: new Set<string>(),
|
||
loadedDataResourcePaths: new Set<string>(),
|
||
};
|
||
|
||
return {
|
||
...initial,
|
||
|
||
loadChildren: async (parentPath, ownerUserId) => {
|
||
const api = requireApi();
|
||
const cacheKey = ownerCacheKey(ownerUserId, parentPath);
|
||
if (get().loadedChildPaths.has(cacheKey)) return;
|
||
const next = new Set(get().loadingChildrenPaths);
|
||
next.add(cacheKey);
|
||
set({ loadingChildrenPaths: next });
|
||
try {
|
||
const children = await api.listWorkspaceDirectories(parentPath, ownerUserId);
|
||
set((state) => {
|
||
const nextLoaded = new Set(state.loadedChildPaths);
|
||
nextLoaded.add(cacheKey);
|
||
// 丢弃该 owner 该 parent 下的旧目录行,再并入 fresh(按 (owner,path) 去重)。
|
||
const targetOwner = ownerUserId ?? getCurrentUserId() ?? null;
|
||
const trimmed = state.directories.filter(
|
||
(d) =>
|
||
!(d.owner_user_id === targetOwner && d.parent_path === parentPath),
|
||
);
|
||
const byId = new Map(
|
||
trimmed.map((d) => [`${d.owner_user_id}:${d.path}`, d]),
|
||
);
|
||
for (const c of children) byId.set(`${c.owner_user_id}:${c.path}`, c);
|
||
return {
|
||
directories: Array.from(byId.values()),
|
||
loadedChildPaths: nextLoaded,
|
||
loadingChildrenPaths: new Set(
|
||
[...state.loadingChildrenPaths].filter((p) => p !== cacheKey),
|
||
),
|
||
};
|
||
});
|
||
} catch (error) {
|
||
set((state) => ({
|
||
loadingChildrenPaths: new Set(
|
||
[...state.loadingChildrenPaths].filter((p) => p !== cacheKey),
|
||
),
|
||
}));
|
||
pushToast(
|
||
"error",
|
||
error instanceof Error ? error.message : "目录加载失败",
|
||
);
|
||
}
|
||
},
|
||
|
||
toggleExpanded: async (path, loadPath, ownerUserId) => {
|
||
const state = get();
|
||
const isOpen = state.expandedPaths.has(path);
|
||
const next = new Set(state.expandedPaths);
|
||
if (isOpen) {
|
||
next.delete(path);
|
||
} else {
|
||
next.add(path);
|
||
const me = getCurrentUserId();
|
||
// 用 `loadPath === undefined` 区分分组头与真实目录,而不是用
|
||
// `path.startsWith("__group__")`:目录的 expandKey 是
|
||
// `${groupKey}/${dir.path}` 即 `__group__<owner>/dir`,同样以
|
||
// `__group__` 开头,前缀判断会把子目录点击误当成分组头,导致
|
||
// 既不调 loadChildren 也不调 loadScripts("子目录点击不触发接口")。
|
||
// 分组头 always 传 loadPath=undefined;目录 always 传 loadPath=dir.path。
|
||
if (loadPath === undefined) {
|
||
// 分组头:他人分组首次展开 → loadOwnerGroup 按需拉取其根级可见
|
||
// 脚本+数据+目录(守门去重)。仅翻转 expand;真实子目录的懒加载
|
||
// 由目录分支(loadPath !== undefined)负责。
|
||
if (
|
||
ownerUserId &&
|
||
ownerUserId !== me &&
|
||
!state.loadedOwnerGroups.has(ownerUserId)
|
||
) {
|
||
void get().loadOwnerGroup(ownerUserId);
|
||
}
|
||
} else {
|
||
// 真实目录展开:loadChildren(owner 限定的显式目录行)+ loadScripts
|
||
// + 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 });
|
||
},
|
||
};
|
||
}; |