feat: 收口 tree-first graph 主链与前端测试修复

This commit is contained in:
lix-2026
2026-04-18 05:43:49 +08:00
parent d8de820d93
commit d3876e56eb
33 changed files with 2421 additions and 345 deletions
@@ -67,13 +67,19 @@ import type { MediaAsset } from "@/types/media";
import { AssetContextMenu } from "@/components/sidebar/asset-context-menu";
import { ASSETS_CHANGED_EVENT, DOCUMENTS_CHANGED_EVENT, emitAssetsChanged, emitAssetsRestored, emitDocumentsChanged } from "@/lib/events";
import { getMnoteRuntimeConfig } from "@/lib/runtime-config";
import { useSidebarTreeStream } from "@/lib/tree-stream/use-sidebar-tree-stream";
import { DocumentShareDialog } from "@/components/sharing/document-share-dialog";
import { api } from "@/lib/convex/api";
import { GroupManagerDialog } from "@/components/groups/group-manager-dialog";
import {
copyTreeCommand,
createDocumentCommand,
deleteDocumentCommand,
embedDocumentCommand,
moveDocumentCommand,
renameDocumentCommand,
restoreDocumentCommand,
purgeDocumentCommand,
} from "@/lib/documents/tree-command-client";
const TOP_BUTTONS = [
@@ -149,16 +155,18 @@ export function Sidebar({ initialData }: SidebarProps) {
// Convex 模式专用组件 - 只调用 Convex hooks
function SidebarConvex({ initialData }: SidebarProps) {
const sidebarData = useSidebarData(initialData);
return <SidebarContent initialData={initialData} sidebarQuery={sidebarData} />;
const treeStream = useSidebarTreeStream(initialData);
return <SidebarContent initialData={initialData} sidebarQuery={sidebarData} treeStream={treeStream} />;
}
// 共享的 UI 内容组件 - 包含所有现有的 Sidebar 逻辑
interface SidebarContentProps {
initialData: SidebarInitialData;
sidebarQuery: SidebarDataResult;
treeStream: ReturnType<typeof useSidebarTreeStream>;
}
function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
function SidebarContent({ initialData, sidebarQuery, treeStream }: SidebarContentProps) {
const convex = useConvex();
const { isAuthenticated } = useConvexAuth();
const currentUser = useQuery(api.users.currentUser, isAuthenticated ? {} : "skip");
@@ -174,8 +182,8 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
// 处理数据
const sidebarData = useMemo(() => {
return sidebarQuery.data ?? initialData;
}, [sidebarQuery, initialData]);
return treeStream.data ?? sidebarQuery.data ?? initialData;
}, [treeStream.data, sidebarQuery, initialData]);
// isLoading 判断
const isLoading = sidebarQuery.isLoading;
@@ -986,20 +994,17 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
});
if (docItemsMap.size > 0) {
const resp = await fetch("/api/documents/copy-tree", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
try {
await copyTreeCommand({
items: Array.from(docItemsMap.entries()).map(([documentId, recursive]) => ({
documentId,
recursive,
})),
targetParentId: targetDocId,
}),
});
if (!resp.ok) {
const data = await resp.json().catch(() => ({}));
setTimeout(() => window.alert(data?.error ?? "粘贴页面失败"), 0);
});
} catch (error) {
const message = error instanceof Error ? error.message : "粘贴页面失败";
setTimeout(() => window.alert(message), 0);
return;
}
await sidebarQuery.refetch();
@@ -1345,12 +1350,15 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
if (docIds.length > 0) {
const results = await Promise.all(
docIds.map(async (documentId) => {
const resp = await fetch("/api/documents/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ documentId }),
});
return { documentId, ok: resp.ok };
try {
await deleteDocumentCommand({
documentId,
workspaceId: sidebarData.activeWorkspaceId ?? null,
});
return { documentId, ok: true };
} catch {
return { documentId, ok: false };
}
}),
);
const failed = results.filter((item) => !item.ok).map((item) => item.documentId);
@@ -1666,17 +1674,14 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
if (args.copy) {
if (docIds.length > 0) {
const resp = await fetch("/api/documents/copy-tree", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
try {
await copyTreeCommand({
items: docIds.map((documentId) => ({ documentId, recursive: true })),
targetParentId: targetDocId,
}),
});
if (!resp.ok) {
const payload = await resp.json().catch(() => ({}));
setTimeout(() => window.alert(payload?.error ?? "复制页面失败"), 0);
});
} catch (error) {
const message = error instanceof Error ? error.message : "复制页面失败";
setTimeout(() => window.alert(message), 0);
return;
}
await sidebarQuery.refetch();
@@ -1782,10 +1787,9 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
const handleDelete = useCallback(
async (documentId: string) => {
await fetch("/api/documents/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ documentId }),
await deleteDocumentCommand({
documentId,
workspaceId: sidebarData.activeWorkspaceId ?? null,
});
await refreshTree();
emitDocumentsChanged(documentId);
@@ -1793,7 +1797,7 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
router.push("/");
}
},
[activeId, refreshTree, router],
[activeId, refreshTree, router, sidebarData.activeWorkspaceId],
);
const handleDeleteFromContextMenuNode = useCallback(
@@ -1879,14 +1883,13 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
if (!confirmTrashAction("确认恢复该页面吗?")) {
return;
}
await fetch("/api/documents/restore", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ documentId }),
await restoreDocumentCommand({
documentId,
workspaceId: sidebarData.activeWorkspaceId ?? null,
});
await Promise.all([sidebarQuery.refetch(), refreshTree()]);
},
[confirmTrashAction, refreshTree, sidebarQuery],
[confirmTrashAction, refreshTree, sidebarData.activeWorkspaceId, sidebarQuery],
);
const handlePurgeFromTrash = useCallback(
@@ -1894,11 +1897,7 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
if (!confirmTrashAction("彻底删除后将无法找回,是否继续?")) {
return;
}
await fetch("/api/documents/purge", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ documentId }),
});
await purgeDocumentCommand({ documentId });
await Promise.all([sidebarQuery.refetch(), refreshTree()]);
},
[confirmTrashAction, refreshTree, sidebarQuery],
@@ -2817,14 +2816,15 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
window.alert("不能嵌入到自身页面");
return;
}
const response = await fetch("/api/documents/embed", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sourceId: source.id, targetId }),
});
if (!response.ok) {
try {
await embedDocumentCommand({
sourceId: source.id,
targetId,
});
} catch (error) {
const message = error instanceof Error ? error.message : "嵌入失败,请检查目标页面";
if (typeof window !== "undefined") {
window.alert("嵌入失败,请检查目标页面");
window.alert(message);
}
return;
}