feat: land page aggregate and phase7 document ai mainline
- 收口 page aggregate 读取、本地状态与命令客户端\n- 接入 phase7 document ai sidecar 与前端编排入口\n- 更新 architecture 与 design 状态迁移
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
import type { PageBodyPersistedMeta } from "@/lib/documents/page-command-client";
|
||||
import type { PageAggregateProjection } from "@/lib/documents/page-aggregate";
|
||||
import type { PageSubtreeProjection } from "@/lib/documents/page-subtree";
|
||||
import type { PageOptionsState } from "@/types/page-options";
|
||||
import type { Json } from "@/types/supabase";
|
||||
|
||||
export type PageAggregateClientState = {
|
||||
options: PageOptionsState;
|
||||
content: unknown;
|
||||
serverContentSnapshot: unknown;
|
||||
serverPageSubtreeSnapshot: PageSubtreeProjection | null;
|
||||
serverPageSubtreeTitle: string;
|
||||
contentRevision: number | null;
|
||||
conflictDetectionKey: string | null;
|
||||
};
|
||||
|
||||
export type PageAggregateClientStateAction =
|
||||
| {
|
||||
type: "hydrate_from_page";
|
||||
page: PageAggregateProjection;
|
||||
}
|
||||
| {
|
||||
type: "patch_page_options";
|
||||
patch: Partial<PageOptionsState>;
|
||||
}
|
||||
| {
|
||||
type: "apply_local_content_snapshot";
|
||||
content: unknown;
|
||||
}
|
||||
| {
|
||||
type: "apply_persisted_body_meta";
|
||||
meta: PageBodyPersistedMeta;
|
||||
}
|
||||
| {
|
||||
type: "update_server_page_subtree_title";
|
||||
title: string;
|
||||
};
|
||||
|
||||
function normalizePageTitle(title: string | null | undefined): string {
|
||||
const normalized = String(title ?? "").trim();
|
||||
return normalized || "无标题";
|
||||
}
|
||||
|
||||
function resolveServerPageSubtreeTitle(page: PageAggregateProjection): string {
|
||||
const subtreeTitle = page.tree.pageSubtree?.rootNode.metadata.title;
|
||||
if (typeof subtreeTitle === "string" && subtreeTitle.trim()) {
|
||||
return subtreeTitle.trim();
|
||||
}
|
||||
return normalizePageTitle(page.head.title);
|
||||
}
|
||||
|
||||
export function createPageAggregateClientState(
|
||||
page: PageAggregateProjection,
|
||||
): PageAggregateClientState {
|
||||
return {
|
||||
options: page.layout.pageOptions,
|
||||
content: page.body.content,
|
||||
serverContentSnapshot: page.body.content,
|
||||
serverPageSubtreeSnapshot: page.tree.pageSubtree,
|
||||
serverPageSubtreeTitle: resolveServerPageSubtreeTitle(page),
|
||||
contentRevision: page.body.revision,
|
||||
conflictDetectionKey: page.body.conflictDetectionKey,
|
||||
};
|
||||
}
|
||||
|
||||
export function pageAggregateClientStateReducer(
|
||||
state: PageAggregateClientState,
|
||||
action: PageAggregateClientStateAction,
|
||||
): PageAggregateClientState {
|
||||
switch (action.type) {
|
||||
case "hydrate_from_page":
|
||||
return createPageAggregateClientState(action.page);
|
||||
case "patch_page_options":
|
||||
return {
|
||||
...state,
|
||||
options: {
|
||||
...state.options,
|
||||
...action.patch,
|
||||
},
|
||||
};
|
||||
case "apply_local_content_snapshot":
|
||||
return {
|
||||
...state,
|
||||
content: action.content,
|
||||
};
|
||||
case "apply_persisted_body_meta":
|
||||
return {
|
||||
...state,
|
||||
contentRevision: action.meta.revision,
|
||||
conflictDetectionKey: action.meta.conflictDetectionKey,
|
||||
};
|
||||
case "update_server_page_subtree_title":
|
||||
return {
|
||||
...state,
|
||||
serverPageSubtreeTitle: normalizePageTitle(action.title),
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export function selectPageAggregateClientPageSubtree(
|
||||
state: PageAggregateClientState,
|
||||
pageTitle: string,
|
||||
): PageSubtreeProjection | null {
|
||||
const hasServerPageSubtree = Boolean(state.serverPageSubtreeSnapshot);
|
||||
const titleUnchanged = normalizePageTitle(pageTitle) === state.serverPageSubtreeTitle;
|
||||
const contentUnchanged = state.content === state.serverContentSnapshot;
|
||||
|
||||
if (hasServerPageSubtree && titleUnchanged && contentUnchanged) {
|
||||
return state.serverPageSubtreeSnapshot;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function selectPageAggregateClientAiSnapshot(
|
||||
state: PageAggregateClientState,
|
||||
input: {
|
||||
workspaceId: string | null;
|
||||
pageTitle: string;
|
||||
},
|
||||
): {
|
||||
blocks: Json | null;
|
||||
pageSubtree: PageSubtreeProjection | null;
|
||||
persistedMeta: {
|
||||
workspaceId: string | null;
|
||||
revision: number | null;
|
||||
conflictDetectionKey: string | null;
|
||||
};
|
||||
pageOptions: PageOptionsState;
|
||||
} {
|
||||
const blocks = state.content as Json | null;
|
||||
|
||||
return {
|
||||
blocks,
|
||||
pageSubtree: selectPageAggregateClientPageSubtree(state, input.pageTitle),
|
||||
persistedMeta: {
|
||||
workspaceId: input.workspaceId,
|
||||
revision: state.contentRevision,
|
||||
conflictDetectionKey: state.conflictDetectionKey,
|
||||
},
|
||||
pageOptions: state.options,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user