|
|
@@ -12,6 +12,7 @@
|
|
12
|
12
|
|
|
13
|
13
|
import { create } from 'zustand';
|
|
14
|
14
|
import pLimit from 'p-limit';
|
|
|
15
|
+import hashSum from 'hash-sum';
|
|
15
|
16
|
import { v4 as uuidv4 } from 'uuid';
|
|
16
|
17
|
import { message } from 'antd';
|
|
17
|
18
|
import type {
|
|
|
@@ -23,20 +24,11 @@ import type {
|
|
23
|
24
|
} from '../types/editor';
|
|
24
|
25
|
import { blockService } from '../services/blockService';
|
|
25
|
26
|
import { getErrorMessage, isApiError } from '../services/api';
|
|
26
|
|
-import { normalizeTableBlock, validateTableStructure } from '../utils/blockOperations';
|
|
27
|
27
|
import {
|
|
28
|
|
- computeBlockHash,
|
|
29
|
|
- computeInsertOrder,
|
|
30
|
|
- isCanceledRequest,
|
|
31
|
|
- isReadonlyBlock,
|
|
32
|
|
- mergeBlockUpdates,
|
|
33
|
|
- rebalanceOrders,
|
|
34
|
|
- serializeBlockForSave,
|
|
35
|
|
- serializePendingBlockUpdate,
|
|
36
|
|
- snapshotBlocks,
|
|
37
|
|
- waitForStructuralOperations,
|
|
38
|
|
-} from '../utils/editorStoreHelpers';
|
|
39
|
|
-import type { EditorHistoryEntry, EditorStore } from './editorStoreTypes';
|
|
|
28
|
+ normalizeTableBlock,
|
|
|
29
|
+ serializeTableBlock,
|
|
|
30
|
+ validateTableStructure,
|
|
|
31
|
+} from '../utils/blockOperations';
|
|
40
|
32
|
|
|
41
|
33
|
// 并发保存限制器(最多同时进行 3 个请求,降低服务器压力)
|
|
42
|
34
|
const saveConcurrencyLimit = pLimit(3);
|
|
|
@@ -50,6 +42,350 @@ const MAX_RETRY_ATTEMPTS = 3;
|
|
50
|
42
|
const MAX_HISTORY_ENTRIES = 100;
|
|
51
|
43
|
const HISTORY_COALESCE_WINDOW = 500;
|
|
52
|
44
|
|
|
|
45
|
+function waitForStructuralOperations(isReady: () => boolean): Promise<void> {
|
|
|
46
|
+ return new Promise((resolve) => {
|
|
|
47
|
+ const check = () => {
|
|
|
48
|
+ if (isReady()) {
|
|
|
49
|
+ resolve();
|
|
|
50
|
+ return;
|
|
|
51
|
+ }
|
|
|
52
|
+ setTimeout(check, 16);
|
|
|
53
|
+ };
|
|
|
54
|
+ check();
|
|
|
55
|
+ });
|
|
|
56
|
+}
|
|
|
57
|
+
|
|
|
58
|
+interface EditorHistoryEntry {
|
|
|
59
|
+ blocks: DocumentBlock[];
|
|
|
60
|
+ selectedBlockId: string | null;
|
|
|
61
|
+}
|
|
|
62
|
+
|
|
|
63
|
+function snapshotBlocks(blocks: DocumentBlock[]): DocumentBlock[] {
|
|
|
64
|
+ // 块更新采用不可变替换,历史快照只需复制数组即可复用块对象。
|
|
|
65
|
+ return blocks.slice();
|
|
|
66
|
+}
|
|
|
67
|
+
|
|
|
68
|
+/**
|
|
|
69
|
+ * 计算块内容的哈希值
|
|
|
70
|
+ * 用于检测内容是否真的发生了变化
|
|
|
71
|
+ */
|
|
|
72
|
+function computeBlockHash(block: DocumentBlock): string {
|
|
|
73
|
+ // 只计算可编辑字段的哈希,忽略 id 等不可变元数据
|
|
|
74
|
+ const contentForHash = {
|
|
|
75
|
+ type: block.type,
|
|
|
76
|
+ content: block.content,
|
|
|
77
|
+ style: block.style,
|
|
|
78
|
+ word_style: block.word_style,
|
|
|
79
|
+ level: block.level,
|
|
|
80
|
+ block_order: block.block_order,
|
|
|
81
|
+ };
|
|
|
82
|
+ return hashSum(contentForHash);
|
|
|
83
|
+}
|
|
|
84
|
+
|
|
|
85
|
+function isReadonlyBlock(block: DocumentBlock): boolean {
|
|
|
86
|
+ const metadata: object = block.metadata;
|
|
|
87
|
+ return (
|
|
|
88
|
+ ('readonly' in metadata && metadata.readonly === true) ||
|
|
|
89
|
+ ('is_auto_generated' in metadata && metadata.is_auto_generated === true)
|
|
|
90
|
+ );
|
|
|
91
|
+}
|
|
|
92
|
+
|
|
|
93
|
+function isCanceledRequest(error: unknown): boolean {
|
|
|
94
|
+ return (
|
|
|
95
|
+ error instanceof Error &&
|
|
|
96
|
+ (error.name === 'AbortError' ||
|
|
|
97
|
+ error.name === 'CanceledError' ||
|
|
|
98
|
+ error.message.toLowerCase().includes('cancel'))
|
|
|
99
|
+ );
|
|
|
100
|
+}
|
|
|
101
|
+
|
|
|
102
|
+function serializeBlockForSave(block: DocumentBlock): Pick<BlockUpdate, 'content' | 'style'> {
|
|
|
103
|
+ if (block.type === 'table') {
|
|
|
104
|
+ const serializedTable = serializeTableBlock(block as TableBlock);
|
|
|
105
|
+ return {
|
|
|
106
|
+ content: serializedTable.content,
|
|
|
107
|
+ style: block.style,
|
|
|
108
|
+ };
|
|
|
109
|
+ }
|
|
|
110
|
+
|
|
|
111
|
+ if ((block.type === 'heading' || block.type === 'paragraph') && Array.isArray(block.content)) {
|
|
|
112
|
+ const firstStyle = block.content[0]?.style;
|
|
|
113
|
+ const allSameStyle =
|
|
|
114
|
+ !!firstStyle &&
|
|
|
115
|
+ block.content.every(
|
|
|
116
|
+ (segment) => JSON.stringify(segment.style) === JSON.stringify(firstStyle)
|
|
|
117
|
+ );
|
|
|
118
|
+
|
|
|
119
|
+ return {
|
|
|
120
|
+ content: block.content.map((segment) => segment.text).join(''),
|
|
|
121
|
+ style:
|
|
|
122
|
+ allSameStyle && Object.keys(firstStyle).length > 0
|
|
|
123
|
+ ? { ...block.style, ...firstStyle }
|
|
|
124
|
+ : block.style,
|
|
|
125
|
+ };
|
|
|
126
|
+ }
|
|
|
127
|
+
|
|
|
128
|
+ return {
|
|
|
129
|
+ content: block.content,
|
|
|
130
|
+ style: block.style,
|
|
|
131
|
+ };
|
|
|
132
|
+}
|
|
|
133
|
+
|
|
|
134
|
+function mergeBlockUpdates(previous: BlockUpdate | undefined, next: BlockUpdate): BlockUpdate {
|
|
|
135
|
+ return { ...(previous || {}), ...next };
|
|
|
136
|
+}
|
|
|
137
|
+
|
|
|
138
|
+function serializePendingBlockUpdate(
|
|
|
139
|
+ block: DocumentBlock,
|
|
|
140
|
+ pending: BlockUpdate | undefined
|
|
|
141
|
+): BlockUpdate {
|
|
|
142
|
+ if (!pending) {
|
|
|
143
|
+ const serialized = serializeBlockForSave(block);
|
|
|
144
|
+ return {
|
|
|
145
|
+ type: block.type,
|
|
|
146
|
+ level: block.level,
|
|
|
147
|
+ content: serialized.content,
|
|
|
148
|
+ style: serialized.style,
|
|
|
149
|
+ word_style: block.word_style,
|
|
|
150
|
+ metadata: block.metadata,
|
|
|
151
|
+ block_order: block.block_order,
|
|
|
152
|
+ };
|
|
|
153
|
+ }
|
|
|
154
|
+ const updates = pending || {};
|
|
|
155
|
+ const serialized = serializeBlockForSave(block);
|
|
|
156
|
+ const payload: BlockUpdate = {};
|
|
|
157
|
+
|
|
|
158
|
+ if ('type' in updates) payload.type = block.type;
|
|
|
159
|
+ if ('level' in updates) payload.level = block.level;
|
|
|
160
|
+ if ('content' in updates) payload.content = serialized.content;
|
|
|
161
|
+ if ('style' in updates) payload.style = serialized.style;
|
|
|
162
|
+ if ('word_style' in updates) payload.word_style = block.word_style;
|
|
|
163
|
+ if ('metadata' in updates) payload.metadata = block.metadata;
|
|
|
164
|
+ if ('block_order' in updates) payload.block_order = block.block_order;
|
|
|
165
|
+
|
|
|
166
|
+ return payload;
|
|
|
167
|
+}
|
|
|
168
|
+
|
|
|
169
|
+// ══════════════════════════════════════════════════════════════════════════════
|
|
|
170
|
+// Store State Interface
|
|
|
171
|
+// ══════════════════════════════════════════════════════════════════════════════
|
|
|
172
|
+
|
|
|
173
|
+interface EditorStore {
|
|
|
174
|
+ // ── 文档状态 ────────────────────────────────────────────────────────────
|
|
|
175
|
+ documentId: string | null;
|
|
|
176
|
+ documentTitle: string;
|
|
|
177
|
+ blocks: DocumentBlock[];
|
|
|
178
|
+ selectedBlockId: string | null;
|
|
|
179
|
+ focusRequestId: number;
|
|
|
180
|
+ past: EditorHistoryEntry[];
|
|
|
181
|
+ future: EditorHistoryEntry[];
|
|
|
182
|
+ lastHistoryEditBlockId: string | null;
|
|
|
183
|
+ lastHistoryEditAt: number | null;
|
|
|
184
|
+ isHistoryApplying: boolean;
|
|
|
185
|
+ pendingStructuralOperations: number;
|
|
|
186
|
+
|
|
|
187
|
+ // ── 加载/保存状态 ──────────────────────────────────────────────────────
|
|
|
188
|
+ isLoading: boolean;
|
|
|
189
|
+ isSaving: boolean;
|
|
|
190
|
+ error: string | null;
|
|
|
191
|
+
|
|
|
192
|
+ // ── 保存进度 ────────────────────────────────────────────────────────────
|
|
|
193
|
+ /** 正在保存的块数量 */
|
|
|
194
|
+ savingProgress: { current: number; total: number } | null;
|
|
|
195
|
+
|
|
|
196
|
+ // ── 修改状态追踪 ────────────────────────────────────────────────────────
|
|
|
197
|
+ /** 文档是否已被修改(用于控制保存按钮状态) */
|
|
|
198
|
+ hasModified: boolean;
|
|
|
199
|
+ /** 原始blocks快照(用于检测变化) */
|
|
|
200
|
+ originalBlocksSnapshot: string | null;
|
|
|
201
|
+ /** 保存失败的块ID列表 */
|
|
|
202
|
+ failedBlocks: string[];
|
|
|
203
|
+ /** 被修改但未保存的块ID集合 */
|
|
|
204
|
+ dirtyBlocks: Set<string>;
|
|
|
205
|
+ /** 上次成功保存的快照 */
|
|
|
206
|
+ lastSavedSnapshot: string | null;
|
|
|
207
|
+ /** 块内容哈希映射表(用于精确检测内容变化)*/
|
|
|
208
|
+ blockHashes: Map<string, string>;
|
|
|
209
|
+ /** 每个块尚未提交的字段变更,只在保存时转换为 API patch */
|
|
|
210
|
+ pendingBlockUpdates: Map<string, BlockUpdate>;
|
|
|
211
|
+
|
|
|
212
|
+ // ── 保存控制 ────────────────────────────────────────────────────────────
|
|
|
213
|
+ /** 当前正在进行的保存 Promise */
|
|
|
214
|
+ currentSavePromise: Promise<void> | null;
|
|
|
215
|
+ /** 用于取消请求的 AbortController */
|
|
|
216
|
+ saveAbortController: AbortController | null;
|
|
|
217
|
+ /** 用于取消文档加载请求的 AbortController */
|
|
|
218
|
+ loadAbortController: AbortController | null;
|
|
|
219
|
+
|
|
|
220
|
+ // ── 自动保存 ────────────────────────────────────────────────────────────
|
|
|
221
|
+ /** 自动保存定时器 */
|
|
|
222
|
+ autoSaveTimer: ReturnType<typeof setTimeout> | null;
|
|
|
223
|
+ /** 是否启用自动保存 */
|
|
|
224
|
+ autoSaveEnabled: boolean;
|
|
|
225
|
+ /** 上次保存时间戳 */
|
|
|
226
|
+ lastSaveTime: number | null;
|
|
|
227
|
+
|
|
|
228
|
+ // ── 重试机制 ────────────────────────────────────────────────────────────
|
|
|
229
|
+ /** 块重试次数记录 */
|
|
|
230
|
+ retryAttempts: Map<string, number>;
|
|
|
231
|
+ /** 延迟重试定时器 */
|
|
|
232
|
+ retryTimer: ReturnType<typeof setTimeout> | null;
|
|
|
233
|
+
|
|
|
234
|
+ // ── 操作方法 ────────────────────────────────────────────────────────────
|
|
|
235
|
+
|
|
|
236
|
+ /**
|
|
|
237
|
+ * 加载文档
|
|
|
238
|
+ */
|
|
|
239
|
+ loadDocument: (documentId: string) => Promise<void>;
|
|
|
240
|
+
|
|
|
241
|
+ /**
|
|
|
242
|
+ * 保存文档
|
|
|
243
|
+ */
|
|
|
244
|
+ saveDocument: () => Promise<void>;
|
|
|
245
|
+
|
|
|
246
|
+ /**
|
|
|
247
|
+ * 重试保存失败的块
|
|
|
248
|
+ */
|
|
|
249
|
+ retryFailedBlocks: () => Promise<void>;
|
|
|
250
|
+
|
|
|
251
|
+ /**
|
|
|
252
|
+ * 添加块
|
|
|
253
|
+ * @param block 块数据(部分字段)
|
|
|
254
|
+ * @param afterBlockId 插入位置(在此块之后),不传则追加到末尾
|
|
|
255
|
+ */
|
|
|
256
|
+ addBlock: (block: PartialBlock, afterBlockId?: string) => void;
|
|
|
257
|
+
|
|
|
258
|
+ /**
|
|
|
259
|
+ * 更新块
|
|
|
260
|
+ * @param id 块ID
|
|
|
261
|
+ * @param updates 更新数据
|
|
|
262
|
+ */
|
|
|
263
|
+ updateBlock: (id: string, updates: BlockUpdate) => void;
|
|
|
264
|
+
|
|
|
265
|
+ /** 应用来自聊天/WebMCP 的已保存块更新 */
|
|
|
266
|
+ applyRemoteBlockUpdate: (id: string, updates: BlockUpdate) => void;
|
|
|
267
|
+
|
|
|
268
|
+ /** 应用来自 WebMCP 的已保存块插入 */
|
|
|
269
|
+ applyRemoteBlockInsert: (block: DocumentBlock) => void;
|
|
|
270
|
+
|
|
|
271
|
+ /** 应用来自聊天/WebMCP 的已保存块删除 */
|
|
|
272
|
+ applyRemoteBlockDelete: (id: string) => void;
|
|
|
273
|
+
|
|
|
274
|
+ /**
|
|
|
275
|
+ * 标记文档已修改
|
|
|
276
|
+ */
|
|
|
277
|
+ markAsModified: () => void;
|
|
|
278
|
+
|
|
|
279
|
+ /**
|
|
|
280
|
+ * 标记文档已保存
|
|
|
281
|
+ */
|
|
|
282
|
+ markAsSaved: () => void;
|
|
|
283
|
+
|
|
|
284
|
+ /**
|
|
|
285
|
+ * 删除块
|
|
|
286
|
+ * @param id 块ID
|
|
|
287
|
+ */
|
|
|
288
|
+ deleteBlock: (id: string) => Promise<void>;
|
|
|
289
|
+
|
|
|
290
|
+ /**
|
|
|
291
|
+ * 移动块
|
|
|
292
|
+ * @param id 块ID
|
|
|
293
|
+ * @param targetOrder 目标位置
|
|
|
294
|
+ */
|
|
|
295
|
+ moveBlock: (id: string, targetOrder: number) => void;
|
|
|
296
|
+
|
|
|
297
|
+ /**
|
|
|
298
|
+ * 选中块
|
|
|
299
|
+ * @param id 块ID
|
|
|
300
|
+ */
|
|
|
301
|
+ selectBlock: (id: string | null) => void;
|
|
|
302
|
+
|
|
|
303
|
+ /**
|
|
|
304
|
+ * 根据ID获取块
|
|
|
305
|
+ */
|
|
|
306
|
+ getBlockById: (id: string) => DocumentBlock | undefined;
|
|
|
307
|
+
|
|
|
308
|
+ /**
|
|
|
309
|
+ * 根据类型获取块
|
|
|
310
|
+ */
|
|
|
311
|
+ getBlocksByType: (type: BlockType) => DocumentBlock[];
|
|
|
312
|
+
|
|
|
313
|
+ /**
|
|
|
314
|
+ * 检查块是否为脏块(已修改未保存)
|
|
|
315
|
+ */
|
|
|
316
|
+ isBlockDirty: (id: string) => boolean;
|
|
|
317
|
+
|
|
|
318
|
+ /**
|
|
|
319
|
+ * 获取所有脏块
|
|
|
320
|
+ */
|
|
|
321
|
+ getDirtyBlocks: () => DocumentBlock[];
|
|
|
322
|
+
|
|
|
323
|
+ /**
|
|
|
324
|
+ * 保存单个block的更改
|
|
|
325
|
+ */
|
|
|
326
|
+ saveBlock: (id: string) => Promise<void>;
|
|
|
327
|
+
|
|
|
328
|
+ /**
|
|
|
329
|
+ * 重新计算所有块的block_order(稀疏排序)
|
|
|
330
|
+ */
|
|
|
331
|
+ recomputeBlockOrders: () => void;
|
|
|
332
|
+
|
|
|
333
|
+ /**
|
|
|
334
|
+ * 启用/禁用自动保存
|
|
|
335
|
+ */
|
|
|
336
|
+ setAutoSaveEnabled: (enabled: boolean) => void;
|
|
|
337
|
+
|
|
|
338
|
+ /**
|
|
|
339
|
+ * 触发自动保存(带防抖)
|
|
|
340
|
+ */
|
|
|
341
|
+ triggerAutoSave: () => void;
|
|
|
342
|
+
|
|
|
343
|
+ /**
|
|
|
344
|
+ * 取消自动保存定时器
|
|
|
345
|
+ */
|
|
|
346
|
+ cancelAutoSave: () => void;
|
|
|
347
|
+
|
|
|
348
|
+ /**
|
|
|
349
|
+ * 重置状态
|
|
|
350
|
+ */
|
|
|
351
|
+ reset: () => void;
|
|
|
352
|
+ /** 撤销最近一次编辑 */
|
|
|
353
|
+ undo: () => Promise<void>;
|
|
|
354
|
+ /** 重做最近一次撤销 */
|
|
|
355
|
+ redo: () => Promise<void>;
|
|
|
356
|
+}
|
|
|
357
|
+
|
|
|
358
|
+// ══════════════════════════════════════════════════════════════════════════════
|
|
|
359
|
+// Utility Functions
|
|
|
360
|
+// ══════════════════════════════════════════════════════════════════════════════
|
|
|
361
|
+
|
|
|
362
|
+/**
|
|
|
363
|
+ * 计算插入位置的block_order
|
|
|
364
|
+ * 稀疏排序策略:在两个块之间找到中间值
|
|
|
365
|
+ */
|
|
|
366
|
+function computeInsertOrder(prevOrder: number, nextOrder: number): number {
|
|
|
367
|
+ const gap = nextOrder - prevOrder;
|
|
|
368
|
+
|
|
|
369
|
+ if (gap > 1) {
|
|
|
370
|
+ // 有间隙,直接取中间值
|
|
|
371
|
+ return Math.floor((prevOrder + nextOrder) / 2);
|
|
|
372
|
+ }
|
|
|
373
|
+
|
|
|
374
|
+ // 间隙不足,需要重排
|
|
|
375
|
+ return -1;
|
|
|
376
|
+}
|
|
|
377
|
+
|
|
|
378
|
+/**
|
|
|
379
|
+ * 重新平衡block_order(稀疏排序,间隔100)
|
|
|
380
|
+ */
|
|
|
381
|
+function rebalanceOrders(blocks: DocumentBlock[]): DocumentBlock[] {
|
|
|
382
|
+ const sorted = [...blocks].sort((a, b) => a.block_order - b.block_order);
|
|
|
383
|
+ return sorted.map((block, index) => ({
|
|
|
384
|
+ ...block,
|
|
|
385
|
+ block_order: index * 100,
|
|
|
386
|
+ }));
|
|
|
387
|
+}
|
|
|
388
|
+
|
|
53
|
389
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
54
|
390
|
// Store Implementation
|
|
55
|
391
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
|
@@ -319,14 +655,14 @@ export const useEditorStore = create<EditorStore>((set, get) => {
|
|
319
|
655
|
return;
|
|
320
|
656
|
}
|
|
321
|
657
|
|
|
322
|
|
- const dirtyBlocks = new Set<string>();
|
|
323
|
|
- const blocksToSave: DocumentBlock[] = [];
|
|
324
|
|
- for (const block of blocks) {
|
|
325
|
|
- if (currentDirtyBlocks.has(block.id) && block.type !== 'toc' && !isReadonlyBlock(block)) {
|
|
326
|
|
- dirtyBlocks.add(block.id);
|
|
327
|
|
- blocksToSave.push(block);
|
|
328
|
|
- }
|
|
329
|
|
- }
|
|
|
658
|
+ const dirtyBlocks = new Set(
|
|
|
659
|
+ blocks
|
|
|
660
|
+ .filter(
|
|
|
661
|
+ (block) =>
|
|
|
662
|
+ currentDirtyBlocks.has(block.id) && block.type !== 'toc' && !isReadonlyBlock(block)
|
|
|
663
|
+ )
|
|
|
664
|
+ .map((block) => block.id)
|
|
|
665
|
+ );
|
|
330
|
666
|
|
|
331
|
667
|
if (dirtyBlocks.size === 0) {
|
|
332
|
668
|
set({
|
|
|
@@ -367,6 +703,15 @@ export const useEditorStore = create<EditorStore>((set, get) => {
|
|
367
|
703
|
// 创建保存 Promise
|
|
368
|
704
|
const savePromise = (async () => {
|
|
369
|
705
|
try {
|
|
|
706
|
+ // 获取需要保存的块(只保存脏块)
|
|
|
707
|
+ const blocksToSave = blocks.filter((block) => {
|
|
|
708
|
+ // 必须在脏块列表中
|
|
|
709
|
+ if (!dirtyBlocks.has(block.id)) {
|
|
|
710
|
+ return false;
|
|
|
711
|
+ }
|
|
|
712
|
+
|
|
|
713
|
+ return true;
|
|
|
714
|
+ });
|
|
370
|
715
|
const pendingUpdatesAtSave = new Map(currentPendingBlockUpdates);
|
|
371
|
716
|
|
|
372
|
717
|
const totalBlocks = blocksToSave.length;
|
|
|
@@ -389,9 +734,12 @@ export const useEditorStore = create<EditorStore>((set, get) => {
|
|
389
|
734
|
);
|
|
390
|
735
|
|
|
391
|
736
|
try {
|
|
392
|
|
- const result = await blockService.updateBlock(documentId, block.id, payload, {
|
|
393
|
|
- signal: newAbortController.signal,
|
|
394
|
|
- });
|
|
|
737
|
+ const result = await blockService.updateBlock(
|
|
|
738
|
+ documentId,
|
|
|
739
|
+ block.id,
|
|
|
740
|
+ payload,
|
|
|
741
|
+ { signal: newAbortController.signal }
|
|
|
742
|
+ );
|
|
395
|
743
|
if (newAbortController.signal.aborted || get().documentId !== documentId) {
|
|
396
|
744
|
return result;
|
|
397
|
745
|
}
|
|
|
@@ -925,7 +1273,10 @@ export const useEditorStore = create<EditorStore>((set, get) => {
|
|
925
|
1273
|
if (originalHash && newHash === originalHash) {
|
|
926
|
1274
|
newPendingBlockUpdates.delete(id);
|
|
927
|
1275
|
} else {
|
|
928
|
|
- newPendingBlockUpdates.set(id, mergeBlockUpdates(pendingBlockUpdates.get(id), updates));
|
|
|
1276
|
+ newPendingBlockUpdates.set(
|
|
|
1277
|
+ id,
|
|
|
1278
|
+ mergeBlockUpdates(pendingBlockUpdates.get(id), updates)
|
|
|
1279
|
+ );
|
|
929
|
1280
|
}
|
|
930
|
1281
|
|
|
931
|
1282
|
set({
|
|
|
@@ -977,9 +1328,7 @@ export const useEditorStore = create<EditorStore>((set, get) => {
|
|
977
|
1328
|
const { documentId, blocks, blockHashes, dirtyBlocks, pendingBlockUpdates } = get();
|
|
978
|
1329
|
if (!documentId || blocks.some((currentBlock) => currentBlock.id === block.id)) return;
|
|
979
|
1330
|
|
|
980
|
|
- const updatedBlocks = [...blocks, block].sort(
|
|
981
|
|
- (left, right) => left.block_order - right.block_order
|
|
982
|
|
- );
|
|
|
1331
|
+ const updatedBlocks = [...blocks, block].sort((left, right) => left.block_order - right.block_order);
|
|
983
|
1332
|
const updatedHashes = new Map(blockHashes);
|
|
984
|
1333
|
updatedHashes.set(block.id, computeBlockHash(block));
|
|
985
|
1334
|
const snapshot = JSON.stringify(updatedBlocks);
|