feat: 接入 mnote web tree shell 与主页链路整理
- 增加 mnote-web tree/command 支持与前端 MnoteWebTreeShell 集成 - 调整 sidebar、documents、runtime config 与 dev/prod server 配套逻辑 - 补充 homepage/tree shell smoke 脚本并更新 harness 进度文件
This commit is contained in:
@@ -21,7 +21,6 @@ import type { MediaAsset } from "@/types/media";
|
||||
import { customBlockSchema, type CustomBlockSchema } from "./schema";
|
||||
import { CustomSideMenu } from "./menus/CustomSideMenu";
|
||||
import { CustomSlashMenu } from "./menus/CustomSlashMenu";
|
||||
import { MoveEmbedPickerHost } from "@/components/documents/move-embed-picker-host";
|
||||
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
|
||||
import { DocumentToc, type TocEntry } from "@/components/editor/document-toc";
|
||||
import { useSearchPaletteStore } from "@/store/search-palette";
|
||||
@@ -36,6 +35,7 @@ import { useCommentsUiStore } from "@/store/comments-ui";
|
||||
import { useConvexAuth, useQuery } from "convex/react";
|
||||
import { api } from "@/lib/convex/api";
|
||||
import { buildDocumentSavePayload } from "@/lib/documents/save-contract";
|
||||
import type { EditorReferenceBridge } from "@/store/editor-bridge";
|
||||
|
||||
interface BlockNoteEditorProps {
|
||||
documentId: string;
|
||||
@@ -197,6 +197,10 @@ export function BlockNoteEditor({
|
||||
const isFullScreenTableOpen = fullScreenTableId !== null;
|
||||
const revisionRef = useRef<number | null>(initialRevision);
|
||||
const conflictDetectionKeyRef = useRef<string | null>(initialConflictDetectionKey);
|
||||
const onStatsChangeRef = useRef(onStatsChange);
|
||||
const onSnapshotRef = useRef(onSnapshot);
|
||||
const onPersistedMetaChangeRef = useRef(onPersistedMetaChange);
|
||||
const stableInitialContentRef = useRef<{ documentId: string; content: Json | undefined } | null>(null);
|
||||
|
||||
const openReferencePalette = useSearchPaletteStore((state) => state.openReference);
|
||||
const registerEditorBridge = useEditorBridgeStore((state) => state.registerBridge);
|
||||
@@ -226,6 +230,16 @@ export function BlockNoteEditor({
|
||||
[initialContent],
|
||||
);
|
||||
|
||||
if (
|
||||
!stableInitialContentRef.current ||
|
||||
stableInitialContentRef.current.documentId !== documentId
|
||||
) {
|
||||
stableInitialContentRef.current = {
|
||||
documentId,
|
||||
content: normalizedInitialContent,
|
||||
};
|
||||
}
|
||||
|
||||
const collaboration = useMemo(() => {
|
||||
const url = process.env.NEXT_PUBLIC_HOCUSPOCUS_URL;
|
||||
if (!url) return null;
|
||||
@@ -240,7 +254,7 @@ export function BlockNoteEditor({
|
||||
|
||||
const editor = useCreateBlockNote(
|
||||
{
|
||||
initialContent: normalizedInitialContent as never,
|
||||
initialContent: stableInitialContentRef.current?.content as never,
|
||||
schema: customBlockSchema,
|
||||
placeholders: {
|
||||
default: "输入'/'选择,按 空格 打开AI...",
|
||||
@@ -257,7 +271,7 @@ export function BlockNoteEditor({
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
[documentId, normalizedInitialContent],
|
||||
[documentId],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
@@ -268,6 +282,20 @@ export function BlockNoteEditor({
|
||||
[collaboration],
|
||||
);
|
||||
|
||||
const editorRef = useRef(editor);
|
||||
const insertMediaAssetRef = useRef<(asset: MediaAsset) => void>(() => {});
|
||||
const insertMindmapRef = useRef<(args: { documentId: string; mindmapId: string }) => void>(() => {});
|
||||
const insertOnlineTableRef = useRef<(args: { documentId: string; tableId: string }) => void>(() => {});
|
||||
const setFullScreenTableIdRef = useRef(setFullScreenTableId);
|
||||
|
||||
useEffect(() => {
|
||||
editorRef.current = editor;
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
setFullScreenTableIdRef.current = setFullScreenTableId;
|
||||
}, [setFullScreenTableId]);
|
||||
|
||||
useEffect(() => {
|
||||
revisionRef.current = initialRevision;
|
||||
}, [initialRevision]);
|
||||
@@ -276,6 +304,18 @@ export function BlockNoteEditor({
|
||||
conflictDetectionKeyRef.current = initialConflictDetectionKey;
|
||||
}, [initialConflictDetectionKey]);
|
||||
|
||||
useEffect(() => {
|
||||
onStatsChangeRef.current = onStatsChange;
|
||||
}, [onStatsChange]);
|
||||
|
||||
useEffect(() => {
|
||||
onSnapshotRef.current = onSnapshot;
|
||||
}, [onSnapshot]);
|
||||
|
||||
useEffect(() => {
|
||||
onPersistedMetaChangeRef.current = onPersistedMetaChange;
|
||||
}, [onPersistedMetaChange]);
|
||||
|
||||
const saveContent = useCallback(
|
||||
async (content: Json) => {
|
||||
setIsSaving(true);
|
||||
@@ -324,7 +364,7 @@ export function BlockNoteEditor({
|
||||
: conflictDetectionKeyRef.current;
|
||||
revisionRef.current = nextRevision ?? null;
|
||||
conflictDetectionKeyRef.current = nextConflictDetectionKey ?? null;
|
||||
onPersistedMetaChange?.({
|
||||
onPersistedMetaChangeRef.current?.({
|
||||
revision: revisionRef.current,
|
||||
conflictDetectionKey: conflictDetectionKeyRef.current,
|
||||
});
|
||||
@@ -337,7 +377,7 @@ export function BlockNoteEditor({
|
||||
setIsSaving(false);
|
||||
}
|
||||
},
|
||||
[documentId, onPersistedMetaChange, workspaceId],
|
||||
[documentId, workspaceId],
|
||||
);
|
||||
|
||||
const debouncedSave = useDebouncedCallback(saveContent, 800);
|
||||
@@ -705,8 +745,8 @@ export function BlockNoteEditor({
|
||||
setTocEntries(buildHeadingToc(typedBlocks));
|
||||
syncProgressMeters(editor);
|
||||
const stats = computeDocumentStats(typedBlocks);
|
||||
onStatsChange?.(stats);
|
||||
onSnapshot?.({ blocks: blocks as Json, stats });
|
||||
onStatsChangeRef.current?.(stats);
|
||||
onSnapshotRef.current?.({ blocks: blocks as Json, stats });
|
||||
|
||||
// 检测块删除 -> 同步删除附件/思维导图并刷新侧边栏
|
||||
const { assetIds, mindmapBlockIds, onlineTableIds } = collectAssets(typedBlocks);
|
||||
@@ -745,7 +785,7 @@ export function BlockNoteEditor({
|
||||
disposed = true;
|
||||
unsubscribe?.();
|
||||
};
|
||||
}, [collectAssets, debouncedSave, deleteAssets, deleteMindmapAssets, deleteOnlineTables, editor, onSnapshot, onStatsChange, restoreOnlineTableIfNeeded]);
|
||||
}, [collectAssets, debouncedSave, deleteAssets, deleteMindmapAssets, deleteOnlineTables, editor, restoreOnlineTableIfNeeded]);
|
||||
|
||||
const jumpToHeading = useCallback((headingId: string) => {
|
||||
const target = document.querySelector<HTMLElement>(`[data-id="${headingId}"]`);
|
||||
@@ -1003,6 +1043,18 @@ const computeDocumentStats = (blocks: Block<CustomBlockSchema>[]): DocumentStats
|
||||
[documentId, editor],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
insertMediaAssetRef.current = insertMediaAssetBlock;
|
||||
}, [insertMediaAssetBlock]);
|
||||
|
||||
useEffect(() => {
|
||||
insertMindmapRef.current = insertMindmapBlock;
|
||||
}, [insertMindmapBlock]);
|
||||
|
||||
useEffect(() => {
|
||||
insertOnlineTableRef.current = insertOnlineTableBlock;
|
||||
}, [insertOnlineTableBlock]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return undefined;
|
||||
|
||||
@@ -1090,46 +1142,48 @@ const computeDocumentStats = (blocks: Block<CustomBlockSchema>[]): DocumentStats
|
||||
[documentId, workspaceId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) {
|
||||
registerEditorBridge(null);
|
||||
return;
|
||||
}
|
||||
const bridge = {
|
||||
const stableEditorBridge = useMemo<EditorReferenceBridge>(
|
||||
() => ({
|
||||
undo: () => {
|
||||
try {
|
||||
editor.focus();
|
||||
editor.undo();
|
||||
const currentEditor = editorRef.current;
|
||||
if (!currentEditor) return;
|
||||
currentEditor.focus();
|
||||
currentEditor.undo();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
redo: () => {
|
||||
try {
|
||||
editor.focus();
|
||||
editor.redo();
|
||||
const currentEditor = editorRef.current;
|
||||
if (!currentEditor) return;
|
||||
currentEditor.focus();
|
||||
currentEditor.redo();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
openTableFullScreen: (tableId: string) => {
|
||||
setFullScreenTableId(tableId);
|
||||
setFullScreenTableIdRef.current(tableId);
|
||||
},
|
||||
insertMediaAsset: (asset: MediaAsset) => {
|
||||
insertMediaAssetBlock(asset);
|
||||
insertMediaAssetRef.current(asset);
|
||||
},
|
||||
insertMindmapAsset: (args: { documentId: string; mindmapId: string }) => {
|
||||
insertMindmapBlock(args);
|
||||
insertMindmapRef.current(args);
|
||||
},
|
||||
insertOnlineTableAsset: (args: { documentId: string; tableId: string }) => {
|
||||
insertOnlineTableBlock(args);
|
||||
insertOnlineTableRef.current(args);
|
||||
},
|
||||
insertInlineReference: (target: ReferenceTarget, aliasText?: string) => {
|
||||
editor.focus();
|
||||
const cursor = editor.getTextCursorPosition();
|
||||
const currentEditor = editorRef.current;
|
||||
if (!currentEditor) return { blockId: null };
|
||||
currentEditor.focus();
|
||||
const cursor = currentEditor.getTextCursorPosition();
|
||||
const blockId = cursor?.block?.id ?? null;
|
||||
const text = aliasText || target.title || "无标题";
|
||||
editor.insertInlineContent([
|
||||
currentEditor.insertInlineContent([
|
||||
{
|
||||
type: "link",
|
||||
href: buildDocumentPath(target.id),
|
||||
@@ -1140,12 +1194,14 @@ const computeDocumentStats = (blocks: Block<CustomBlockSchema>[]): DocumentStats
|
||||
return { blockId };
|
||||
},
|
||||
insertEmbedReference: (target: ReferenceTarget) => {
|
||||
editor.focus();
|
||||
const cursor = editor.getTextCursorPosition();
|
||||
const currentEditor = editorRef.current;
|
||||
if (!currentEditor) return { blockId: null };
|
||||
currentEditor.focus();
|
||||
const cursor = currentEditor.getTextCursorPosition();
|
||||
const referenceBlock =
|
||||
cursor?.block ?? editor.topLevelBlocks[editor.topLevelBlocks.length - 1];
|
||||
cursor?.block ?? currentEditor.topLevelBlocks[currentEditor.topLevelBlocks.length - 1];
|
||||
const blockId = generateBlockId();
|
||||
editor.insertBlocks(
|
||||
currentEditor.insertBlocks(
|
||||
[
|
||||
{
|
||||
id: blockId,
|
||||
@@ -1162,7 +1218,9 @@ const computeDocumentStats = (blocks: Block<CustomBlockSchema>[]): DocumentStats
|
||||
return { blockId };
|
||||
},
|
||||
replaceWithSnapshot: (payload: Json) => {
|
||||
editor.focus();
|
||||
const currentEditor = editorRef.current;
|
||||
if (!currentEditor) return;
|
||||
currentEditor.focus();
|
||||
const nextBlocks = Array.isArray(payload)
|
||||
? payload
|
||||
: Array.isArray((payload as { blocks?: Json }).blocks)
|
||||
@@ -1171,20 +1229,29 @@ const computeDocumentStats = (blocks: Block<CustomBlockSchema>[]): DocumentStats
|
||||
if (!Array.isArray(nextBlocks)) {
|
||||
return;
|
||||
}
|
||||
editor.replaceBlocks(editor.topLevelBlocks, nextBlocks as never);
|
||||
currentEditor.replaceBlocks(currentEditor.topLevelBlocks, nextBlocks as never);
|
||||
},
|
||||
getCursorBlockId: () => {
|
||||
try {
|
||||
const cursor = editor.getTextCursorPosition();
|
||||
const currentEditor = editorRef.current;
|
||||
if (!currentEditor) return null;
|
||||
const cursor = currentEditor.getTextCursorPosition();
|
||||
return cursor?.block?.id ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
};
|
||||
registerEditorBridge(bridge);
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
registerEditorBridge(editor ? stableEditorBridge : null);
|
||||
}, [editor, registerEditorBridge, stableEditorBridge]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => registerEditorBridge(null);
|
||||
}, [editor, insertMediaAssetBlock, insertMindmapBlock, insertOnlineTableBlock, registerEditorBridge]);
|
||||
}, [registerEditorBridge]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
@@ -1336,9 +1403,6 @@ const computeDocumentStats = (blocks: Block<CustomBlockSchema>[]): DocumentStats
|
||||
</div>
|
||||
<DocumentToc entries={tocEntries} visible={pageOptions.showToc} onJump={jumpToHeading} onClose={onCloseToc} />
|
||||
</div>
|
||||
|
||||
<MoveEmbedPickerHost />
|
||||
|
||||
{/* 全屏表格编辑器 Modal */}
|
||||
{fullScreenTableId && (
|
||||
<FullScreenTableEditor
|
||||
|
||||
Reference in New Issue
Block a user