import type { SupabaseClient } from "@supabase/supabase-js"; import type { DocumentRecord, DocumentNode } from "@/lib/documents"; import type { SidebarSectionId, TrashRecord } from "@/components/sidebar/types"; import type { Database } from "@/types/supabase"; import { buildDocumentTree } from "@/lib/documents"; type TypedClient = SupabaseClient; export interface SidebarSectionSnapshot { id: SidebarSectionId; title: string; icon?: string; nodes: DocumentNode[]; } export interface FlattenedTreeNode { node: DocumentNode; depth: number; parentId: string | null; } const SECTION_META: Record = { starred: { title: "星标置顶", icon: "star" }, public: { title: "公共页面", icon: "globe" }, shared: { title: "共享页面", icon: "users" }, private: { title: "私有 / 我的页面", icon: "lock" }, templates: { title: "模板中心", icon: "grid" }, }; export async function fetchSidebarDataset( client: TypedClient, workspaceId: string, ): Promise<{ documents: DocumentRecord[]; trashedDocuments: TrashRecord[] }> { const { data: documentRows, error } = await client .from("documents") .select( "id,title,parent_id,sort_order,is_starred,access_scope,is_template,created_at,updated_at,workspace_id", ) .eq("workspace_id", workspaceId) .is("deleted_at", null) .order("sort_order", { ascending: true, nullsFirst: false }) .order("created_at", { ascending: true }); if (error) { throw new Error(`获取工作空间文档列表失败:${error.message}`); } const documents: DocumentRecord[] = (documentRows ?? []).map((row) => ({ access_scope: (row.access_scope ?? "private") as DocumentRecord["access_scope"], id: row.id, workspace_id: row.workspace_id, title: row.title ?? "无标题", parent_id: row.parent_id, sort_order: row.sort_order, is_starred: row.is_starred, is_template: row.is_template ?? false, created_at: row.created_at ?? "", updated_at: row.updated_at ?? null, })); const { data: trashRows, error: trashError } = await client .from("documents") .select("id,title,parent_id,deleted_at,access_scope") .eq("workspace_id", workspaceId) .not("deleted_at", "is", null) .order("deleted_at", { ascending: false }) .limit(100); if (trashError) { throw new Error(`获取垃圾桶内容失败:${trashError.message}`); } const trashedDocuments: TrashRecord[] = (trashRows ?? []).map((row) => ({ id: row.id, title: row.title, parent_id: row.parent_id, deleted_at: row.deleted_at!, access_scope: (row.access_scope ?? "private") as DocumentRecord["access_scope"], })); return { documents, trashedDocuments, }; } type NodePredicate = (node: DocumentNode) => boolean; function projectTree(nodes: DocumentNode[], predicate: NodePredicate): DocumentNode[] { const result: DocumentNode[] = []; nodes.forEach((node) => { const projectedChildren = projectTree(node.children, predicate); if (predicate(node)) { result.push({ ...node, children: projectedChildren, }); } else { result.push(...projectedChildren); } }); return result; } export function flattenDocumentTree( nodes: DocumentNode[], expanded: Set, depth = 0, parentId: string | null = null, ): FlattenedTreeNode[] { const list: FlattenedTreeNode[] = []; nodes.forEach((node) => { list.push({ node, depth, parentId }); if (node.children.length > 0 && expanded.has(node.id)) { list.push(...flattenDocumentTree(node.children, expanded, depth + 1, node.id)); } }); return list; } export function buildSidebarSections(records: DocumentRecord[]): SidebarSectionSnapshot[] { const tree = buildDocumentTree(records); return buildSidebarSectionsFromTree(tree); } export function buildSidebarSectionsFromTree(tree: DocumentNode[]): SidebarSectionSnapshot[] { const sections: Array<{ id: SidebarSectionId; predicate: NodePredicate }> = [ { id: "starred", predicate: (node) => Boolean(node.is_starred) }, { id: "public", predicate: (node) => node.access_scope === "public" }, { id: "shared", predicate: (node) => node.access_scope === "shared" }, { id: "private", predicate: (node) => node.access_scope === "private" }, { id: "templates", predicate: (node) => node.is_template }, ]; return sections.map(({ id, predicate }) => ({ id, title: SECTION_META[id].title, icon: SECTION_META[id].icon, nodes: projectTree(tree, predicate), })); }