feat: 收口 tree-first graph 主链与前端测试修复
This commit is contained in:
@@ -36,7 +36,7 @@ describe("AiAgentPanel", () => {
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("全局 AI");
|
||||
expect(container.textContent).toContain("自动工具编排");
|
||||
expect(container.textContent).toContain("前端已经退为桥接层");
|
||||
expect(container.textContent).toContain("联网检索");
|
||||
expect(container.textContent).toContain("LightRAG");
|
||||
expect(container.textContent).toContain("跨页面文档");
|
||||
@@ -50,12 +50,13 @@ describe("AiAgentPanel", () => {
|
||||
|
||||
const toggleButton = container.querySelector('button[aria-label="切换工具活动面板"]');
|
||||
expect(toggleButton).not.toBeNull();
|
||||
expect(container.textContent).toContain("本轮活动");
|
||||
expect(container.textContent).toContain("活动轨迹");
|
||||
expect(container.textContent).toContain("暂无工具活动");
|
||||
|
||||
act(() => {
|
||||
toggleButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(container.textContent).not.toContain("本轮活动");
|
||||
expect(container.textContent).not.toContain("暂无工具活动");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,7 +21,12 @@ import { Button } from "@/components/ui/button";
|
||||
import { DocumentToc } from "@/components/editor/document-toc";
|
||||
import { DocumentReadView } from "@/components/editor/document-read-view";
|
||||
import { buildPageSubtreeProjection, extractPageBlocks, type PageSubtreeProjection } from "@/lib/documents/page-subtree";
|
||||
import { moveDocumentCommand, renameDocumentCommand } from "@/lib/documents/tree-command-client";
|
||||
import {
|
||||
deleteDocumentCommand,
|
||||
embedDocumentCommand,
|
||||
moveDocumentCommand,
|
||||
renameDocumentCommand,
|
||||
} from "@/lib/documents/tree-command-client";
|
||||
|
||||
const BlockNoteEditor = dynamic(
|
||||
() => import("@/components/editor/blocknote-editor").then((mod) => mod.BlockNoteEditor),
|
||||
@@ -602,19 +607,16 @@ export function DocumentContent({
|
||||
if (readOnly) return;
|
||||
const ok = window.confirm("确定删除该页面吗?删除后会进入垃圾桶。");
|
||||
if (!ok) return;
|
||||
const resp = await fetch("/api/documents/delete", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ documentId }),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const payload = await resp.json().catch(() => ({}));
|
||||
window.alert(payload?.error ?? "删除失败");
|
||||
try {
|
||||
await deleteDocumentCommand({ documentId, workspaceId });
|
||||
} catch (error) {
|
||||
const payload = error instanceof Error ? error.message : null;
|
||||
window.alert(payload ?? "删除失败");
|
||||
return;
|
||||
}
|
||||
router.push("/");
|
||||
router.refresh();
|
||||
}, [documentId, readOnly, router]);
|
||||
}, [documentId, readOnly, router, workspaceId]);
|
||||
|
||||
const handleOpenMoveEmbed = useCallback(() => {
|
||||
if (readOnly) return;
|
||||
@@ -642,16 +644,22 @@ export function DocumentContent({
|
||||
return;
|
||||
}
|
||||
|
||||
const resp = await fetch("/api/documents/embed", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ sourceId: documentId, targetId }),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const payload = await resp.json().catch(() => ({}));
|
||||
window.alert(payload?.error ?? "嵌入失败");
|
||||
if (!targetId) {
|
||||
window.alert("请选择目标页面");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await embedDocumentCommand({
|
||||
sourceId: documentId,
|
||||
targetId,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "嵌入失败";
|
||||
window.alert(message);
|
||||
return;
|
||||
}
|
||||
|
||||
window.alert("已嵌入到目标页面");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ import { deleteOnlineTable } from "@/lib/online-table";
|
||||
import { emitAssetsChanged, emitDocumentsChanged } from "@/lib/events";
|
||||
import { useMoveEmbedPickerStore } from "@/store/move-embed-picker";
|
||||
import { useCommentsUiStore } from "@/store/comments-ui";
|
||||
import { createChildDocumentCommand } from "@/lib/documents/tree-command-client";
|
||||
import { createChildDocumentCommand, deleteDocumentCommand } from "@/lib/documents/tree-command-client";
|
||||
|
||||
type InlineNode = { text?: unknown };
|
||||
type TableMenuBlock = Parameters<
|
||||
@@ -119,10 +119,9 @@ const CustomDragHandleMenu = ({ block, currentDocumentId, workspaceId }: CustomD
|
||||
if (block.type === "pageReference") {
|
||||
const pageId = block.props.pageId;
|
||||
if (pageId) {
|
||||
await fetch("/api/documents/delete", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ documentId: pageId }),
|
||||
await deleteDocumentCommand({
|
||||
documentId: pageId,
|
||||
workspaceId,
|
||||
});
|
||||
if (typeof window !== "undefined") {
|
||||
emitDocumentsChanged(pageId);
|
||||
@@ -131,7 +130,7 @@ const CustomDragHandleMenu = ({ block, currentDocumentId, workspaceId }: CustomD
|
||||
}
|
||||
editor.removeBlocks([block.id]);
|
||||
router.refresh();
|
||||
}, [block, editor, router]);
|
||||
}, [block, editor, router, workspaceId]);
|
||||
|
||||
const handleDeleteBlock = useCallback(async () => {
|
||||
if (block.type === "pageReference") {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user