Files
mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx
T

205 lines
6.7 KiB
TypeScript

import { headers } from "next/headers";
import { notFound, redirect } from "next/navigation";
import { DocumentShell } from "@/components/editor/document-shell";
import type { PageFont, PageLayoutDensity, PageOptionsState, DocumentStats } from "@/types/page-options";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { fetchDocumentMetaViaBridge } from "@/lib/documents/bridge-server";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { buildDocumentBridgeContext, buildDocumentQueryEnvelope } from "@/lib/documents/bridge";
import { buildPageSubtreeProjection } from "@/lib/documents/page-subtree";
import { executeRustBridgeQueryTransport, resolveRustBridgeQueryPlan } from "@/lib/documents/rust-runtime";
interface DocumentPageProps {
params: Promise<{ id: string }>;
searchParams?: Promise<Record<string, string | string[] | undefined>>;
}
type DocumentMetaPayload = {
id: string;
workspace_id: string;
title: string | null;
updated_at: string | null;
can_edit?: boolean | null;
disable_download?: boolean | null;
disable_copy?: boolean | null;
wide_layout?: boolean | null;
use_small_text?: boolean | null;
show_heading_numbers?: boolean | null;
show_toc?: boolean | null;
show_structure?: boolean | null;
protect_editing?: boolean | null;
show_word_count?: boolean | null;
collapse_backlinks?: boolean | null;
page_font?: PageFont | null;
layout_density?: PageLayoutDensity | null;
hide_child_pages?: boolean | null;
show_block_ref_count?: boolean | null;
embed_default_block_id?: string | null;
word_count?: number | null;
character_count?: number | null;
block_count?: number | null;
todo_total?: number | null;
todo_total_count?: number | null;
todo_done?: number | null;
todo_done_count?: number | null;
};
type DocumentContentPayload = {
content?: unknown;
revision?: number | null;
conflict_detection_key?: string | null;
};
async function fetchDocumentContentOnServer(input: {
documentId: string;
workspaceId: string;
}): Promise<{
content: unknown;
revision: number | null;
conflictDetectionKey: string | null;
}> {
try {
const headerList = await headers();
const requestHeaders = new Headers();
[
"cookie",
"authorization",
"x-request-id",
"x-trace-id",
"x-session-id",
"x-source-channel",
"x-source-client",
"user-agent",
].forEach((name) => {
const value = headerList.get(name);
if (value) {
requestHeaders.set(name, value);
}
});
const request = new Request("http://mnote.local/documents/content", {
method: "GET",
headers: requestHeaders,
});
const { client } = await getAuthedConvexClient();
const bridgeContext = await buildDocumentBridgeContext({
request,
workspaceId: input.workspaceId,
});
const envelope = buildDocumentQueryEnvelope({
name: "documents.content.get",
payload: {
documentId: input.documentId,
workspaceId: input.workspaceId,
},
});
const plan = await resolveRustBridgeQueryPlan({
context: bridgeContext,
envelope,
});
const result = await executeRustBridgeQueryTransport<DocumentContentPayload | null>({
client,
plan,
});
return {
content: result?.content ?? null,
revision:
typeof result?.revision === "number" && Number.isInteger(result.revision)
? result.revision
: 0,
conflictDetectionKey:
typeof result?.conflict_detection_key === "string" && result.conflict_detection_key.trim()
? result.conflict_detection_key
: `${input.documentId}:0`,
};
} catch {
return {
content: null,
revision: null,
conflictDetectionKey: null,
};
}
}
export default async function DocumentPage({ params, searchParams }: DocumentPageProps) {
const { id } = await params;
const resolvedSearch = (await searchParams) ?? {};
const openTableIdRaw = resolvedSearch?.openTableId;
const openTableId = typeof openTableIdRaw === "string" ? openTableIdRaw : null;
if (isConvexEnabled()) {
const workspaceIdRaw = resolvedSearch?.workspaceId;
const workspaceId = typeof workspaceIdRaw === "string" ? workspaceIdRaw : null;
const result = await fetchDocumentMetaViaBridge<DocumentMetaPayload>({
documentId: id,
workspaceId,
});
const doc = result?.doc;
if (!doc) {
notFound();
}
const readOnly = doc.can_edit === false;
const disableDownload = Boolean(doc.disable_download);
const disableCopy = Boolean(doc.disable_copy);
const initialOptions: PageOptionsState = {
wideLayout: doc.wide_layout ?? false,
smallText: doc.use_small_text ?? false,
showHeadingNumbers: doc.show_heading_numbers ?? true,
showToc: doc.show_toc ?? false,
showStructure: doc.show_structure ?? false,
protectEditing: doc.protect_editing ?? false,
showWordCount: doc.show_word_count ?? true,
collapseBacklinks: doc.collapse_backlinks ?? false,
pageFont: doc.page_font ?? "default",
layoutDensity: doc.layout_density ?? "normal",
hideChildPages: doc.hide_child_pages ?? false,
showBlockRefCount: doc.show_block_ref_count ?? false,
embedDefaultBlockId: doc.embed_default_block_id ?? null,
};
const initialStats: DocumentStats = {
wordCount: doc.word_count ?? 0,
characterCount: doc.character_count ?? 0,
blockCount: doc.block_count ?? 0,
todoTotal: doc.todo_total ?? doc.todo_total_count ?? 0,
todoDone: doc.todo_done ?? doc.todo_done_count ?? 0,
};
const initialDocumentContent = await fetchDocumentContentOnServer({
documentId: doc.id,
workspaceId: doc.workspace_id,
});
const initialPageSubtree = buildPageSubtreeProjection({
documentId: doc.id,
title: doc.title ?? "无标题",
content: initialDocumentContent.content,
});
return (
<div className="flex h-screen flex-col">
<div className="min-h-0 flex-1">
<DocumentShell
documentId={doc.id}
workspaceId={doc.workspace_id}
title={doc.title ?? "无标题"}
updatedAt={doc.updated_at}
initialContent={initialDocumentContent.content}
initialContentRevision={initialDocumentContent.revision}
initialConflictDetectionKey={initialDocumentContent.conflictDetectionKey}
initialPageSubtree={initialPageSubtree}
initialOptions={initialOptions}
initialStats={initialStats}
openTableId={openTableId}
readOnly={readOnly}
disableDownload={disableDownload}
disableCopy={disableCopy}
/>
</div>
</div>
);
}
redirect("/auth");
}