@rowsncolumns/y-spreadsheet 14.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +100 -0
- package/Readme.md +77 -0
- package/dist/cjs/helpers.d.ts +179 -0
- package/dist/cjs/helpers.d.ts.map +1 -0
- package/dist/cjs/helpers.js +1635 -0
- package/dist/cjs/index.d.ts +6 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +8 -0
- package/dist/cjs/observables.d.ts +10 -0
- package/dist/cjs/observables.d.ts.map +1 -0
- package/dist/cjs/observables.js +90 -0
- package/dist/cjs/profiling/batch-profile.spec.d.ts +2 -0
- package/dist/cjs/profiling/batch-profile.spec.d.ts.map +1 -0
- package/dist/cjs/profiling/batch-profile.spec.js +175 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -0
- package/dist/cjs/types.d.ts +62 -0
- package/dist/cjs/types.d.ts.map +1 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/use-yspreadsheet-v2.d.ts +152 -0
- package/dist/cjs/use-yspreadsheet-v2.d.ts.map +1 -0
- package/dist/cjs/use-yspreadsheet-v2.js +980 -0
- package/dist/cjs/use-yspreadsheet.d.ts +39 -0
- package/dist/cjs/use-yspreadsheet.d.ts.map +1 -0
- package/dist/cjs/use-yspreadsheet.js +430 -0
- package/dist/cjs/vitest.config.d.ts +3 -0
- package/dist/cjs/vitest.config.d.ts.map +1 -0
- package/dist/cjs/vitest.config.js +23 -0
- package/dist/esm/helpers.d.ts +179 -0
- package/dist/esm/helpers.d.ts.map +1 -0
- package/dist/esm/helpers.js +1346 -0
- package/dist/esm/index.d.ts +6 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/observables.d.ts +10 -0
- package/dist/esm/observables.d.ts.map +1 -0
- package/dist/esm/observables.js +51 -0
- package/dist/esm/profiling/batch-profile.spec.d.ts +2 -0
- package/dist/esm/profiling/batch-profile.spec.d.ts.map +1 -0
- package/dist/esm/profiling/batch-profile.spec.js +131 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -0
- package/dist/esm/types.d.ts +62 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/use-yspreadsheet-v2.d.ts +152 -0
- package/dist/esm/use-yspreadsheet-v2.d.ts.map +1 -0
- package/dist/esm/use-yspreadsheet-v2.js +814 -0
- package/dist/esm/use-yspreadsheet.d.ts +39 -0
- package/dist/esm/use-yspreadsheet.d.ts.map +1 -0
- package/dist/esm/use-yspreadsheet.js +304 -0
- package/dist/esm/vitest.config.d.ts +3 -0
- package/dist/esm/vitest.config.d.ts.map +1 -0
- package/dist/esm/vitest.config.js +21 -0
- package/package.json +54 -0
|
@@ -0,0 +1,1346 @@
|
|
|
1
|
+
import { cellToAddress, getCellEffectiveValue, getCellUserEnteredValue, getExtendedValueBool, getExtendedValueError, getExtendedValueFormula, getExtendedValueNumber, getExtendedValueString, } from "@rowsncolumns/utils";
|
|
2
|
+
import * as Y from "yjs";
|
|
3
|
+
import cloneDeep from "lodash/cloneDeep";
|
|
4
|
+
import isEqual from "lodash/isEqual";
|
|
5
|
+
import set from "lodash/set";
|
|
6
|
+
import get from "lodash/get";
|
|
7
|
+
import unset from "lodash/unset";
|
|
8
|
+
/**
|
|
9
|
+
* Apply patches to yjs
|
|
10
|
+
* @param doc
|
|
11
|
+
* @param userId
|
|
12
|
+
* @param patches
|
|
13
|
+
* @param source
|
|
14
|
+
* @param sheetDataVersion - Version of sheet data format ("v2" or "v3"). Falls back to docMeta if undefined.
|
|
15
|
+
*/
|
|
16
|
+
export const applyPatchesToYDoc = (doc, userId, patches, source, sheetDataVersion) => {
|
|
17
|
+
const ySheetDataV2 = doc.getMap("sheetDataV2");
|
|
18
|
+
const ySheetDataV3 = doc.getMap("sheetDataV3");
|
|
19
|
+
const yDocMeta = doc.getMap("docMeta");
|
|
20
|
+
const yRecalcCells = doc.getArray("recalcCells");
|
|
21
|
+
const ySheets = doc.getArray("sheets");
|
|
22
|
+
const yTables = doc.getArray("tables");
|
|
23
|
+
const yEmbeds = doc.getArray("embeds");
|
|
24
|
+
const yCharts = doc.getArray("charts");
|
|
25
|
+
const yConditionalFormats = doc.getArray("conditionalFormats");
|
|
26
|
+
const yDataValidations = doc.getArray("dataValidations");
|
|
27
|
+
const yNamedRanges = doc.getArray("namedRanges");
|
|
28
|
+
const ySharedStringsMap = doc.getMap("sharedStringsMap");
|
|
29
|
+
const yProtectedRanges = doc.getArray("protectedRanges");
|
|
30
|
+
const yCellXfs = doc.getMap("cellXfs");
|
|
31
|
+
const yDisableRecalc = doc.getMap("disableRecalc");
|
|
32
|
+
// Check version flag to determine which sheet data format to use
|
|
33
|
+
// Use param if provided, otherwise fall back to docMeta for backwards compatibility
|
|
34
|
+
const resolvedVersion = sheetDataVersion ?? yDocMeta.get("sheetDataVersion");
|
|
35
|
+
const isV3 = resolvedVersion === "v3";
|
|
36
|
+
for (let [patch, type = "redo"] of patches) {
|
|
37
|
+
type = type ?? "redo";
|
|
38
|
+
const patchKey = type === "redo" ? "patches" : "inversePatches";
|
|
39
|
+
if (patch.sheetData) {
|
|
40
|
+
const patches = patch.sheetData?.[patchKey];
|
|
41
|
+
if (isV3) {
|
|
42
|
+
addSheetDataPatchesV3(ySheetDataV3, patches);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
addSheetDataPatches(ySheetDataV2, patches);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (patch.sheets) {
|
|
49
|
+
const patches = patch.sheets[patchKey];
|
|
50
|
+
updateArrayType(ySheets, patches);
|
|
51
|
+
}
|
|
52
|
+
if (patch.tables) {
|
|
53
|
+
const patches = patch.tables[patchKey];
|
|
54
|
+
updateArrayType(yTables, patches);
|
|
55
|
+
}
|
|
56
|
+
if (patch.embeds) {
|
|
57
|
+
const patches = patch.embeds[patchKey];
|
|
58
|
+
updateArrayType(yEmbeds, patches);
|
|
59
|
+
}
|
|
60
|
+
if (patch.charts) {
|
|
61
|
+
const patches = patch.charts[patchKey];
|
|
62
|
+
updateArrayType(yCharts, patches);
|
|
63
|
+
}
|
|
64
|
+
if (patch.conditionalFormats) {
|
|
65
|
+
const patches = patch.conditionalFormats[patchKey];
|
|
66
|
+
updateArrayType(yConditionalFormats, patches);
|
|
67
|
+
}
|
|
68
|
+
if (patch.dataValidations) {
|
|
69
|
+
const patches = patch.dataValidations[patchKey];
|
|
70
|
+
updateArrayType(yDataValidations, patches);
|
|
71
|
+
}
|
|
72
|
+
if (patch.namedRanges) {
|
|
73
|
+
const patches = patch.namedRanges[patchKey];
|
|
74
|
+
updateArrayType(yNamedRanges, patches);
|
|
75
|
+
}
|
|
76
|
+
if (patch.sharedStrings) {
|
|
77
|
+
const patches = patch.sharedStrings[patchKey];
|
|
78
|
+
// Always use Map for shared strings
|
|
79
|
+
applyMapPatches(ySharedStringsMap, patches);
|
|
80
|
+
}
|
|
81
|
+
if (patch.protectedRanges) {
|
|
82
|
+
const patches = patch.protectedRanges[patchKey];
|
|
83
|
+
updateArrayType(yProtectedRanges, patches);
|
|
84
|
+
}
|
|
85
|
+
if (patch.cellXfs) {
|
|
86
|
+
const patches = patch.cellXfs[patchKey];
|
|
87
|
+
applyMapPatches(yCellXfs, patches);
|
|
88
|
+
}
|
|
89
|
+
// Recalc cells
|
|
90
|
+
if (patch.recalcCells?.[type]) {
|
|
91
|
+
// Removing this
|
|
92
|
+
// multiple calls to update_sheet_range deletes recalc cells
|
|
93
|
+
// yRecalcCells.delete(0, yRecalcCells.length);
|
|
94
|
+
yRecalcCells.push([
|
|
95
|
+
{
|
|
96
|
+
userId,
|
|
97
|
+
patches: Array.from(patch.recalcCells[type]).map((value) => {
|
|
98
|
+
return [...value, source ?? "agent"];
|
|
99
|
+
}),
|
|
100
|
+
},
|
|
101
|
+
]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Set disable recalc flag (only if it's currently truthy, to avoid redundant Y.Map writes)
|
|
105
|
+
if (yDisableRecalc.get("disableRecalc") !== false) {
|
|
106
|
+
yDisableRecalc.set("disableRecalc", false);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Add sheet data to a yjs map using immer patches
|
|
111
|
+
*
|
|
112
|
+
* Optimized for bulk operations (paste, import) by consolidating cell-level
|
|
113
|
+
* patches before applying to Y.js. This reduces overhead by:
|
|
114
|
+
* 1. Deduplicating patches targeting the same cell (later patches win)
|
|
115
|
+
* 2. Grouping updates by sheet/row for efficient batch application
|
|
116
|
+
*/
|
|
117
|
+
export const addSheetDataPatches = (ySheetData, patches) => {
|
|
118
|
+
// Optimization: Pre-process and consolidate cell-level patches
|
|
119
|
+
// For bulk operations like paste (10k+ rows), this dramatically reduces overhead
|
|
120
|
+
// Key format: "sheetId:rowIndex:columnIndex"
|
|
121
|
+
const cellUpdates = new Map();
|
|
122
|
+
const otherPatches = [];
|
|
123
|
+
// First pass: categorize and consolidate patches
|
|
124
|
+
for (const patch of patches) {
|
|
125
|
+
const { op, path, value } = patch;
|
|
126
|
+
const [sheetIdRaw] = path;
|
|
127
|
+
// Handle undefined sheetId separately (full replace)
|
|
128
|
+
if (typeof sheetIdRaw === "undefined") {
|
|
129
|
+
otherPatches.push(patch);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
// Use string representation for sheet ID to handle both string and numeric IDs
|
|
133
|
+
const sheetIdStr = String(sheetIdRaw);
|
|
134
|
+
if (op === "replace" || op === "add") {
|
|
135
|
+
// [sheetId, rowIndex, 'values', columnIndex] - Single cell update (most common in paste)
|
|
136
|
+
if (path.length === 4) {
|
|
137
|
+
const [, rowIndex, , columnIndex] = path;
|
|
138
|
+
const rowIdx = Number(rowIndex);
|
|
139
|
+
const colIdx = Number(columnIndex);
|
|
140
|
+
const key = `${sheetIdStr}:${rowIdx}:${colIdx}`;
|
|
141
|
+
// Later patches override earlier ones for the same cell
|
|
142
|
+
cellUpdates.set(key, { sheetIdStr, rowIdx, colIdx, value });
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// Row-level or sheet-level updates
|
|
146
|
+
otherPatches.push(patch);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (op === "remove") {
|
|
150
|
+
if (path.length === 4) {
|
|
151
|
+
const [, rowIndex, , columnIndex] = path;
|
|
152
|
+
const rowIdx = Number(rowIndex);
|
|
153
|
+
const colIdx = Number(columnIndex);
|
|
154
|
+
const key = `${sheetIdStr}:${rowIdx}:${colIdx}`;
|
|
155
|
+
cellUpdates.set(key, { sheetIdStr, rowIdx, colIdx, value: null });
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
otherPatches.push(patch);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
otherPatches.push(patch);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Second pass: Group cell updates by sheet and row for efficient batch application
|
|
166
|
+
// This minimizes Y.js operations by batching updates to the same row
|
|
167
|
+
const sheetRowUpdates = new Map();
|
|
168
|
+
for (const [, { sheetIdStr, rowIdx, colIdx, value }] of cellUpdates) {
|
|
169
|
+
if (!sheetRowUpdates.has(sheetIdStr)) {
|
|
170
|
+
sheetRowUpdates.set(sheetIdStr, new Map());
|
|
171
|
+
}
|
|
172
|
+
const rowUpdates = sheetRowUpdates.get(sheetIdStr);
|
|
173
|
+
if (!rowUpdates.has(rowIdx)) {
|
|
174
|
+
rowUpdates.set(rowIdx, new Map());
|
|
175
|
+
}
|
|
176
|
+
rowUpdates.get(rowIdx).set(colIdx, value);
|
|
177
|
+
}
|
|
178
|
+
// Apply batched cell updates
|
|
179
|
+
for (const [sheetIdStr, rowUpdates] of sheetRowUpdates) {
|
|
180
|
+
const ysheet = getSheetFromYSheetData(ySheetData, sheetIdStr);
|
|
181
|
+
for (const [rowIdx, colUpdates] of rowUpdates) {
|
|
182
|
+
const rowData = getRowDataFromSheet(ysheet, rowIdx);
|
|
183
|
+
for (const [colIdx, value] of colUpdates) {
|
|
184
|
+
setCellDataInRowData(rowData, colIdx, value);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Process non-cell-level patches using original logic
|
|
189
|
+
const processPatch = ({ op, path, value }) => {
|
|
190
|
+
let [sheetId] = path;
|
|
191
|
+
if (typeof sheetId === "undefined") {
|
|
192
|
+
// Replace. User is replacing the entire data
|
|
193
|
+
if (op === "replace") {
|
|
194
|
+
// Remove all sheet data
|
|
195
|
+
ySheetData.clear();
|
|
196
|
+
}
|
|
197
|
+
// Assign values
|
|
198
|
+
for (const curSheetId in value) {
|
|
199
|
+
let sheetId = Number(curSheetId);
|
|
200
|
+
processPatch({
|
|
201
|
+
op: "replace",
|
|
202
|
+
path: [sheetId],
|
|
203
|
+
value: value[curSheetId],
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (op === "replace" || op === "add") {
|
|
209
|
+
const ysheet = getSheetFromYSheetData(ySheetData, sheetId);
|
|
210
|
+
// Cell-level patches (path.length === 4) are already handled above via consolidation
|
|
211
|
+
// [sheetId, rowIndex, 'values'] - Replace values array directly
|
|
212
|
+
if (path.length === 3 && path[2] === "values") {
|
|
213
|
+
const [, rowIndex] = path;
|
|
214
|
+
const rowData = getRowDataFromSheet(ysheet, rowIndex);
|
|
215
|
+
// Clear existing row data
|
|
216
|
+
rowData.delete(0, rowData.length);
|
|
217
|
+
if (Array.isArray(value)) {
|
|
218
|
+
// Value is the values array directly
|
|
219
|
+
// Sort keys to maintain order during insertion
|
|
220
|
+
Object.entries(value)
|
|
221
|
+
.sort(([a], [b]) => Number(a) - Number(b))
|
|
222
|
+
.forEach(([columnIndex, cellData]) => {
|
|
223
|
+
setCellDataInRowData(rowData, Number(columnIndex), cellData);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// [sheetId, rowIndex] - Entire row update
|
|
228
|
+
else if (path.length === 2) {
|
|
229
|
+
const [, rowIndex] = path;
|
|
230
|
+
if (value?.values) {
|
|
231
|
+
// Replace entire row with new values
|
|
232
|
+
const rowData = getRowDataFromSheet(ysheet, rowIndex);
|
|
233
|
+
// Clear existing row data
|
|
234
|
+
rowData.delete(0, rowData.length);
|
|
235
|
+
// Add new cell values
|
|
236
|
+
// Sort keys to maintain order during insertion
|
|
237
|
+
Object.entries(value.values)
|
|
238
|
+
.sort(([a], [b]) => Number(a) - Number(b))
|
|
239
|
+
.forEach(([columnIndex, cellData]) => {
|
|
240
|
+
setCellDataInRowData(rowData, Number(columnIndex), cellData);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
// Remove row if value is null/undefined
|
|
245
|
+
removeRowData(ysheet, rowIndex);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// [sheetId] - Entire sheet update
|
|
249
|
+
else if (path.length === 1) {
|
|
250
|
+
if (value) {
|
|
251
|
+
// Clear entire sheet
|
|
252
|
+
ysheet.delete(0, ysheet.length);
|
|
253
|
+
// Add all new data
|
|
254
|
+
Object.entries(value)
|
|
255
|
+
.sort(([a], [b]) => Number(a) - Number(b))
|
|
256
|
+
.forEach(([rowIndex, rowData]) => {
|
|
257
|
+
if (rowData?.values) {
|
|
258
|
+
const rowDataArray = getRowDataFromSheet(ysheet, Number(rowIndex));
|
|
259
|
+
Object.entries(rowData.values)
|
|
260
|
+
.sort(([a], [b]) => Number(a) - Number(b))
|
|
261
|
+
.forEach(([columnIndex, cellData]) => {
|
|
262
|
+
setCellDataInRowData(rowDataArray, Number(columnIndex), cellData);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
else if (op === "remove") {
|
|
270
|
+
// Cell-level removes (path.length === 4) are already handled above via consolidation
|
|
271
|
+
// [sheetId, rowIndex] - Remove entire row
|
|
272
|
+
if (path.length === 2) {
|
|
273
|
+
const [, rowIndex] = path;
|
|
274
|
+
const ysheet = getSheetFromYSheetData(ySheetData, sheetId);
|
|
275
|
+
removeRowData(ysheet, rowIndex);
|
|
276
|
+
}
|
|
277
|
+
// [sheetId] - Remove entire sheet
|
|
278
|
+
else if (path.length === 1) {
|
|
279
|
+
deleteSheetFromYSheetData(ySheetData, sheetId);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
// Only process non-cell-level patches (cell-level already handled via consolidation)
|
|
284
|
+
otherPatches.forEach(processPatch);
|
|
285
|
+
};
|
|
286
|
+
export const getSheetFromYSheetData = (ySheetData, sheetId) => {
|
|
287
|
+
const id = String(sheetId);
|
|
288
|
+
let sheet = ySheetData.get(id);
|
|
289
|
+
if (!sheet) {
|
|
290
|
+
sheet = new Y.Array();
|
|
291
|
+
ySheetData.set(id, sheet);
|
|
292
|
+
}
|
|
293
|
+
return sheet;
|
|
294
|
+
};
|
|
295
|
+
export const deleteSheetFromYSheetData = (ySheetData, sheetId) => {
|
|
296
|
+
const id = String(sheetId);
|
|
297
|
+
ySheetData.delete(id);
|
|
298
|
+
};
|
|
299
|
+
export const getRowDataFromSheet = (ysheet, rowIndex) => {
|
|
300
|
+
const index = Number(rowIndex);
|
|
301
|
+
if (index < 0) {
|
|
302
|
+
throw new Error("Row index cannot be negative");
|
|
303
|
+
}
|
|
304
|
+
// Extend array if needed
|
|
305
|
+
if (index >= ysheet.length) {
|
|
306
|
+
const padding = new Array(index - ysheet.length).fill(null);
|
|
307
|
+
ysheet.insert(ysheet.length, padding);
|
|
308
|
+
ysheet.insert(index, [null]); // Insert null first
|
|
309
|
+
// Create new row data
|
|
310
|
+
const newRowData = new Y.Map();
|
|
311
|
+
newRowData.set("values", new Y.Array());
|
|
312
|
+
// Replace the null
|
|
313
|
+
ysheet.insert(index, [newRowData]);
|
|
314
|
+
ysheet.delete(index + 1, 1);
|
|
315
|
+
// Delete the initially inserted null (wait, index shifts)
|
|
316
|
+
// index is target.
|
|
317
|
+
// Insert padding up to index. length = index.
|
|
318
|
+
// Insert null at index. length = index+1.
|
|
319
|
+
// Replace null at index.
|
|
320
|
+
// Insert newRowData at index. length = index+2.
|
|
321
|
+
// Delete at index+1. length = index+1.
|
|
322
|
+
// Correct.
|
|
323
|
+
return newRowData.get("values");
|
|
324
|
+
}
|
|
325
|
+
// Get or create existing row data
|
|
326
|
+
let rowData = ysheet.get(index);
|
|
327
|
+
if (!rowData || !rowData.get("values")) {
|
|
328
|
+
// Create new row data if it doesn't exist or is incomplete
|
|
329
|
+
const newRowData = new Y.Map();
|
|
330
|
+
newRowData.set("values", new Y.Array());
|
|
331
|
+
// Replace the existing item
|
|
332
|
+
ysheet.insert(index, [newRowData]);
|
|
333
|
+
ysheet.delete(index + 1, 1);
|
|
334
|
+
return newRowData.get("values");
|
|
335
|
+
}
|
|
336
|
+
return rowData.get("values");
|
|
337
|
+
};
|
|
338
|
+
export const setCellDataInRowData = (rowData, columnIndex, cellData = null) => {
|
|
339
|
+
const index = Number(columnIndex);
|
|
340
|
+
// Extend array if needed
|
|
341
|
+
if (index >= rowData.length) {
|
|
342
|
+
const padding = new Array(index - rowData.length).fill(null);
|
|
343
|
+
if (padding.length > 0) {
|
|
344
|
+
rowData.insert(rowData.length, padding);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (index === rowData.length) {
|
|
348
|
+
rowData.insert(index, [cellData]);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
// Replace cell data
|
|
352
|
+
rowData.insert(index, [cellData]);
|
|
353
|
+
rowData.delete(index + 1, 1);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
export const removeRowData = (ysheet, rowIndex) => {
|
|
357
|
+
const index = Number(rowIndex);
|
|
358
|
+
// Only remove if index is valid
|
|
359
|
+
if (index >= 0 && index < ysheet.length) {
|
|
360
|
+
ysheet.insert(index, [null]);
|
|
361
|
+
ysheet.delete(index + 1, 1);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
/**
|
|
365
|
+
* Read a cell's data from the V2 Y.js structure without creating any
|
|
366
|
+
* missing sheet/row/values containers (unlike getSheetFromYSheetData /
|
|
367
|
+
* getRowDataFromSheet, which lazily create them).
|
|
368
|
+
*/
|
|
369
|
+
export const getCellDataFromYSheetData = (ySheetData, sheetId, rowIndex, columnIndex) => {
|
|
370
|
+
const sheet = ySheetData.get(String(sheetId));
|
|
371
|
+
const rowIdx = Number(rowIndex);
|
|
372
|
+
if (!sheet || rowIdx < 0 || rowIdx >= sheet.length) {
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
const row = sheet.get(rowIdx);
|
|
376
|
+
const values = row?.get("values");
|
|
377
|
+
const colIdx = Number(columnIndex);
|
|
378
|
+
if (!values || colIdx < 0 || colIdx >= values.length) {
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
381
|
+
return values.get(colIdx);
|
|
382
|
+
};
|
|
383
|
+
/**
|
|
384
|
+
* Extract the error effective value from cell data, if any.
|
|
385
|
+
* SheetCell.setErrorValue persists formula errors as
|
|
386
|
+
* `ev.ev = { type: "Error", name, message }` (legacy long form:
|
|
387
|
+
* `effectiveValue.errorValue`).
|
|
388
|
+
*/
|
|
389
|
+
export const getCellDataErrorValue = (cellData) => {
|
|
390
|
+
return getExtendedValueError(getCellEffectiveValue(cellData));
|
|
391
|
+
};
|
|
392
|
+
/**
|
|
393
|
+
* Keep only the cell-level sheetData patches
|
|
394
|
+
* (`[sheetId, rowIndex, "values", columnIndex]`) whose new cell data carries
|
|
395
|
+
* an error effective value (#VALUE!, #NAME?, …). Used to let calc-result
|
|
396
|
+
* errors bypass the leader gate: errors must reach the Y.Doc even from
|
|
397
|
+
* non-leader clients, otherwise they render blank after reload.
|
|
398
|
+
*/
|
|
399
|
+
export const filterErrorCellPatches = (patches) => {
|
|
400
|
+
return patches.filter((patch) => (patch.op === "add" || patch.op === "replace") &&
|
|
401
|
+
patch.path.length === 4 &&
|
|
402
|
+
patch.path[2] === "values" &&
|
|
403
|
+
Boolean(getCellDataErrorValue(patch.value)));
|
|
404
|
+
};
|
|
405
|
+
/**
|
|
406
|
+
* Normalize an extended value (short or legacy long keys) into the short-key
|
|
407
|
+
* shape so semantically identical values compare equal regardless of which
|
|
408
|
+
* key shape produced them.
|
|
409
|
+
*/
|
|
410
|
+
const normalizeExtendedValue = (extValue) => {
|
|
411
|
+
if (extValue === null || extValue === undefined) {
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
const normalized = {};
|
|
415
|
+
const numberValue = getExtendedValueNumber(extValue);
|
|
416
|
+
if (numberValue !== undefined) {
|
|
417
|
+
normalized.nv = numberValue;
|
|
418
|
+
}
|
|
419
|
+
const stringValue = getExtendedValueString(extValue);
|
|
420
|
+
if (stringValue !== undefined) {
|
|
421
|
+
normalized.sv = stringValue;
|
|
422
|
+
}
|
|
423
|
+
const boolValue = getExtendedValueBool(extValue);
|
|
424
|
+
if (boolValue !== undefined) {
|
|
425
|
+
normalized.bv = boolValue;
|
|
426
|
+
}
|
|
427
|
+
const formulaValue = getExtendedValueFormula(extValue);
|
|
428
|
+
if (formulaValue !== undefined) {
|
|
429
|
+
normalized.fv = formulaValue;
|
|
430
|
+
}
|
|
431
|
+
const errorValue = getExtendedValueError(extValue);
|
|
432
|
+
if (errorValue !== undefined) {
|
|
433
|
+
normalized.ev = errorValue;
|
|
434
|
+
}
|
|
435
|
+
return normalized;
|
|
436
|
+
};
|
|
437
|
+
/**
|
|
438
|
+
* Containment guard for the non-leader error-persistence path
|
|
439
|
+
* (use-yspreadsheet-v2 onBroadcastPatch, gated disableRecalc branch).
|
|
440
|
+
* A gated error cell patch may be written ONLY when all of these hold:
|
|
441
|
+
*
|
|
442
|
+
* 1. The incoming cell data carries an error effective value.
|
|
443
|
+
* 2. The current doc cell holds no effective value at all (the
|
|
444
|
+
* blank-after-reload case) or its effective value is itself an error.
|
|
445
|
+
* A non-error result (number/string/bool/formula result) is never
|
|
446
|
+
* overwritten — a stale non-leader error must not clobber a newer
|
|
447
|
+
* leader-written success.
|
|
448
|
+
* 3. If the doc cell exists, its user-entered value (`ue`, short or legacy
|
|
449
|
+
* key shape) matches the incoming patch's — a cell edited after the
|
|
450
|
+
* calculation started is never touched.
|
|
451
|
+
* 4. The persisted error differs from the incoming one (identical errors
|
|
452
|
+
* are dropped so concurrent non-leaders don't append redundant
|
|
453
|
+
* Y.js updates).
|
|
454
|
+
*/
|
|
455
|
+
export const shouldPersistErrorCellPatch = (currentCellData, incomingCellData) => {
|
|
456
|
+
const incomingError = getCellDataErrorValue(incomingCellData);
|
|
457
|
+
if (!incomingError) {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
const currentEffectiveValue = getCellEffectiveValue(currentCellData);
|
|
461
|
+
const currentError = getExtendedValueError(currentEffectiveValue);
|
|
462
|
+
if (currentEffectiveValue !== null &&
|
|
463
|
+
currentEffectiveValue !== undefined &&
|
|
464
|
+
!currentError) {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
if (currentCellData !== null && currentCellData !== undefined) {
|
|
468
|
+
const currentUserEnteredValue = normalizeExtendedValue(getCellUserEnteredValue(currentCellData));
|
|
469
|
+
const incomingUserEnteredValue = normalizeExtendedValue(getCellUserEnteredValue(incomingCellData));
|
|
470
|
+
if (!isEqual(currentUserEnteredValue, incomingUserEnteredValue)) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return !isEqual(currentError, incomingError);
|
|
475
|
+
};
|
|
476
|
+
/**
|
|
477
|
+
* Build the cell data to write for a gated error patch that passed
|
|
478
|
+
* shouldPersistErrorCellPatch. If the doc cell doesn't exist yet there is
|
|
479
|
+
* nothing to destroy and the incoming cell data is written as-is. If it
|
|
480
|
+
* does exist, ONLY the error effective value is merged in — every other
|
|
481
|
+
* field of the doc cell (ue, formats, notes, …) is preserved so this write
|
|
482
|
+
* path can never remove doc state it didn't produce. The effective value is
|
|
483
|
+
* written into whichever key shape(s) the doc cell already uses (short `ev`
|
|
484
|
+
* by default).
|
|
485
|
+
*/
|
|
486
|
+
export const mergeErrorEffectiveValueIntoCellData = (currentCellData, incomingCellData) => {
|
|
487
|
+
if (currentCellData === null || currentCellData === undefined) {
|
|
488
|
+
return incomingCellData;
|
|
489
|
+
}
|
|
490
|
+
const incomingEffectiveValue = getCellEffectiveValue(incomingCellData);
|
|
491
|
+
const merged = { ...currentCellData };
|
|
492
|
+
const hasLegacyKey = "effectiveValue" in merged;
|
|
493
|
+
if (hasLegacyKey) {
|
|
494
|
+
merged.effectiveValue = incomingEffectiveValue;
|
|
495
|
+
}
|
|
496
|
+
if ("ev" in merged || !hasLegacyKey) {
|
|
497
|
+
merged.ev = incomingEffectiveValue;
|
|
498
|
+
}
|
|
499
|
+
return merged;
|
|
500
|
+
};
|
|
501
|
+
/**
|
|
502
|
+
* Update array type with immer patches
|
|
503
|
+
*/
|
|
504
|
+
export const updateArrayType = (yarray, patches) => {
|
|
505
|
+
// Consecutive nested-path patches targeting the same array item are
|
|
506
|
+
// consolidated into one cloneDeep + one Y.Array write. An immer splice on
|
|
507
|
+
// a nested array (e.g. a sheet's rowMetadata on row insert/delete) emits
|
|
508
|
+
// one patch per shifted entry; writing each patch individually clones and
|
|
509
|
+
// re-serializes the entire item per entry — O(n²) on large sheets, which
|
|
510
|
+
// freezes the tab.
|
|
511
|
+
let pending = null;
|
|
512
|
+
const flushPending = () => {
|
|
513
|
+
if (!pending) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const { index, item } = pending;
|
|
517
|
+
pending = null;
|
|
518
|
+
ensureArraySize(yarray, index);
|
|
519
|
+
yarray.insert(index, [item]);
|
|
520
|
+
yarray.delete(index + 1, 1);
|
|
521
|
+
};
|
|
522
|
+
patches.forEach(({ op, path, value }) => {
|
|
523
|
+
const [arrayIndex, ...nestedPath] = path;
|
|
524
|
+
const index = Number(arrayIndex);
|
|
525
|
+
if (isNaN(index)) {
|
|
526
|
+
flushPending();
|
|
527
|
+
// User is replacing the entire array
|
|
528
|
+
if (op === "replace") {
|
|
529
|
+
yarray.delete(0, yarray.length);
|
|
530
|
+
yarray.push(value);
|
|
531
|
+
}
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (op === "replace" || op === "add") {
|
|
535
|
+
if (nestedPath.length === 0) {
|
|
536
|
+
flushPending();
|
|
537
|
+
// Direct array item replacement
|
|
538
|
+
ensureArraySize(yarray, index);
|
|
539
|
+
yarray.insert(index, [value]);
|
|
540
|
+
yarray.delete(index + 1, 1);
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
// Nested property update within array item
|
|
544
|
+
let updatedItem;
|
|
545
|
+
if (pending && pending.index === index) {
|
|
546
|
+
updatedItem = pending.item;
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
flushPending();
|
|
550
|
+
const item = index < yarray.length ? yarray.get(index) : {};
|
|
551
|
+
const baseItem = item === null || item === undefined ? {} : item;
|
|
552
|
+
// Clone the item and use lodash to set the nested property
|
|
553
|
+
updatedItem = cloneDeep(baseItem);
|
|
554
|
+
pending = { index, item: updatedItem };
|
|
555
|
+
}
|
|
556
|
+
set(updatedItem, nestedPath, value);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
else if (op === "remove") {
|
|
560
|
+
if (nestedPath.length === 0) {
|
|
561
|
+
flushPending();
|
|
562
|
+
// Remove entire array item
|
|
563
|
+
if (index >= 0 && index < yarray.length) {
|
|
564
|
+
yarray.delete(index, 1);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
// Remove nested property from array item
|
|
569
|
+
let updatedItem = null;
|
|
570
|
+
if (pending && pending.index === index) {
|
|
571
|
+
if (pending.item && typeof pending.item === "object") {
|
|
572
|
+
updatedItem = pending.item;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
flushPending();
|
|
577
|
+
if (index >= 0 && index < yarray.length) {
|
|
578
|
+
const item = yarray.get(index);
|
|
579
|
+
if (item && typeof item === "object") {
|
|
580
|
+
updatedItem = cloneDeep(item);
|
|
581
|
+
pending = { index, item: updatedItem };
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (updatedItem) {
|
|
586
|
+
// Check if we're removing from an array
|
|
587
|
+
const parentPath = nestedPath.slice(0, -1);
|
|
588
|
+
const lastKey = nestedPath[nestedPath.length - 1];
|
|
589
|
+
if (parentPath.length > 0) {
|
|
590
|
+
const parentValue = get(updatedItem, parentPath);
|
|
591
|
+
if (Array.isArray(parentValue) && typeof lastKey === "number") {
|
|
592
|
+
// Remove array element using splice
|
|
593
|
+
parentValue.splice(lastKey, 1);
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
// Remove object property
|
|
597
|
+
unset(updatedItem, nestedPath);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
// Direct property removal
|
|
602
|
+
unset(updatedItem, nestedPath);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
flushPending();
|
|
609
|
+
};
|
|
610
|
+
/**
|
|
611
|
+
* Helper function to ensure array is large enough
|
|
612
|
+
*/
|
|
613
|
+
function ensureArraySize(yarray, index) {
|
|
614
|
+
if (index >= yarray.length) {
|
|
615
|
+
const gaps = new Array(index - yarray.length).fill(null);
|
|
616
|
+
yarray.insert(yarray.length, gaps);
|
|
617
|
+
yarray.insert(index, [null]); // Insert placeholder that will be replaced
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Normalize values to always be an array, converting object notation if needed
|
|
622
|
+
*/
|
|
623
|
+
function normalizeValuesToArray(values) {
|
|
624
|
+
if (!values) {
|
|
625
|
+
return [];
|
|
626
|
+
}
|
|
627
|
+
if (Array.isArray(values)) {
|
|
628
|
+
return [...values];
|
|
629
|
+
}
|
|
630
|
+
// Convert object notation to array
|
|
631
|
+
const entries = Object.entries(values);
|
|
632
|
+
if (entries.length === 0) {
|
|
633
|
+
return [];
|
|
634
|
+
}
|
|
635
|
+
const maxIndex = Math.max(...entries.map(([key]) => Number(key)));
|
|
636
|
+
const result = new Array(maxIndex + 1).fill(null);
|
|
637
|
+
entries.forEach(([key, value]) => {
|
|
638
|
+
result[Number(key)] = value;
|
|
639
|
+
});
|
|
640
|
+
return result;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Validate and clean sheets data
|
|
644
|
+
*/
|
|
645
|
+
export const validateSheets = (sheets) => {
|
|
646
|
+
return sheets.filter(Boolean).map((sheet) => ({
|
|
647
|
+
...sheet,
|
|
648
|
+
// Ensure merges is always an array and filter out falsy values
|
|
649
|
+
merges: sheet.merges?.filter(Boolean),
|
|
650
|
+
}));
|
|
651
|
+
};
|
|
652
|
+
/**
|
|
653
|
+
* Map operations
|
|
654
|
+
*/
|
|
655
|
+
export const applyMapPatches = (yMap, patches) => {
|
|
656
|
+
for (const { op, path, value } of patches) {
|
|
657
|
+
if (!path || !path.length) {
|
|
658
|
+
if (op === "remove") {
|
|
659
|
+
// Only clear if there's something to clear
|
|
660
|
+
if (yMap.size > 0)
|
|
661
|
+
yMap.clear();
|
|
662
|
+
}
|
|
663
|
+
else if (op === "replace") {
|
|
664
|
+
// Diff against current contents instead of clear-and-rewrite, so keys
|
|
665
|
+
// whose references haven't changed don't generate Y.Map tombstones.
|
|
666
|
+
const incoming = new Map(value);
|
|
667
|
+
for (const k of Array.from(yMap.keys())) {
|
|
668
|
+
if (!incoming.has(k))
|
|
669
|
+
yMap.delete(k);
|
|
670
|
+
}
|
|
671
|
+
for (const [k, v] of incoming) {
|
|
672
|
+
if (yMap.get(k) !== v)
|
|
673
|
+
yMap.set(k, v);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
const index = path[0];
|
|
679
|
+
if (op === "remove") {
|
|
680
|
+
if (yMap.has(index))
|
|
681
|
+
yMap.delete(index);
|
|
682
|
+
}
|
|
683
|
+
else if (op === "add" || op === "replace") {
|
|
684
|
+
if (yMap.get(index) !== value)
|
|
685
|
+
yMap.set(index, value);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
/**
|
|
690
|
+
* Process events to find changes in sheet data
|
|
691
|
+
* Returns a new sheet data object with only the changes applied if possible, or null if a full reload is needed
|
|
692
|
+
*/
|
|
693
|
+
export const processSheetDataEvents = (events, currentSheetData, ySheetDataV2) => {
|
|
694
|
+
const changes = new Map();
|
|
695
|
+
const fullReloadSheets = new Set();
|
|
696
|
+
for (const event of events) {
|
|
697
|
+
const path = event.path;
|
|
698
|
+
if (path.length > 0) {
|
|
699
|
+
const sheetId = String(path[0]);
|
|
700
|
+
if (!changes.has(sheetId)) {
|
|
701
|
+
changes.set(sheetId, new Map());
|
|
702
|
+
}
|
|
703
|
+
const rows = changes.get(sheetId);
|
|
704
|
+
// Path structure: [sheetId, rowIndex, "values", colIndex] or [sheetId, rowIndex]
|
|
705
|
+
if (path.length >= 4 && path[2] === "values") {
|
|
706
|
+
const rowIndex = Number(path[1]);
|
|
707
|
+
const columnIndex = Number(path[3]);
|
|
708
|
+
if (!isNaN(rowIndex) && !isNaN(columnIndex)) {
|
|
709
|
+
const existing = rows.get(rowIndex);
|
|
710
|
+
if (existing) {
|
|
711
|
+
if (!existing.full) {
|
|
712
|
+
existing.columns.add(columnIndex);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
rows.set(rowIndex, {
|
|
717
|
+
full: false,
|
|
718
|
+
columns: new Set([columnIndex]),
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
else if (path.length >= 2) {
|
|
724
|
+
const rowIndex = Number(path[1]);
|
|
725
|
+
if (!isNaN(rowIndex)) {
|
|
726
|
+
const existing = rows.get(rowIndex);
|
|
727
|
+
if (existing) {
|
|
728
|
+
existing.full = true;
|
|
729
|
+
existing.columns.clear();
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
rows.set(rowIndex, { full: true, columns: new Set() });
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
else if (path.length === 1) {
|
|
737
|
+
if (event.isYArrayEvent && event.delta) {
|
|
738
|
+
let index = 0;
|
|
739
|
+
event.delta.forEach((op) => {
|
|
740
|
+
if (op.retain) {
|
|
741
|
+
index += op.retain;
|
|
742
|
+
}
|
|
743
|
+
if (op.insert) {
|
|
744
|
+
const count = Array.isArray(op.insert) ? op.insert.length : 1;
|
|
745
|
+
for (let i = 0; i < count; i++) {
|
|
746
|
+
const rowIndex = index + i;
|
|
747
|
+
const existing = rows.get(rowIndex);
|
|
748
|
+
if (existing) {
|
|
749
|
+
existing.full = true;
|
|
750
|
+
existing.columns.clear();
|
|
751
|
+
}
|
|
752
|
+
else {
|
|
753
|
+
rows.set(rowIndex, { full: true, columns: new Set() });
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
index += count;
|
|
757
|
+
}
|
|
758
|
+
if (op.delete) {
|
|
759
|
+
fullReloadSheets.add(sheetId); // Mark full sheet reload
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (changes.size > 0 || fullReloadSheets.size > 0) {
|
|
767
|
+
const next = { ...currentSheetData };
|
|
768
|
+
let hasChanges = false;
|
|
769
|
+
fullReloadSheets.forEach((sheetId) => {
|
|
770
|
+
const ySheet = ySheetDataV2.get(sheetId);
|
|
771
|
+
if (!ySheet)
|
|
772
|
+
return;
|
|
773
|
+
const sheetData = ySheet.toJSON();
|
|
774
|
+
next[sheetId] = sheetData;
|
|
775
|
+
hasChanges = true;
|
|
776
|
+
});
|
|
777
|
+
changes.forEach((rowIndices, sheetId) => {
|
|
778
|
+
const ySheet = ySheetDataV2.get(sheetId);
|
|
779
|
+
if (!ySheet)
|
|
780
|
+
return;
|
|
781
|
+
// If full sheet reload needed
|
|
782
|
+
if (fullReloadSheets.has(sheetId)) {
|
|
783
|
+
const sheetData = ySheet.toJSON();
|
|
784
|
+
next[sheetId] = sheetData;
|
|
785
|
+
hasChanges = true;
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
// Partial update
|
|
789
|
+
if (!next[sheetId]) {
|
|
790
|
+
next[sheetId] = [];
|
|
791
|
+
}
|
|
792
|
+
if (next[sheetId].length < ySheet.length) {
|
|
793
|
+
if (next[sheetId].length !== ySheet.length) {
|
|
794
|
+
const sheetData = ySheet.toJSON();
|
|
795
|
+
next[sheetId] = sheetData;
|
|
796
|
+
hasChanges = true;
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
const nextSheet = [...next[sheetId]];
|
|
801
|
+
rowIndices.forEach((change, rowIndex) => {
|
|
802
|
+
if (rowIndex < 0 || rowIndex >= ySheet.length)
|
|
803
|
+
return;
|
|
804
|
+
const yRow = ySheet.get(rowIndex);
|
|
805
|
+
if (change.full) {
|
|
806
|
+
let normalizedRow = null;
|
|
807
|
+
if (yRow) {
|
|
808
|
+
const rowJson = yRow.toJSON();
|
|
809
|
+
normalizedRow = rowJson;
|
|
810
|
+
}
|
|
811
|
+
nextSheet[rowIndex] = normalizedRow;
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
const currentRow = nextSheet[rowIndex] ?? null;
|
|
815
|
+
if (!yRow) {
|
|
816
|
+
if (currentRow) {
|
|
817
|
+
nextSheet[rowIndex] = currentRow;
|
|
818
|
+
}
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
const yValues = yRow.get("values");
|
|
822
|
+
if (!yValues) {
|
|
823
|
+
if (currentRow) {
|
|
824
|
+
nextSheet[rowIndex] = currentRow;
|
|
825
|
+
}
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
const baseRow = currentRow ?? yRow.toJSON();
|
|
829
|
+
if (!baseRow) {
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
const baseValues = baseRow.values;
|
|
833
|
+
// Always normalize to array to ensure .entries() works in calculation code
|
|
834
|
+
const mergedValues = normalizeValuesToArray(baseValues);
|
|
835
|
+
change.columns.forEach((columnIndex) => {
|
|
836
|
+
const nextValue = yValues.get(columnIndex);
|
|
837
|
+
mergedValues[columnIndex] =
|
|
838
|
+
nextValue === undefined ? null : nextValue;
|
|
839
|
+
});
|
|
840
|
+
nextSheet[rowIndex] = {
|
|
841
|
+
...baseRow,
|
|
842
|
+
values: mergedValues,
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
next[sheetId] = nextSheet;
|
|
847
|
+
hasChanges = true;
|
|
848
|
+
});
|
|
849
|
+
return hasChanges ? next : null;
|
|
850
|
+
}
|
|
851
|
+
return null;
|
|
852
|
+
};
|
|
853
|
+
// ============================================================================
|
|
854
|
+
// V3 Helper Functions (Flat Map Structure)
|
|
855
|
+
// ============================================================================
|
|
856
|
+
/**
|
|
857
|
+
* Convert letter to column index (1-based)
|
|
858
|
+
* A = 1, B = 2, ..., Z = 26, AA = 27, etc.
|
|
859
|
+
*/
|
|
860
|
+
const letterToColumnIndex = (letters) => {
|
|
861
|
+
return letters
|
|
862
|
+
.toUpperCase()
|
|
863
|
+
.split("")
|
|
864
|
+
.reduce((r, a) => r * 26 + parseInt(a, 36) - 9, 0);
|
|
865
|
+
};
|
|
866
|
+
/**
|
|
867
|
+
* Create a V3 cell key from sheet ID, row and column indices
|
|
868
|
+
* Format: `${sheetId}!${A1Address}` (e.g., "1!A1", "2!B5")
|
|
869
|
+
*/
|
|
870
|
+
export const createCellKeyV3 = (sheetId, rowIndex, columnIndex) => {
|
|
871
|
+
const address = cellToAddress({ rowIndex, columnIndex }) ??
|
|
872
|
+
`${String.fromCharCode(65 + columnIndex - 1)}${rowIndex}`;
|
|
873
|
+
return `${sheetId}!${address}`;
|
|
874
|
+
};
|
|
875
|
+
/**
|
|
876
|
+
* Parse a V3 cell key to extract sheet ID and position information
|
|
877
|
+
* Example: parseCellKeyV3("1!A1") => { sheetId: 1, address: "A1", rowIndex: 1, columnIndex: 1 }
|
|
878
|
+
*/
|
|
879
|
+
export const parseCellKeyV3 = (cellKey) => {
|
|
880
|
+
const match = cellKey.match(/^(\d+)!([A-Z]+)(\d+)$/i);
|
|
881
|
+
if (!match)
|
|
882
|
+
return null;
|
|
883
|
+
const [, sheetIdStr, columnLetters, rowIndexStr] = match;
|
|
884
|
+
return {
|
|
885
|
+
sheetId: Number(sheetIdStr),
|
|
886
|
+
address: `${columnLetters.toUpperCase()}${rowIndexStr}`,
|
|
887
|
+
rowIndex: Number(rowIndexStr),
|
|
888
|
+
columnIndex: letterToColumnIndex(columnLetters),
|
|
889
|
+
};
|
|
890
|
+
};
|
|
891
|
+
/**
|
|
892
|
+
* Create a CellDataV3 object with position and sheetId
|
|
893
|
+
*/
|
|
894
|
+
export const createCellDataV3 = (value, sheetId, rowIndex, columnIndex) => ({
|
|
895
|
+
value,
|
|
896
|
+
sId: sheetId,
|
|
897
|
+
r: rowIndex,
|
|
898
|
+
c: columnIndex,
|
|
899
|
+
});
|
|
900
|
+
/**
|
|
901
|
+
* Convert V2 SheetData to V3 flat Map format
|
|
902
|
+
* V2: { sheetId: [rows with values arrays] }
|
|
903
|
+
* V3: Map<cellKey, CellDataV3>
|
|
904
|
+
*/
|
|
905
|
+
export const sheetDataV2ToV3 = (v2Data) => {
|
|
906
|
+
const v3Map = new Map();
|
|
907
|
+
for (const sheetIdStr in v2Data) {
|
|
908
|
+
const sheetId = Number(sheetIdStr);
|
|
909
|
+
const rows = v2Data[sheetId];
|
|
910
|
+
if (!rows)
|
|
911
|
+
continue;
|
|
912
|
+
rows.forEach((row, rowIndex) => {
|
|
913
|
+
if (!row?.values)
|
|
914
|
+
return;
|
|
915
|
+
row.values.forEach((cellData, colIndex) => {
|
|
916
|
+
if (cellData) {
|
|
917
|
+
const key = createCellKeyV3(sheetId, rowIndex, colIndex);
|
|
918
|
+
v3Map.set(key, createCellDataV3(cellData, sheetId, rowIndex, colIndex));
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
return v3Map;
|
|
924
|
+
};
|
|
925
|
+
/**
|
|
926
|
+
* Convert V3 flat Map format to V2 SheetData
|
|
927
|
+
* V3: Map<cellKey, CellDataV3>
|
|
928
|
+
* V2: { sheetId: [rows with values arrays] }
|
|
929
|
+
*/
|
|
930
|
+
export const sheetDataV3ToV2 = (v3Data) => {
|
|
931
|
+
const sheetData = {};
|
|
932
|
+
for (const [_, cellDataV3] of v3Data) {
|
|
933
|
+
if (!cellDataV3)
|
|
934
|
+
continue;
|
|
935
|
+
const { sId: sheetId, r: rowIdx, c: colIdx, value } = cellDataV3;
|
|
936
|
+
// Ensure sheet exists
|
|
937
|
+
if (!sheetData[sheetId]) {
|
|
938
|
+
sheetData[sheetId] = [];
|
|
939
|
+
}
|
|
940
|
+
// Ensure row exists
|
|
941
|
+
while (sheetData[sheetId].length <= rowIdx) {
|
|
942
|
+
sheetData[sheetId].push(null);
|
|
943
|
+
}
|
|
944
|
+
if (!sheetData[sheetId][rowIdx]) {
|
|
945
|
+
sheetData[sheetId][rowIdx] = { values: [] };
|
|
946
|
+
}
|
|
947
|
+
const row = sheetData[sheetId][rowIdx];
|
|
948
|
+
if (!row.values)
|
|
949
|
+
row.values = [];
|
|
950
|
+
// Ensure values array is long enough
|
|
951
|
+
while (row.values.length <= colIdx) {
|
|
952
|
+
row.values.push(null);
|
|
953
|
+
}
|
|
954
|
+
row.values[colIdx] = value;
|
|
955
|
+
}
|
|
956
|
+
return sheetData;
|
|
957
|
+
};
|
|
958
|
+
/**
|
|
959
|
+
* Get a cell from V3 flat structure
|
|
960
|
+
*/
|
|
961
|
+
export const getCellV3 = (ySheetDataV3, sheetId, rowIndex, columnIndex) => {
|
|
962
|
+
const key = createCellKeyV3(sheetId, rowIndex, columnIndex);
|
|
963
|
+
return ySheetDataV3.get(key);
|
|
964
|
+
};
|
|
965
|
+
/**
|
|
966
|
+
* Set a cell in V3 flat structure
|
|
967
|
+
*/
|
|
968
|
+
export const setCellV3 = (ySheetDataV3, sheetId, rowIndex, columnIndex, value) => {
|
|
969
|
+
const key = createCellKeyV3(sheetId, rowIndex, columnIndex);
|
|
970
|
+
ySheetDataV3.set(key, createCellDataV3(value, sheetId, rowIndex, columnIndex));
|
|
971
|
+
};
|
|
972
|
+
/**
|
|
973
|
+
* Delete a cell from V3 flat structure
|
|
974
|
+
*/
|
|
975
|
+
export const deleteCellV3 = (ySheetDataV3, sheetId, rowIndex, columnIndex) => {
|
|
976
|
+
const key = createCellKeyV3(sheetId, rowIndex, columnIndex);
|
|
977
|
+
ySheetDataV3.delete(key);
|
|
978
|
+
};
|
|
979
|
+
/**
|
|
980
|
+
* Delete all cells for a sheet from V3 flat structure
|
|
981
|
+
*/
|
|
982
|
+
export const deleteSheetV3 = (ySheetDataV3, sheetId) => {
|
|
983
|
+
const prefix = `${sheetId}!`;
|
|
984
|
+
for (const key of ySheetDataV3.keys()) {
|
|
985
|
+
if (key.startsWith(prefix)) {
|
|
986
|
+
ySheetDataV3.delete(key);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
/**
|
|
991
|
+
* Get V3 sheet data map from Y.Doc
|
|
992
|
+
*/
|
|
993
|
+
export const getSheetDataV3 = (doc) => {
|
|
994
|
+
return doc.getMap("sheetDataV3");
|
|
995
|
+
};
|
|
996
|
+
/**
|
|
997
|
+
* Apply immer patches to V3 sheet data (flat map structure)
|
|
998
|
+
*
|
|
999
|
+
* Optimized for bulk operations (paste, import) by consolidating cell-level
|
|
1000
|
+
* patches before applying to Y.js. This reduces overhead by:
|
|
1001
|
+
* 1. Deduplicating patches targeting the same cell (later patches win)
|
|
1002
|
+
* 2. Processing all cell updates in a single pass
|
|
1003
|
+
*/
|
|
1004
|
+
export const addSheetDataPatchesV3 = (ySheetDataV3, patches) => {
|
|
1005
|
+
// Optimization: Pre-process and consolidate cell-level patches
|
|
1006
|
+
// For bulk operations like paste (10k+ rows), this dramatically reduces overhead
|
|
1007
|
+
const cellUpdates = new Map();
|
|
1008
|
+
const otherPatches = [];
|
|
1009
|
+
// First pass: categorize and consolidate patches
|
|
1010
|
+
for (const patch of patches) {
|
|
1011
|
+
const { op, path, value } = patch;
|
|
1012
|
+
const [sheetIdRaw] = path;
|
|
1013
|
+
// Handle undefined sheetId separately (full replace)
|
|
1014
|
+
if (typeof sheetIdRaw === "undefined") {
|
|
1015
|
+
otherPatches.push(patch);
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
const sheetId = Number(sheetIdRaw);
|
|
1019
|
+
if (op === "replace" || op === "add") {
|
|
1020
|
+
// [sheetId, rowIndex, 'values', columnIndex] - Single cell update (most common in paste)
|
|
1021
|
+
if (path.length === 4) {
|
|
1022
|
+
const [, rowIndex, , columnIndex] = path;
|
|
1023
|
+
const rowIdx = Number(rowIndex);
|
|
1024
|
+
const colIdx = Number(columnIndex);
|
|
1025
|
+
const key = createCellKeyV3(sheetId, rowIdx, colIdx);
|
|
1026
|
+
// Later patches override earlier ones for the same cell
|
|
1027
|
+
cellUpdates.set(key, { sheetId, rowIdx, colIdx, value });
|
|
1028
|
+
}
|
|
1029
|
+
else {
|
|
1030
|
+
// Row-level or sheet-level updates
|
|
1031
|
+
otherPatches.push(patch);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
else if (op === "remove") {
|
|
1035
|
+
if (path.length === 4) {
|
|
1036
|
+
const [, rowIndex, , columnIndex] = path;
|
|
1037
|
+
const rowIdx = Number(rowIndex);
|
|
1038
|
+
const colIdx = Number(columnIndex);
|
|
1039
|
+
const key = createCellKeyV3(sheetId, rowIdx, colIdx);
|
|
1040
|
+
cellUpdates.set(key, { sheetId, rowIdx, colIdx, value: null });
|
|
1041
|
+
}
|
|
1042
|
+
else {
|
|
1043
|
+
otherPatches.push(patch);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
else {
|
|
1047
|
+
otherPatches.push(patch);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
// Second pass: Apply consolidated cell updates in batch
|
|
1051
|
+
// This is much faster than processing each patch individually
|
|
1052
|
+
for (const [key, { sheetId, rowIdx, colIdx, value }] of cellUpdates) {
|
|
1053
|
+
if (value) {
|
|
1054
|
+
ySheetDataV3.set(key, createCellDataV3(value, sheetId, rowIdx, colIdx));
|
|
1055
|
+
}
|
|
1056
|
+
else {
|
|
1057
|
+
ySheetDataV3.delete(key);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
// Row-targeted patches (e.g. the per-shifted-row patches an immer array
|
|
1061
|
+
// splice emits for a row insert/delete) previously scanned the entire flat
|
|
1062
|
+
// cell map once per patch — O(patches × totalCells), which freezes the tab
|
|
1063
|
+
// when inserting a row near the top of a large sheet. Build a
|
|
1064
|
+
// row → cell-keys index once per batch instead, and keep it in sync as
|
|
1065
|
+
// patches apply.
|
|
1066
|
+
let rowCellKeyIndex = null;
|
|
1067
|
+
const rowIndexKey = (sheetId, rowIdx) => `${sheetId}:${rowIdx}`;
|
|
1068
|
+
const ensureRowCellKeyIndex = () => {
|
|
1069
|
+
if (rowCellKeyIndex) {
|
|
1070
|
+
return rowCellKeyIndex;
|
|
1071
|
+
}
|
|
1072
|
+
rowCellKeyIndex = new Map();
|
|
1073
|
+
for (const [key, cellData] of ySheetDataV3.entries()) {
|
|
1074
|
+
const rk = rowIndexKey(cellData?.sId, cellData?.r);
|
|
1075
|
+
let keys = rowCellKeyIndex.get(rk);
|
|
1076
|
+
if (!keys) {
|
|
1077
|
+
keys = new Set();
|
|
1078
|
+
rowCellKeyIndex.set(rk, keys);
|
|
1079
|
+
}
|
|
1080
|
+
keys.add(key);
|
|
1081
|
+
}
|
|
1082
|
+
return rowCellKeyIndex;
|
|
1083
|
+
};
|
|
1084
|
+
const trackCellKeySet = (sheetId, rowIdx, key) => {
|
|
1085
|
+
if (!rowCellKeyIndex) {
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
const rk = rowIndexKey(sheetId, rowIdx);
|
|
1089
|
+
let keys = rowCellKeyIndex.get(rk);
|
|
1090
|
+
if (!keys) {
|
|
1091
|
+
keys = new Set();
|
|
1092
|
+
rowCellKeyIndex.set(rk, keys);
|
|
1093
|
+
}
|
|
1094
|
+
keys.add(key);
|
|
1095
|
+
};
|
|
1096
|
+
const deleteRowCells = (sheetId, rowIdx) => {
|
|
1097
|
+
const keys = ensureRowCellKeyIndex().get(rowIndexKey(sheetId, rowIdx));
|
|
1098
|
+
if (!keys) {
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
for (const key of keys) {
|
|
1102
|
+
ySheetDataV3.delete(key);
|
|
1103
|
+
}
|
|
1104
|
+
rowCellKeyIndex.delete(rowIndexKey(sheetId, rowIdx));
|
|
1105
|
+
};
|
|
1106
|
+
const dropSheetFromIndex = (sheetId) => {
|
|
1107
|
+
if (!rowCellKeyIndex) {
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
const prefix = `${sheetId}:`;
|
|
1111
|
+
for (const rk of Array.from(rowCellKeyIndex.keys())) {
|
|
1112
|
+
if (rk.startsWith(prefix)) {
|
|
1113
|
+
rowCellKeyIndex.delete(rk);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
// Process non-cell-level patches using original logic
|
|
1118
|
+
const processPatch = ({ op, path, value }) => {
|
|
1119
|
+
const [sheetIdRaw] = path;
|
|
1120
|
+
if (typeof sheetIdRaw === "undefined") {
|
|
1121
|
+
// Replace entire data
|
|
1122
|
+
if (op === "replace") {
|
|
1123
|
+
// Clear all cells
|
|
1124
|
+
for (const key of ySheetDataV3.keys()) {
|
|
1125
|
+
ySheetDataV3.delete(key);
|
|
1126
|
+
}
|
|
1127
|
+
if (rowCellKeyIndex) {
|
|
1128
|
+
rowCellKeyIndex.clear();
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
// Assign values from the replacement object
|
|
1132
|
+
for (const curSheetId in value) {
|
|
1133
|
+
const sheetId = Number(curSheetId);
|
|
1134
|
+
const rows = value[curSheetId];
|
|
1135
|
+
if (!rows)
|
|
1136
|
+
continue;
|
|
1137
|
+
Object.entries(rows).forEach(([rowIndexStr, rowData]) => {
|
|
1138
|
+
if (!rowData?.values)
|
|
1139
|
+
return;
|
|
1140
|
+
const rowIdx = Number(rowIndexStr);
|
|
1141
|
+
Object.entries(rowData.values).forEach(([colIndexStr, cellData]) => {
|
|
1142
|
+
if (cellData) {
|
|
1143
|
+
const colIdx = Number(colIndexStr);
|
|
1144
|
+
const key = createCellKeyV3(sheetId, rowIdx, colIdx);
|
|
1145
|
+
ySheetDataV3.set(key, createCellDataV3(cellData, sheetId, rowIdx, colIdx));
|
|
1146
|
+
trackCellKeySet(sheetId, rowIdx, key);
|
|
1147
|
+
}
|
|
1148
|
+
});
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
const sheetId = Number(sheetIdRaw);
|
|
1154
|
+
if (op === "replace" || op === "add") {
|
|
1155
|
+
// Cell-level patches (path.length === 4) are already handled above via consolidation
|
|
1156
|
+
// [sheetId, rowIndex, 'values', columnIndex, ...nestedPath] - Deep nested property update
|
|
1157
|
+
if (path.length > 4) {
|
|
1158
|
+
const [, rowIndex, , columnIndex, ...nestedPath] = path;
|
|
1159
|
+
const rowIdx = Number(rowIndex);
|
|
1160
|
+
const colIdx = Number(columnIndex);
|
|
1161
|
+
const key = createCellKeyV3(sheetId, rowIdx, colIdx);
|
|
1162
|
+
// Get existing cell data or create empty
|
|
1163
|
+
const existing = ySheetDataV3.get(key);
|
|
1164
|
+
const cellData = existing?.value ? cloneDeep(existing.value) : {};
|
|
1165
|
+
// Apply nested property change
|
|
1166
|
+
set(cellData, nestedPath, value);
|
|
1167
|
+
ySheetDataV3.set(key, createCellDataV3(cellData, sheetId, rowIdx, colIdx));
|
|
1168
|
+
trackCellKeySet(sheetId, rowIdx, key);
|
|
1169
|
+
}
|
|
1170
|
+
// [sheetId, rowIndex, 'values'] - Replace values array
|
|
1171
|
+
else if (path.length === 3 && path[2] === "values") {
|
|
1172
|
+
const [, rowIndex] = path;
|
|
1173
|
+
const rowIdx = Number(rowIndex);
|
|
1174
|
+
// Delete existing cells for this row
|
|
1175
|
+
deleteRowCells(sheetId, rowIdx);
|
|
1176
|
+
// Add new cells
|
|
1177
|
+
if (Array.isArray(value)) {
|
|
1178
|
+
value.forEach((cellData, colIndex) => {
|
|
1179
|
+
if (cellData) {
|
|
1180
|
+
const key = createCellKeyV3(sheetId, rowIdx, colIndex);
|
|
1181
|
+
ySheetDataV3.set(key, createCellDataV3(cellData, sheetId, rowIdx, colIndex));
|
|
1182
|
+
trackCellKeySet(sheetId, rowIdx, key);
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
// [sheetId, rowIndex] - Entire row update
|
|
1188
|
+
else if (path.length === 2) {
|
|
1189
|
+
const [, rowIndex] = path;
|
|
1190
|
+
const rowIdx = Number(rowIndex);
|
|
1191
|
+
// Delete existing cells for this row
|
|
1192
|
+
deleteRowCells(sheetId, rowIdx);
|
|
1193
|
+
// Add new cells from row data
|
|
1194
|
+
if (value?.values) {
|
|
1195
|
+
Object.entries(value.values).forEach(([colIndexStr, cellData]) => {
|
|
1196
|
+
if (cellData) {
|
|
1197
|
+
const colIndex = Number(colIndexStr);
|
|
1198
|
+
const key = createCellKeyV3(sheetId, rowIdx, colIndex);
|
|
1199
|
+
ySheetDataV3.set(key, createCellDataV3(cellData, sheetId, rowIdx, colIndex));
|
|
1200
|
+
trackCellKeySet(sheetId, rowIdx, key);
|
|
1201
|
+
}
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
// [sheetId] - Entire sheet update
|
|
1206
|
+
else if (path.length === 1) {
|
|
1207
|
+
// Delete all cells for this sheet
|
|
1208
|
+
const prefix = `${sheetId}!`;
|
|
1209
|
+
for (const key of ySheetDataV3.keys()) {
|
|
1210
|
+
if (key.startsWith(prefix)) {
|
|
1211
|
+
ySheetDataV3.delete(key);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
dropSheetFromIndex(sheetId);
|
|
1215
|
+
// Add all new data
|
|
1216
|
+
if (value) {
|
|
1217
|
+
Object.entries(value).forEach(([rowIndexStr, rowData]) => {
|
|
1218
|
+
if (rowData?.values) {
|
|
1219
|
+
const rowIdx = Number(rowIndexStr);
|
|
1220
|
+
Object.entries(rowData.values).forEach(([colIndexStr, cellData]) => {
|
|
1221
|
+
if (cellData) {
|
|
1222
|
+
const colIndex = Number(colIndexStr);
|
|
1223
|
+
const key = createCellKeyV3(sheetId, rowIdx, colIndex);
|
|
1224
|
+
ySheetDataV3.set(key, createCellDataV3(cellData, sheetId, rowIdx, colIndex));
|
|
1225
|
+
trackCellKeySet(sheetId, rowIdx, key);
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
else if (op === "remove") {
|
|
1234
|
+
// Cell-level removes (path.length === 4) are already handled above via consolidation
|
|
1235
|
+
// [sheetId, rowIndex, values, columnIndex, ...nestedPath] - Remove nested property
|
|
1236
|
+
if (path.length > 4) {
|
|
1237
|
+
const [, rowIndex, , columnIndex, ...nestedPath] = path;
|
|
1238
|
+
const rowIdx = Number(rowIndex);
|
|
1239
|
+
const colIdx = Number(columnIndex);
|
|
1240
|
+
const key = createCellKeyV3(sheetId, rowIdx, colIdx);
|
|
1241
|
+
const existing = ySheetDataV3.get(key);
|
|
1242
|
+
if (existing?.value && typeof existing.value === "object") {
|
|
1243
|
+
const cellData = cloneDeep(existing.value);
|
|
1244
|
+
unset(cellData, nestedPath);
|
|
1245
|
+
ySheetDataV3.set(key, createCellDataV3(cellData, sheetId, rowIdx, colIdx));
|
|
1246
|
+
trackCellKeySet(sheetId, rowIdx, key);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
// [sheetId, rowIndex] - Remove entire row
|
|
1250
|
+
else if (path.length === 2) {
|
|
1251
|
+
const [, rowIndex] = path;
|
|
1252
|
+
const rowIdx = Number(rowIndex);
|
|
1253
|
+
deleteRowCells(sheetId, rowIdx);
|
|
1254
|
+
}
|
|
1255
|
+
// [sheetId] - Remove entire sheet
|
|
1256
|
+
else if (path.length === 1) {
|
|
1257
|
+
const prefix = `${sheetId}!`;
|
|
1258
|
+
for (const key of ySheetDataV3.keys()) {
|
|
1259
|
+
if (key.startsWith(prefix)) {
|
|
1260
|
+
ySheetDataV3.delete(key);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
dropSheetFromIndex(sheetId);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
// Only process non-cell-level patches (cell-level already handled via consolidation)
|
|
1268
|
+
otherPatches.forEach(processPatch);
|
|
1269
|
+
};
|
|
1270
|
+
/**
|
|
1271
|
+
* Process V3 events and return updated sheet data
|
|
1272
|
+
* @param changedKeys - Pre-extracted changes from Yjs event
|
|
1273
|
+
* @param currentSheetData - Current sheet data state
|
|
1274
|
+
* @param ySheetDataV3 - The Y.Map for V3 data
|
|
1275
|
+
*/
|
|
1276
|
+
export const processSheetDataEventsV3 = (changedKeys, currentSheetData, ySheetDataV3) => {
|
|
1277
|
+
if (changedKeys.size === 0)
|
|
1278
|
+
return null;
|
|
1279
|
+
const next = { ...currentSheetData };
|
|
1280
|
+
// Track which sheets/rows have been cloned to avoid re-cloning
|
|
1281
|
+
const clonedSheets = new Set();
|
|
1282
|
+
const clonedRows = new Map(); // "sheetId:rowIdx" -> cloned
|
|
1283
|
+
let hasChanges = false;
|
|
1284
|
+
changedKeys.forEach((change, cellKey) => {
|
|
1285
|
+
const parsed = parseCellKeyV3(cellKey);
|
|
1286
|
+
if (!parsed)
|
|
1287
|
+
return;
|
|
1288
|
+
const { sheetId, rowIndex, columnIndex } = parsed;
|
|
1289
|
+
const rowIdx = rowIndex;
|
|
1290
|
+
const colIdx = columnIndex;
|
|
1291
|
+
const rowKey = `${sheetId}:${rowIdx}`;
|
|
1292
|
+
// Ensure sheet exists and is cloned (to avoid mutating frozen React state)
|
|
1293
|
+
if (!next[sheetId]) {
|
|
1294
|
+
next[sheetId] = [];
|
|
1295
|
+
clonedSheets.add(sheetId);
|
|
1296
|
+
}
|
|
1297
|
+
else if (!clonedSheets.has(sheetId)) {
|
|
1298
|
+
// Clone the sheet array to avoid mutating frozen state
|
|
1299
|
+
next[sheetId] = [...next[sheetId]];
|
|
1300
|
+
clonedSheets.add(sheetId);
|
|
1301
|
+
}
|
|
1302
|
+
const sheet = next[sheetId];
|
|
1303
|
+
// Extend array efficiently using length assignment instead of while loop
|
|
1304
|
+
if (sheet.length <= rowIdx) {
|
|
1305
|
+
sheet.length = rowIdx + 1;
|
|
1306
|
+
}
|
|
1307
|
+
if (change.action === "delete") {
|
|
1308
|
+
// Cell was deleted
|
|
1309
|
+
const row = sheet[rowIdx];
|
|
1310
|
+
if (row?.values && colIdx < row.values.length && row.values[colIdx]) {
|
|
1311
|
+
// Clone row values only once per row
|
|
1312
|
+
if (!clonedRows.has(rowKey)) {
|
|
1313
|
+
sheet[rowIdx] = { ...row, values: [...row.values] };
|
|
1314
|
+
clonedRows.set(rowKey, true);
|
|
1315
|
+
}
|
|
1316
|
+
sheet[rowIdx].values[colIdx] = null;
|
|
1317
|
+
hasChanges = true;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
else {
|
|
1321
|
+
// Cell was added or updated
|
|
1322
|
+
const cellDataV3 = ySheetDataV3.get(cellKey);
|
|
1323
|
+
if (cellDataV3) {
|
|
1324
|
+
// Ensure row exists
|
|
1325
|
+
if (!sheet[rowIdx]) {
|
|
1326
|
+
sheet[rowIdx] = { values: [] };
|
|
1327
|
+
clonedRows.set(rowKey, true);
|
|
1328
|
+
}
|
|
1329
|
+
else if (!clonedRows.has(rowKey)) {
|
|
1330
|
+
// Clone row values only once per row
|
|
1331
|
+
const row = sheet[rowIdx];
|
|
1332
|
+
sheet[rowIdx] = { ...row, values: row.values ? [...row.values] : [] };
|
|
1333
|
+
clonedRows.set(rowKey, true);
|
|
1334
|
+
}
|
|
1335
|
+
const values = sheet[rowIdx].values;
|
|
1336
|
+
// Extend array efficiently using length assignment
|
|
1337
|
+
if (values.length <= colIdx) {
|
|
1338
|
+
values.length = colIdx + 1;
|
|
1339
|
+
}
|
|
1340
|
+
values[colIdx] = cellDataV3.value;
|
|
1341
|
+
hasChanges = true;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
});
|
|
1345
|
+
return hasChanges ? next : null;
|
|
1346
|
+
};
|