feat(lesson-preparation): add AI evaluation, analytics, attachments, calendar, comments, review, substitutes, formative, and version diff
- Add actions-ai-evaluation, actions-analytics, actions-attachments, actions-calendar, actions-comments, actions-formative, actions-questions, actions-review, actions-substitutes - Add corresponding data-access layers for each new action module - Add calendar-view, curriculum-map-view, version-diff-viewer components - Add editor-slice, selection-slice, version-slice hooks for state management - Add document-diff and scope-check lib utilities - Add default-question-service and external-questions-bridge services
This commit is contained in:
257
src/modules/lesson-preparation/hooks/editor-slice.ts
Normal file
257
src/modules/lesson-preparation/hooks/editor-slice.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
import type { StateCreator } from "zustand";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import type {
|
||||
AnchorEdge,
|
||||
AnchorType,
|
||||
AnyLessonPlanEdge,
|
||||
Block,
|
||||
BlockType,
|
||||
FlowEdge,
|
||||
LessonPlanDocument,
|
||||
LessonPlanNode,
|
||||
NodeAnchor,
|
||||
TextbookContentNode,
|
||||
TextbookContentNodeData,
|
||||
} from "../types";
|
||||
import { defaultDataForType } from "../lib/document-migration";
|
||||
import type { EditorState } from "./use-lesson-plan-editor";
|
||||
|
||||
export interface EditorSlice {
|
||||
planId: string;
|
||||
title: string;
|
||||
doc: LessonPlanDocument;
|
||||
setTitle: (title: string) => void;
|
||||
setPlanId: (planId: string) => void;
|
||||
addNode: (type: BlockType, position?: { x: number; y: number }, title?: string) => string;
|
||||
updateNode: (id: string, patch: Omit<Partial<Block>, "type">) => void;
|
||||
updateNodePosition: (id: string, position: { x: number; y: number }) => void;
|
||||
removeNode: (id: string) => void;
|
||||
updateTextbookContent: (data: Partial<TextbookContentNodeData>) => void;
|
||||
getTextbookContentNode: () => TextbookContentNode | undefined;
|
||||
addAnchor: (params: {
|
||||
nodeId: string;
|
||||
type: AnchorType;
|
||||
start: number;
|
||||
end?: number;
|
||||
textPreview?: string;
|
||||
}) => string;
|
||||
removeAnchor: (anchorId: string) => void;
|
||||
updateAnchor: (anchorId: string, patch: Partial<NodeAnchor>) => void;
|
||||
connect: (source: string, target: string) => void;
|
||||
disconnect: (edgeId: string) => void;
|
||||
setEdges: (edges: AnyLessonPlanEdge[]) => void;
|
||||
}
|
||||
|
||||
function reindex(nodes: LessonPlanNode[]): LessonPlanNode[] {
|
||||
return nodes.map((n, i) => ({ ...n, order: i }));
|
||||
}
|
||||
|
||||
export const createEditorSlice: StateCreator<
|
||||
EditorState,
|
||||
[],
|
||||
[],
|
||||
EditorSlice
|
||||
> = (set, get) => ({
|
||||
planId: "",
|
||||
title: "",
|
||||
doc: {
|
||||
version: 3,
|
||||
textbookContentNodeId: "",
|
||||
nodes: [],
|
||||
edges: [],
|
||||
anchors: [],
|
||||
},
|
||||
|
||||
setTitle: (title) => set({ title, isDirty: true }),
|
||||
setPlanId: (planId) => set({ planId }),
|
||||
|
||||
addNode: (type, position, title) => {
|
||||
const id = createId();
|
||||
const state = get();
|
||||
const teachingNodes = state.doc.nodes.filter(
|
||||
(n): n is LessonPlanNode => n.type !== "textbook_content",
|
||||
);
|
||||
const nodeCount = teachingNodes.length;
|
||||
const node: LessonPlanNode = {
|
||||
id,
|
||||
type,
|
||||
title: title ?? type,
|
||||
data: defaultDataForType(type),
|
||||
order: nodeCount,
|
||||
position: position ?? {
|
||||
x: 80 + (nodeCount % 4) * 280,
|
||||
y: 80 + Math.floor(nodeCount / 4) * 200,
|
||||
},
|
||||
};
|
||||
set((s) => ({
|
||||
doc: { ...s.doc, nodes: [...s.doc.nodes, node] },
|
||||
isDirty: true,
|
||||
selectedNodeId: id,
|
||||
}));
|
||||
return id;
|
||||
},
|
||||
|
||||
updateNode: (id, patch) =>
|
||||
set((s) => ({
|
||||
doc: {
|
||||
...s.doc,
|
||||
nodes: s.doc.nodes.map((n) =>
|
||||
n.id === id
|
||||
? n.type === "textbook_content"
|
||||
? ({ ...n, ...patch } as TextbookContentNode)
|
||||
: ({ ...n, ...patch } as LessonPlanNode)
|
||||
: n,
|
||||
),
|
||||
},
|
||||
isDirty: true,
|
||||
})),
|
||||
|
||||
updateNodePosition: (id, position) =>
|
||||
set((s) => ({
|
||||
doc: {
|
||||
...s.doc,
|
||||
nodes: s.doc.nodes.map((n) =>
|
||||
n.id === id
|
||||
? n.type === "textbook_content"
|
||||
? ({ ...n, position } as TextbookContentNode)
|
||||
: ({ ...n, position } as LessonPlanNode)
|
||||
: n,
|
||||
),
|
||||
},
|
||||
isDirty: true,
|
||||
})),
|
||||
|
||||
removeNode: (id) =>
|
||||
set((s) => {
|
||||
const remainingTeachingNodes = reindex(
|
||||
s.doc.nodes.filter(
|
||||
(n): n is LessonPlanNode => n.id !== id && n.type !== "textbook_content",
|
||||
),
|
||||
);
|
||||
const textbookNode = s.doc.nodes.find(
|
||||
(n): n is TextbookContentNode => n.type === "textbook_content",
|
||||
);
|
||||
const nodes = textbookNode
|
||||
? [textbookNode, ...remainingTeachingNodes]
|
||||
: remainingTeachingNodes;
|
||||
return {
|
||||
doc: {
|
||||
...s.doc,
|
||||
nodes,
|
||||
edges: s.doc.edges.filter(
|
||||
(e) => e.source !== id && e.target !== id,
|
||||
),
|
||||
anchors: s.doc.anchors.filter((a) => a.nodeId !== id),
|
||||
},
|
||||
isDirty: true,
|
||||
selectedNodeId: s.selectedNodeId === id ? null : s.selectedNodeId,
|
||||
};
|
||||
}),
|
||||
|
||||
updateTextbookContent: (data) =>
|
||||
set((s) => ({
|
||||
doc: {
|
||||
...s.doc,
|
||||
nodes: s.doc.nodes.map((n) =>
|
||||
n.type === "textbook_content" && n.id === s.doc.textbookContentNodeId
|
||||
? { ...n, data: { ...n.data, ...data } }
|
||||
: n,
|
||||
),
|
||||
},
|
||||
isDirty: true,
|
||||
})),
|
||||
|
||||
getTextbookContentNode: () => {
|
||||
const state = get();
|
||||
return state.doc.nodes.find(
|
||||
(n): n is TextbookContentNode => n.type === "textbook_content",
|
||||
);
|
||||
},
|
||||
|
||||
addAnchor: ({ nodeId, type, start, end, textPreview }) => {
|
||||
const anchorId = createId();
|
||||
const state = get();
|
||||
const textbookNodeId = state.doc.textbookContentNodeId;
|
||||
|
||||
const anchor: NodeAnchor = {
|
||||
id: anchorId,
|
||||
nodeId,
|
||||
type,
|
||||
start,
|
||||
...(end !== undefined ? { end } : {}),
|
||||
...(textPreview ? { textPreview } : {}),
|
||||
};
|
||||
|
||||
const edge: AnchorEdge = {
|
||||
id: `ae_${nodeId}_${textbookNodeId}_${anchorId.slice(0, 6)}`,
|
||||
source: nodeId,
|
||||
target: textbookNodeId,
|
||||
type: "anchor",
|
||||
anchorId,
|
||||
};
|
||||
|
||||
set((s) => ({
|
||||
doc: {
|
||||
...s.doc,
|
||||
anchors: [...s.doc.anchors, anchor],
|
||||
edges: [...s.doc.edges, edge],
|
||||
},
|
||||
isDirty: true,
|
||||
}));
|
||||
|
||||
return anchorId;
|
||||
},
|
||||
|
||||
removeAnchor: (anchorId) =>
|
||||
set((s) => ({
|
||||
doc: {
|
||||
...s.doc,
|
||||
anchors: s.doc.anchors.filter((a) => a.id !== anchorId),
|
||||
edges: s.doc.edges.filter(
|
||||
(e) => !(e.type === "anchor" && e.anchorId === anchorId),
|
||||
),
|
||||
},
|
||||
isDirty: true,
|
||||
})),
|
||||
|
||||
updateAnchor: (anchorId, patch) =>
|
||||
set((s) => ({
|
||||
doc: {
|
||||
...s.doc,
|
||||
anchors: s.doc.anchors.map((a) =>
|
||||
a.id === anchorId ? { ...a, ...patch } : a,
|
||||
),
|
||||
},
|
||||
isDirty: true,
|
||||
})),
|
||||
|
||||
connect: (source, target) =>
|
||||
set((s) => {
|
||||
if (
|
||||
s.doc.edges.some((e) => e.source === source && e.target === target)
|
||||
)
|
||||
return s;
|
||||
const edge: FlowEdge = {
|
||||
id: `e_${source}_${target}_${createId().slice(0, 6)}`,
|
||||
source,
|
||||
target,
|
||||
type: "flow",
|
||||
};
|
||||
return {
|
||||
doc: { ...s.doc, edges: [...s.doc.edges, edge] },
|
||||
isDirty: true,
|
||||
};
|
||||
}),
|
||||
|
||||
disconnect: (edgeId) =>
|
||||
set((s) => ({
|
||||
doc: {
|
||||
...s.doc,
|
||||
edges: s.doc.edges.filter((e) => e.id !== edgeId),
|
||||
},
|
||||
isDirty: true,
|
||||
})),
|
||||
|
||||
setEdges: (edges) =>
|
||||
set((s) => ({ doc: { ...s.doc, edges }, isDirty: true })),
|
||||
});
|
||||
17
src/modules/lesson-preparation/hooks/selection-slice.ts
Normal file
17
src/modules/lesson-preparation/hooks/selection-slice.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { StateCreator } from "zustand";
|
||||
import type { EditorState } from "./use-lesson-plan-editor";
|
||||
|
||||
export interface SelectionSlice {
|
||||
selectedNodeId: string | null;
|
||||
selectNode: (id: string | null) => void;
|
||||
}
|
||||
|
||||
export const createSelectionSlice: StateCreator<
|
||||
EditorState,
|
||||
[],
|
||||
[],
|
||||
SelectionSlice
|
||||
> = (set) => ({
|
||||
selectedNodeId: null,
|
||||
selectNode: (id) => set({ selectedNodeId: id }),
|
||||
});
|
||||
@@ -1,303 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { create } from "zustand";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import type {
|
||||
AnchorEdge,
|
||||
AnchorType,
|
||||
AnyLessonPlanEdge,
|
||||
Block,
|
||||
BlockType,
|
||||
FlowEdge,
|
||||
LessonPlanDocument,
|
||||
LessonPlanNode,
|
||||
NodeAnchor,
|
||||
TextbookContentNode,
|
||||
TextbookContentNodeData,
|
||||
} from "../types";
|
||||
import { defaultDataForType } from "../lib/document-migration";
|
||||
import { createEditorSlice, type EditorSlice } from "./editor-slice";
|
||||
import { createSelectionSlice, type SelectionSlice } from "./selection-slice";
|
||||
import { createVersionSlice, type VersionSlice } from "./version-slice";
|
||||
|
||||
interface EditorState {
|
||||
planId: string;
|
||||
title: string;
|
||||
doc: LessonPlanDocument;
|
||||
isDirty: boolean;
|
||||
isSaving: boolean;
|
||||
lastSavedAt: number | null;
|
||||
selectedNodeId: string | null;
|
||||
/**
|
||||
* V4 P2-6 修复:将单体 Zustand store(原 303 行)拆分为 3 个独立 slice。
|
||||
*
|
||||
* - editor-slice: 文档结构(planId/title/doc)及所有文档操作方法
|
||||
* - selection-slice: 节点选中状态(selectedNodeId / selectNode)
|
||||
* - version-slice: 草稿版本与保存状态(isDirty/isSaving/lastSavedAt/hydrate/markSaved/replaceDoc)
|
||||
*
|
||||
* 主文件仅负责组合 slice 并导出统一的 EditorState 类型,方便测试与维护。
|
||||
* 各 slice 通过 `import type { EditorState }` 引用合并后的类型,TypeScript
|
||||
* 编译后该类型导入会被完全移除,运行时无循环依赖。
|
||||
*/
|
||||
export type EditorState = EditorSlice & SelectionSlice & VersionSlice;
|
||||
|
||||
setTitle: (title: string) => void;
|
||||
setPlanId: (planId: string) => void;
|
||||
hydrate: (planId: string, title: string, doc: LessonPlanDocument) => void;
|
||||
|
||||
addNode: (type: BlockType, position?: { x: number; y: number }, title?: string) => string;
|
||||
// V3 修复:patch 排除 type 字段,防止改变节点类型,同时消除 as 断言
|
||||
updateNode: (id: string, patch: Omit<Partial<Block>, "type">) => void;
|
||||
updateNodePosition: (id: string, position: { x: number; y: number }) => void;
|
||||
removeNode: (id: string) => void;
|
||||
|
||||
// 正文节点操作
|
||||
updateTextbookContent: (data: Partial<TextbookContentNodeData>) => void;
|
||||
getTextbookContentNode: () => TextbookContentNode | undefined;
|
||||
|
||||
// 锚点操作
|
||||
addAnchor: (params: {
|
||||
nodeId: string;
|
||||
type: AnchorType;
|
||||
start: number;
|
||||
end?: number;
|
||||
textPreview?: string;
|
||||
}) => string;
|
||||
removeAnchor: (anchorId: string) => void;
|
||||
updateAnchor: (anchorId: string, patch: Partial<NodeAnchor>) => void;
|
||||
|
||||
// 连线
|
||||
connect: (source: string, target: string) => void;
|
||||
disconnect: (edgeId: string) => void;
|
||||
setEdges: (edges: AnyLessonPlanEdge[]) => void;
|
||||
|
||||
selectNode: (id: string | null) => void;
|
||||
|
||||
markSaved: () => void;
|
||||
setSaving: (saving: boolean) => void;
|
||||
replaceDoc: (doc: LessonPlanDocument) => void;
|
||||
}
|
||||
|
||||
function reindex(nodes: LessonPlanNode[]): LessonPlanNode[] {
|
||||
return nodes.map((n, i) => ({ ...n, order: i }));
|
||||
}
|
||||
|
||||
export const useLessonPlanEditor = create<EditorState>((set, get) => ({
|
||||
planId: "",
|
||||
title: "",
|
||||
doc: {
|
||||
version: 3,
|
||||
textbookContentNodeId: "",
|
||||
nodes: [],
|
||||
edges: [],
|
||||
anchors: [],
|
||||
},
|
||||
isDirty: false,
|
||||
isSaving: false,
|
||||
lastSavedAt: null,
|
||||
selectedNodeId: null,
|
||||
|
||||
setTitle: (title) => set({ title, isDirty: true }),
|
||||
|
||||
setPlanId: (planId) => set({ planId }),
|
||||
|
||||
// 仅在 planId 变化时调用,避免覆盖用户编辑内容(修复 P1-3)
|
||||
hydrate: (planId, title, doc) =>
|
||||
set({
|
||||
planId,
|
||||
title,
|
||||
doc,
|
||||
isDirty: false,
|
||||
lastSavedAt: Date.now(),
|
||||
selectedNodeId: null,
|
||||
}),
|
||||
|
||||
addNode: (type, position, title) => {
|
||||
const id = createId();
|
||||
const state = get();
|
||||
// 教学节点 order 从 0 开始(正文节点 order=-1 不计入)
|
||||
const teachingNodes = state.doc.nodes.filter(
|
||||
(n): n is LessonPlanNode => n.type !== "textbook_content",
|
||||
);
|
||||
const nodeCount = teachingNodes.length;
|
||||
const node: LessonPlanNode = {
|
||||
id,
|
||||
type,
|
||||
title: title ?? type,
|
||||
data: defaultDataForType(type),
|
||||
order: nodeCount,
|
||||
position: position ?? {
|
||||
x: 80 + (nodeCount % 4) * 280,
|
||||
y: 80 + Math.floor(nodeCount / 4) * 200,
|
||||
},
|
||||
};
|
||||
set((s) => ({
|
||||
doc: { ...s.doc, nodes: [...s.doc.nodes, node] },
|
||||
isDirty: true,
|
||||
selectedNodeId: id,
|
||||
}));
|
||||
return id;
|
||||
},
|
||||
|
||||
updateNode: (id, patch) =>
|
||||
set((s) => ({
|
||||
doc: {
|
||||
...s.doc,
|
||||
// V3 修复:patch 已排除 type 字段,但 TypeScript 仍会因 spread 拓宽 data 类型
|
||||
// (BlockData 联合不包含 TextbookContentNodeData)而报错,此处 as 为必要断言。
|
||||
// 实际安全:调用方不会对 textbook_content 节点通过 updateNode 传入 data。
|
||||
nodes: s.doc.nodes.map((n) =>
|
||||
n.id === id
|
||||
? n.type === "textbook_content"
|
||||
? ({ ...n, ...patch } as TextbookContentNode)
|
||||
: ({ ...n, ...patch } as LessonPlanNode)
|
||||
: n,
|
||||
),
|
||||
},
|
||||
isDirty: true,
|
||||
})),
|
||||
|
||||
// 实时拖动:每次调用立即更新位置(不再等待 dragging=false)
|
||||
updateNodePosition: (id, position) =>
|
||||
set((s) => ({
|
||||
doc: {
|
||||
...s.doc,
|
||||
// 同 updateNode:spread 后 TypeScript 拓宽类型,需 as 断言收窄
|
||||
nodes: s.doc.nodes.map((n) =>
|
||||
n.id === id
|
||||
? n.type === "textbook_content"
|
||||
? ({ ...n, position } as TextbookContentNode)
|
||||
: ({ ...n, position } as LessonPlanNode)
|
||||
: n,
|
||||
),
|
||||
},
|
||||
isDirty: true,
|
||||
})),
|
||||
|
||||
removeNode: (id) =>
|
||||
set((s) => {
|
||||
const remainingTeachingNodes = reindex(
|
||||
s.doc.nodes.filter(
|
||||
(n): n is LessonPlanNode => n.id !== id && n.type !== "textbook_content",
|
||||
),
|
||||
);
|
||||
const textbookNode = s.doc.nodes.find(
|
||||
(n): n is TextbookContentNode => n.type === "textbook_content",
|
||||
);
|
||||
const nodes = textbookNode ? [textbookNode, ...remainingTeachingNodes] : remainingTeachingNodes;
|
||||
return {
|
||||
doc: {
|
||||
...s.doc,
|
||||
nodes,
|
||||
edges: s.doc.edges.filter(
|
||||
(e) => e.source !== id && e.target !== id,
|
||||
),
|
||||
// 同时移除关联的锚点
|
||||
anchors: s.doc.anchors.filter((a) => a.nodeId !== id),
|
||||
},
|
||||
isDirty: true,
|
||||
selectedNodeId:
|
||||
s.selectedNodeId === id ? null : s.selectedNodeId,
|
||||
};
|
||||
}),
|
||||
|
||||
// ---- 正文节点操作 ----
|
||||
updateTextbookContent: (data) =>
|
||||
set((s) => ({
|
||||
doc: {
|
||||
...s.doc,
|
||||
nodes: s.doc.nodes.map((n) =>
|
||||
n.type === "textbook_content" && n.id === s.doc.textbookContentNodeId
|
||||
? { ...n, data: { ...n.data, ...data } }
|
||||
: n,
|
||||
),
|
||||
},
|
||||
isDirty: true,
|
||||
})),
|
||||
|
||||
getTextbookContentNode: () => {
|
||||
const state = get();
|
||||
return state.doc.nodes.find(
|
||||
(n): n is TextbookContentNode => n.type === "textbook_content",
|
||||
);
|
||||
},
|
||||
|
||||
// ---- 锚点操作 ----
|
||||
addAnchor: ({ nodeId, type, start, end, textPreview }) => {
|
||||
const anchorId = createId();
|
||||
const state = get();
|
||||
const textbookNodeId = state.doc.textbookContentNodeId;
|
||||
|
||||
const anchor: NodeAnchor = {
|
||||
id: anchorId,
|
||||
nodeId,
|
||||
type,
|
||||
start,
|
||||
...(end !== undefined ? { end } : {}),
|
||||
...(textPreview ? { textPreview } : {}),
|
||||
};
|
||||
|
||||
const edge: AnchorEdge = {
|
||||
id: `ae_${nodeId}_${textbookNodeId}_${anchorId.slice(0, 6)}`,
|
||||
source: nodeId,
|
||||
target: textbookNodeId,
|
||||
type: "anchor",
|
||||
anchorId,
|
||||
};
|
||||
|
||||
set((s) => ({
|
||||
doc: {
|
||||
...s.doc,
|
||||
anchors: [...s.doc.anchors, anchor],
|
||||
edges: [...s.doc.edges, edge],
|
||||
},
|
||||
isDirty: true,
|
||||
}));
|
||||
|
||||
return anchorId;
|
||||
},
|
||||
|
||||
removeAnchor: (anchorId) =>
|
||||
set((s) => ({
|
||||
doc: {
|
||||
...s.doc,
|
||||
anchors: s.doc.anchors.filter((a) => a.id !== anchorId),
|
||||
edges: s.doc.edges.filter(
|
||||
(e) => !(e.type === "anchor" && e.anchorId === anchorId),
|
||||
),
|
||||
},
|
||||
isDirty: true,
|
||||
})),
|
||||
|
||||
updateAnchor: (anchorId, patch) =>
|
||||
set((s) => ({
|
||||
doc: {
|
||||
...s.doc,
|
||||
anchors: s.doc.anchors.map((a) =>
|
||||
a.id === anchorId ? { ...a, ...patch } : a,
|
||||
),
|
||||
},
|
||||
isDirty: true,
|
||||
})),
|
||||
|
||||
// ---- 连线 ----
|
||||
connect: (source, target) =>
|
||||
set((s) => {
|
||||
// 避免重复连线
|
||||
if (
|
||||
s.doc.edges.some(
|
||||
(e) => e.source === source && e.target === target,
|
||||
)
|
||||
)
|
||||
return s;
|
||||
const edge: FlowEdge = {
|
||||
id: `e_${source}_${target}_${createId().slice(0, 6)}`,
|
||||
source,
|
||||
target,
|
||||
type: "flow",
|
||||
};
|
||||
return { doc: { ...s.doc, edges: [...s.doc.edges, edge] }, isDirty: true };
|
||||
}),
|
||||
|
||||
disconnect: (edgeId) =>
|
||||
set((s) => ({
|
||||
doc: {
|
||||
...s.doc,
|
||||
edges: s.doc.edges.filter((e) => e.id !== edgeId),
|
||||
},
|
||||
isDirty: true,
|
||||
})),
|
||||
|
||||
setEdges: (edges) => set((s) => ({ doc: { ...s.doc, edges }, isDirty: true })),
|
||||
|
||||
selectNode: (id) => set({ selectedNodeId: id }),
|
||||
|
||||
markSaved: () => set({ isDirty: false, lastSavedAt: Date.now() }),
|
||||
setSaving: (saving) => set({ isSaving: saving }),
|
||||
replaceDoc: (doc) => set({ doc, isDirty: false }),
|
||||
export const useLessonPlanEditor = create<EditorState>()((...a) => ({
|
||||
...createEditorSlice(...a),
|
||||
...createSelectionSlice(...a),
|
||||
...createVersionSlice(...a),
|
||||
}));
|
||||
|
||||
38
src/modules/lesson-preparation/hooks/version-slice.ts
Normal file
38
src/modules/lesson-preparation/hooks/version-slice.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { StateCreator } from "zustand";
|
||||
import type { LessonPlanDocument } from "../types";
|
||||
import type { EditorState } from "./use-lesson-plan-editor";
|
||||
|
||||
export interface VersionSlice {
|
||||
isDirty: boolean;
|
||||
isSaving: boolean;
|
||||
lastSavedAt: number | null;
|
||||
hydrate: (planId: string, title: string, doc: LessonPlanDocument) => void;
|
||||
markSaved: () => void;
|
||||
setSaving: (saving: boolean) => void;
|
||||
replaceDoc: (doc: LessonPlanDocument) => void;
|
||||
}
|
||||
|
||||
export const createVersionSlice: StateCreator<
|
||||
EditorState,
|
||||
[],
|
||||
[],
|
||||
VersionSlice
|
||||
> = (set) => ({
|
||||
isDirty: false,
|
||||
isSaving: false,
|
||||
lastSavedAt: null,
|
||||
|
||||
hydrate: (planId, title, doc) =>
|
||||
set({
|
||||
planId,
|
||||
title,
|
||||
doc,
|
||||
isDirty: false,
|
||||
lastSavedAt: Date.now(),
|
||||
selectedNodeId: null,
|
||||
}),
|
||||
|
||||
markSaved: () => set({ isDirty: false, lastSavedAt: Date.now() }),
|
||||
setSaving: (saving) => set({ isSaving: saving }),
|
||||
replaceDoc: (doc) => set({ doc, isDirty: false }),
|
||||
});
|
||||
Reference in New Issue
Block a user