feat: 收口 mindmap Phase 6 Leptos UI shell
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
import {
|
||||
createSimpleMindMapBridge,
|
||||
type MindmapAdapterProjection,
|
||||
type SimpleMindMapBridge,
|
||||
type SimpleMindMapBridgeEvent,
|
||||
type SimpleMindMapPluginName,
|
||||
} from "./simple-mind-map-bridge";
|
||||
import type { MindmapCompatPayloadPatch, MindmapKernelCommand } from "./mindmap-command-diff";
|
||||
|
||||
export type {
|
||||
MindmapAdapterProjection,
|
||||
SimpleMindMapBridge,
|
||||
SimpleMindMapBridgeEvent,
|
||||
} from "./simple-mind-map-bridge";
|
||||
|
||||
export type LeptosMindmapAdapterOptions = {
|
||||
el: HTMLElement;
|
||||
projection: MindmapAdapterProjection;
|
||||
mode?: "edit" | "readonly";
|
||||
pluginNames?: SimpleMindMapPluginName[];
|
||||
runtimeOptions?: Record<string, unknown>;
|
||||
onEvent?: (event: SimpleMindMapBridgeEvent) => void;
|
||||
};
|
||||
|
||||
export type RequestMindmapAdapterProjectionInput = {
|
||||
documentId: string;
|
||||
mindmapId: string;
|
||||
endpoint?: string;
|
||||
fetcher?: typeof fetch;
|
||||
};
|
||||
|
||||
export type MindmapCommandApplyInput = {
|
||||
documentId: string;
|
||||
mindmapId: string;
|
||||
commands: Array<MindmapKernelCommand | MindmapCompatPayloadPatch>;
|
||||
projectionRevision?: number | null;
|
||||
endpoint?: string;
|
||||
fetcher?: typeof fetch;
|
||||
};
|
||||
|
||||
export type MindmapCommandApplyResult = {
|
||||
ok: true;
|
||||
kernelRevision: number | null;
|
||||
raw: unknown;
|
||||
};
|
||||
|
||||
export class MindmapCommandBridgeError extends Error {
|
||||
readonly code: "command_failed";
|
||||
readonly status: number | null;
|
||||
|
||||
constructor(message: string, status: number | null = null) {
|
||||
super(message);
|
||||
this.name = "MindmapCommandBridgeError";
|
||||
this.code = "command_failed";
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
export const isMindmapAdapterProjection = (value: unknown): value is MindmapAdapterProjection => {
|
||||
if (!isRecord(value)) return false;
|
||||
return (
|
||||
value.schema === "mnote.mindmap.simple_mind_map_scene.v1" &&
|
||||
value.runtime === "simple-mind-map" &&
|
||||
"root" in value &&
|
||||
typeof value.kernelRevision === "number"
|
||||
);
|
||||
};
|
||||
|
||||
export const createMindmapAdapterProjectionEndpoint = (documentId: string, mindmapId: string) => {
|
||||
const doc = encodeURIComponent(documentId);
|
||||
const map = encodeURIComponent(mindmapId);
|
||||
return `/api/mindmap/${doc}/${map}?view=simple_mind_map_scene&queryName=mindmap.simple_mind_map_scene.get`;
|
||||
};
|
||||
|
||||
export const createMindmapCommandApplyEndpoint = (documentId: string, mindmapId: string) => {
|
||||
const doc = encodeURIComponent(documentId);
|
||||
const map = encodeURIComponent(mindmapId);
|
||||
return `/api/mindmap/${doc}/${map}`;
|
||||
};
|
||||
|
||||
export const requestMindmapAdapterProjection = async (
|
||||
input: RequestMindmapAdapterProjectionInput,
|
||||
): Promise<MindmapAdapterProjection> => {
|
||||
const fetcher = input.fetcher ?? fetch;
|
||||
const endpoint = input.endpoint ?? createMindmapAdapterProjectionEndpoint(input.documentId, input.mindmapId);
|
||||
const response = await fetcher(endpoint, {
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`projection_load_failed:${response.status}`);
|
||||
}
|
||||
const payload = (await response.json()) as unknown;
|
||||
const projection = isRecord(payload) && "result" in payload ? payload.result : payload;
|
||||
if (!isMindmapAdapterProjection(projection)) {
|
||||
throw new Error("projection_load_failed:invalid_adapter_projection");
|
||||
}
|
||||
return projection;
|
||||
};
|
||||
|
||||
const readKernelRevision = (value: unknown): number | null => {
|
||||
if (!isRecord(value)) return null;
|
||||
const candidates = [
|
||||
value.kernelRevision,
|
||||
value.projectionRevision,
|
||||
isRecord(value.result) ? value.result.kernelRevision : null,
|
||||
isRecord(value.result) ? value.result.projectionRevision : null,
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (typeof candidate === "number" && Number.isFinite(candidate)) return candidate;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const executeMindmapCommandApply = async (
|
||||
input: MindmapCommandApplyInput,
|
||||
): Promise<MindmapCommandApplyResult> => {
|
||||
if (input.commands.length === 0) {
|
||||
throw new MindmapCommandBridgeError("command_failed:empty_commands");
|
||||
}
|
||||
const fetcher = input.fetcher ?? fetch;
|
||||
const endpoint = input.endpoint ?? createMindmapCommandApplyEndpoint(input.documentId, input.mindmapId);
|
||||
const response = await fetcher(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
commandName: "mindmap.command.apply",
|
||||
documentId: input.documentId,
|
||||
mindmapId: input.mindmapId,
|
||||
commands: input.commands,
|
||||
projectionRevision: input.projectionRevision ?? null,
|
||||
}),
|
||||
});
|
||||
const raw = (await response.json().catch(() => null)) as unknown;
|
||||
if (!response.ok) {
|
||||
throw new MindmapCommandBridgeError(`command_failed:${response.status}`, response.status);
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
kernelRevision: readKernelRevision(raw),
|
||||
raw,
|
||||
};
|
||||
};
|
||||
|
||||
export const executeMindmapCommandApplyAndRefreshProjection = async (
|
||||
input: MindmapCommandApplyInput & { projectionEndpoint?: string },
|
||||
): Promise<MindmapAdapterProjection> => {
|
||||
await executeMindmapCommandApply(input);
|
||||
return requestMindmapAdapterProjection({
|
||||
documentId: input.documentId,
|
||||
mindmapId: input.mindmapId,
|
||||
endpoint: input.projectionEndpoint,
|
||||
fetcher: input.fetcher,
|
||||
});
|
||||
};
|
||||
|
||||
export const createLeptosMindmapAdapter = async (
|
||||
input: LeptosMindmapAdapterOptions,
|
||||
): Promise<SimpleMindMapBridge> => {
|
||||
if (!isMindmapAdapterProjection(input.projection)) {
|
||||
throw new Error("adapter_init_failed:invalid_projection");
|
||||
}
|
||||
return createSimpleMindMapBridge(input);
|
||||
};
|
||||
Reference in New Issue
Block a user