feat: 收口文档桥接与 OnlyOffice/Sidebar 回归
- 为 documents.save/meta/content、blocks.patch 与 sidebar.dataset.list 补齐 Rust 协议映射、共享契约与桥接执行器 - 对齐 BlockNote、Mindmap、OnlyOffice 的保存/路由元信息,并补真实浏览器回归脚本与 OnlyOffice 部署基线 - 忽略 Rust 本地构建产物与 Harness 调试状态文件,避免临时产物进入仓库历史
This commit is contained in:
@@ -35,16 +35,23 @@ import { useAppPreferencesStore } from "@/store/app-preferences";
|
||||
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";
|
||||
|
||||
interface BlockNoteEditorProps {
|
||||
documentId: string;
|
||||
workspaceId: string;
|
||||
initialContent: unknown;
|
||||
initialRevision?: number | null;
|
||||
initialConflictDetectionKey?: string | null;
|
||||
pageOptions: PageOptionsState;
|
||||
readOnly?: boolean;
|
||||
onStatsChange?: (stats: DocumentStats) => void;
|
||||
onSnapshot?: (payload: { blocks: Json; stats: DocumentStats }) => void;
|
||||
onCloseToc?: () => void;
|
||||
onPersistedMetaChange?: (payload: {
|
||||
revision: number | null;
|
||||
conflictDetectionKey: string | null;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
const extractInitialBlocks = (content: unknown): Json | undefined => {
|
||||
@@ -174,16 +181,22 @@ export function BlockNoteEditor({
|
||||
documentId,
|
||||
workspaceId,
|
||||
initialContent,
|
||||
initialRevision = null,
|
||||
initialConflictDetectionKey = null,
|
||||
pageOptions,
|
||||
readOnly = false,
|
||||
onStatsChange,
|
||||
onSnapshot,
|
||||
onCloseToc,
|
||||
onPersistedMetaChange,
|
||||
}: BlockNoteEditorProps) {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
const [tocEntries, setTocEntries] = useState<TocEntry[]>([]);
|
||||
const [fullScreenTableId, setFullScreenTableId] = useState<string | null>(null);
|
||||
const isFullScreenTableOpen = fullScreenTableId !== null;
|
||||
const revisionRef = useRef<number | null>(initialRevision);
|
||||
const conflictDetectionKeyRef = useRef<string | null>(initialConflictDetectionKey);
|
||||
|
||||
const openReferencePalette = useSearchPaletteStore((state) => state.openReference);
|
||||
const registerEditorBridge = useEditorBridgeStore((state) => state.registerBridge);
|
||||
@@ -255,20 +268,76 @@ export function BlockNoteEditor({
|
||||
[collaboration],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
revisionRef.current = initialRevision;
|
||||
}, [initialRevision]);
|
||||
|
||||
useEffect(() => {
|
||||
conflictDetectionKeyRef.current = initialConflictDetectionKey;
|
||||
}, [initialConflictDetectionKey]);
|
||||
|
||||
const saveContent = useCallback(
|
||||
async (content: Json) => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await fetch("/api/documents/save", {
|
||||
setSaveError(null);
|
||||
const blockCount = Array.isArray(content) ? content.length : null;
|
||||
const response = await fetch("/api/documents/save", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ documentId, workspaceId, content }),
|
||||
body: JSON.stringify(
|
||||
buildDocumentSavePayload({
|
||||
documentId,
|
||||
workspaceId,
|
||||
revision: revisionRef.current,
|
||||
content,
|
||||
conflictDetectionKey: conflictDetectionKeyRef.current,
|
||||
snapshotCapturedAt: new Date().toISOString(),
|
||||
blockCount,
|
||||
}),
|
||||
),
|
||||
});
|
||||
if (!response.ok) {
|
||||
let message = "保存失败";
|
||||
try {
|
||||
const payload = await response.json();
|
||||
if (payload && typeof payload === "object" && typeof payload.error === "string") {
|
||||
message = payload.error;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
setSaveError(message);
|
||||
throw new Error(message);
|
||||
}
|
||||
const payload = await response.json() as {
|
||||
revision?: number | null;
|
||||
conflictDetectionKey?: string | null;
|
||||
};
|
||||
const nextRevision =
|
||||
typeof payload.revision === "number" && Number.isInteger(payload.revision)
|
||||
? payload.revision
|
||||
: revisionRef.current;
|
||||
const nextConflictDetectionKey =
|
||||
typeof payload.conflictDetectionKey === "string" && payload.conflictDetectionKey.trim()
|
||||
? payload.conflictDetectionKey
|
||||
: conflictDetectionKeyRef.current;
|
||||
revisionRef.current = nextRevision ?? null;
|
||||
conflictDetectionKeyRef.current = nextConflictDetectionKey ?? null;
|
||||
onPersistedMetaChange?.({
|
||||
revision: revisionRef.current,
|
||||
conflictDetectionKey: conflictDetectionKeyRef.current,
|
||||
});
|
||||
setSaveError(null);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
setSaveError(error.message);
|
||||
}
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
},
|
||||
[documentId, workspaceId],
|
||||
[documentId, onPersistedMetaChange, workspaceId],
|
||||
);
|
||||
|
||||
const debouncedSave = useDebouncedCallback(saveContent, 800);
|
||||
@@ -1262,7 +1331,7 @@ const computeDocumentStats = (blocks: Block<CustomBlockSchema>[]): DocumentStats
|
||||
<CustomSlashMenu editor={editor} currentDocumentId={documentId} />
|
||||
</BlockNoteView>
|
||||
<div className="pointer-events-none absolute right-4 top-3 text-xs text-gray-400">
|
||||
{isSaving ? "保存中..." : "已保存"}
|
||||
{isSaving ? "保存中..." : saveError ? saveError : "已保存"}
|
||||
</div>
|
||||
</div>
|
||||
<DocumentToc entries={tocEntries} visible={pageOptions.showToc} onJump={jumpToHeading} onClose={onCloseToc} />
|
||||
@@ -1279,4 +1348,4 @@ const computeDocumentStats = (blocks: Block<CustomBlockSchema>[]): DocumentStats
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user