63 lines
3.6 KiB
TypeScript
63 lines
3.6 KiB
TypeScript
/**
|
|
* 工作区画布目录的读取与展示树派生。完整分页包含子分区,随后用parent_board_id构建侧栏关系。
|
|
* 显示树可保守容纳孤儿/循环记录,不据此修改服务端owner,也不替代文档模型中的canvas树校验。
|
|
*/
|
|
import { matchesRef, type Board } from "./workspace.ts";
|
|
|
|
/** 页面注入的通用JSON请求能力,传入signal供导航/卸载取消。 */
|
|
type Request = <T>(path: string, options?: RequestInit) => Promise<T>;
|
|
/** Include children and finish pagination before building ownership paths. */
|
|
/** 分页取齐board后返回按ID去重的列表,后出现的同ID记录更新内容。
|
|
* next_offset必须严格前进且安全,seen防重复游标,不能只拿首页就推断某canvas没有父分区。 */
|
|
export async function loadBoardDirectory(request: Request, workspace: string, signal?: AbortSignal) {
|
|
const boards = new Map<string, Board>();
|
|
let offset = 0;
|
|
const seen = new Set<number>();
|
|
while (true) {
|
|
if (seen.has(offset)) throw new Error("invalid_pagination");
|
|
seen.add(offset);
|
|
const page = await request<{ items: Board[]; next_offset?: number | null }>(`workspaces/${encodeURIComponent(workspace)}/materials?kind=board&sort=modified&include_subgraphs=true&limit=100&offset=${offset}`, { signal });
|
|
for (const board of page.items) boards.set(board.id, board);
|
|
if (page.next_offset == null) return [...boards.values()];
|
|
if (!Number.isSafeInteger(page.next_offset) || page.next_offset <= offset) throw new Error("invalid_pagination");
|
|
offset = page.next_offset;
|
|
}
|
|
}
|
|
/** 侧栏派生分支:保留原board记录引用,children是本次展示树容器。 */
|
|
export type BoardBranch = { board: Board; children: BoardBranch[] };
|
|
/** 每条记录沿父链检查循环;父缺失或成环时作为展示根,避免递归挂载成无限树。
|
|
* 同层顺序沿用输入列表,不在此另做排序或写回parent_board_id。 */
|
|
export function boardDirectory(boards: Board[]): BoardBranch[] {
|
|
const entries = new Map(boards.map((board) => [board.id, { board, children: [] as BoardBranch[] }]));
|
|
const roots: BoardBranch[] = [];
|
|
for (const entry of entries.values()) {
|
|
const seen = new Set([entry.board.id]);
|
|
let parent = entry.board.parent_board_id;
|
|
let cyclic = false;
|
|
while (parent && entries.has(parent)) {
|
|
if (seen.has(parent)) { cyclic = true; break; }
|
|
seen.add(parent); parent = entries.get(parent)!.board.parent_board_id;
|
|
}
|
|
const owner = entry.board.parent_board_id && entries.get(entry.board.parent_board_id);
|
|
if (owner && !cyclic) owner.children.push(entry); else roots.push(entry);
|
|
}
|
|
return roots;
|
|
}
|
|
/** 按ID或slug找到当前项,包含自身向上收集祖先ID,重复ID即停,供侧栏自动展开。 */
|
|
export function activeBoardAncestors(boards: Board[], ref?: string) {
|
|
const result = new Set<string>();
|
|
let item = boards.find((board) => matchesRef(board, ref));
|
|
while (item && !result.has(item.id)) { result.add(item.id); item = boards.find((board) => board.id === item!.parent_board_id); }
|
|
return result;
|
|
}
|
|
/** 忽略首尾空白并做本地小写匹配;父标题命中保留整子树,否则保留命中后代及其路径祖先。 */
|
|
export function filterBoardDirectory(branches: BoardBranch[], query: string): BoardBranch[] {
|
|
const term = query.trim().toLocaleLowerCase();
|
|
if (!term) return branches;
|
|
return branches.flatMap((branch) => {
|
|
if (branch.board.title.toLocaleLowerCase().includes(term)) return [branch];
|
|
const children = filterBoardDirectory(branch.children, term);
|
|
return children.length ? [{ ...branch, children }] : [];
|
|
});
|
|
}
|