2025-11-23 10:55:04 +08:00
|
|
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
2026-01-02 07:25:50 +08:00
|
|
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
2025-11-23 10:55:04 +08:00
|
|
|
import type { DocumentRecord, DocumentNode } from "@/lib/documents";
|
|
|
|
|
import type { SidebarSectionId, TrashRecord } from "@/components/sidebar/types";
|
|
|
|
|
import type { Database } from "@/types/supabase";
|
2026-01-02 07:25:50 +08:00
|
|
|
import type { MediaAsset } from "@/types/media";
|
2025-11-23 10:55:04 +08:00
|
|
|
import { buildDocumentTree } from "@/lib/documents";
|
|
|
|
|
|
|
|
|
|
type TypedClient = SupabaseClient<Database>;
|
|
|
|
|
|
2026-01-02 07:25:50 +08:00
|
|
|
export interface SidebarDataset {
|
|
|
|
|
documents: DocumentRecord[];
|
|
|
|
|
trashedDocuments: TrashRecord[];
|
|
|
|
|
/**
|
|
|
|
|
* 仅依据远端 mindmap_data 判定的页面 id 列表;本地文件检测由服务器侧辅助完成
|
|
|
|
|
*/
|
|
|
|
|
mindmapDocs: string[];
|
|
|
|
|
mediaAssets?: MediaAsset[];
|
2025-11-23 10:55:04 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchSidebarDataset(
|
|
|
|
|
client: TypedClient,
|
|
|
|
|
workspaceId: string,
|
2026-01-02 07:25:50 +08:00
|
|
|
): Promise<SidebarDataset> {
|
2025-11-23 10:55:04 +08:00
|
|
|
const { data: documentRows, error } = await client
|
|
|
|
|
.from("documents")
|
|
|
|
|
.select(
|
2026-01-02 07:25:50 +08:00
|
|
|
"id,title,parent_id,sort_order,is_starred,access_scope,is_template,created_at,updated_at,workspace_id,mindmap_data",
|
2025-11-23 10:55:04 +08:00
|
|
|
)
|
|
|
|
|
.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"],
|
|
|
|
|
}));
|
|
|
|
|
|
2026-01-02 07:25:50 +08:00
|
|
|
const mindmapDocs =
|
|
|
|
|
documentRows?.filter((row) => row.mindmap_data != null).map((row) => row.id) ?? [];
|
|
|
|
|
|
|
|
|
|
const { data: assetRows, error: assetError } = await client
|
|
|
|
|
.from("media_assets")
|
|
|
|
|
.select("*")
|
|
|
|
|
.eq("workspace_id", workspaceId)
|
|
|
|
|
.order("created_at", { ascending: false });
|
|
|
|
|
|
|
|
|
|
if (assetError) {
|
|
|
|
|
throw new Error(`获取附件列表失败:${assetError.message}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const mediaAssets: MediaAsset[] = (assetRows ?? []) as MediaAsset[];
|
|
|
|
|
|
2025-11-23 10:55:04 +08:00
|
|
|
return {
|
|
|
|
|
documents,
|
|
|
|
|
trashedDocuments,
|
2026-01-02 07:25:50 +08:00
|
|
|
mindmapDocs,
|
|
|
|
|
mediaAssets,
|
2025-11-23 10:55:04 +08:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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<string>,
|
|
|
|
|
depth = 0,
|
|
|
|
|
parentId: string | null = null,
|
2026-01-02 07:25:50 +08:00
|
|
|
): Array<{ node: DocumentNode; depth: number; parentId: string | null }> {
|
|
|
|
|
const list: Array<{ node: DocumentNode; depth: number; parentId: string | null }> = [];
|
2025-11-23 10:55:04 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-02 07:25:50 +08:00
|
|
|
const SECTION_META: Record<SidebarSectionId, { title: string; icon: string }> = {
|
|
|
|
|
starred: { title: "星标置顶", icon: "star" },
|
|
|
|
|
public: { title: "公共页面", icon: "globe" },
|
|
|
|
|
shared: { title: "共享页面", icon: "users" },
|
|
|
|
|
private: { title: "私有 / 我的页面", icon: "lock" },
|
|
|
|
|
templates: { title: "模板中心", icon: "grid" },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type SidebarSectionSnapshot = {
|
|
|
|
|
id: SidebarSectionId;
|
|
|
|
|
title: string;
|
|
|
|
|
icon?: string;
|
|
|
|
|
nodes: DocumentNode[];
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-23 10:55:04 +08:00
|
|
|
export function buildSidebarSections(records: DocumentRecord[]): SidebarSectionSnapshot[] {
|
|
|
|
|
const tree = buildDocumentTree(records);
|
|
|
|
|
return buildSidebarSectionsFromTree(tree);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-02 07:25:50 +08:00
|
|
|
export function buildSidebarSectionsFromTree(
|
|
|
|
|
tree: DocumentNode[],
|
|
|
|
|
): SidebarSectionSnapshot[] {
|
2025-11-23 10:55:04 +08:00
|
|
|
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),
|
|
|
|
|
}));
|
|
|
|
|
}
|