Files
model-platform/frontend/app/features/platform/state/treeSlice.ts
T

139 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ---- 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 {
// 真实目录展开:loadChildrenowner 限定的显式目录行)+ 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 });
},
};
};