@marimo-team/frontend 0.24.1-dev79 → 0.24.1-dev80

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.
Files changed (87) hide show
  1. package/dist/assets/{CellStatus-Bfn_Ysig.js → CellStatus-DMQfH9wD.js} +1 -1
  2. package/dist/assets/{JsonOutput-DFd7q8lz.js → JsonOutput-B7sDhGzR.js} +1 -1
  3. package/dist/assets/{MarimoErrorOutput-DfRv7_jc.js → MarimoErrorOutput-C0vk393J.js} +1 -1
  4. package/dist/assets/{RenderHTML-Di-OdQjY.js → RenderHTML-BnMaGpV8.js} +1 -1
  5. package/dist/assets/{RunButton-r21rSvYz.js → RunButton-nocIODpS.js} +1 -1
  6. package/dist/assets/{add-cell-with-ai-Cc3EENeX.js → add-cell-with-ai-B_TSaqjJ.js} +1 -1
  7. package/dist/assets/{add-connection-dialog-Bce8iA2c.js → add-connection-dialog-DOuojDa-.js} +1 -1
  8. package/dist/assets/{agent-panel-COvCNnaD.js → agent-panel-CkKCGjDp.js} +1 -1
  9. package/dist/assets/{ai-model-dropdown-D7nJTvh4.js → ai-model-dropdown-CfWMIzjl.js} +1 -1
  10. package/dist/assets/{app-config-button-C0IsSBiP.js → app-config-button-ii-Hj5CZ.js} +1 -1
  11. package/dist/assets/{blob-CbiWlEJN.js → blob-CYM8xQsK.js} +1 -1
  12. package/dist/assets/{cell-editor-DnyPiOtX.js → cell-editor-BVb6iKbw.js} +1 -1
  13. package/dist/assets/{cell-link-Z8JrraTv.js → cell-link-DCOAqVkE.js} +1 -1
  14. package/dist/assets/{cells-CD9Jbpoa.js → cells-C6lBv8kg.js} +51 -51
  15. package/dist/assets/{chat-display-Dxad67GU.js → chat-display-B6GBZDq8.js} +1 -1
  16. package/dist/assets/{chat-panel-b6eYwY8N.js → chat-panel-wWuBXhXw.js} +1 -1
  17. package/dist/assets/{chat-ui-DWgQGHiC.js → chat-ui-CQ7A3qdB.js} +1 -1
  18. package/dist/assets/click-outside-container-C5RF3h4V.js +1 -0
  19. package/dist/assets/{column-preview-_GkpGxG-.js → column-preview-CatwdUda.js} +1 -1
  20. package/dist/assets/{command-palette-BeHx267T.js → command-palette-DcPM_WiO.js} +1 -1
  21. package/dist/assets/{common-Bb2s_Imb.js → common-B4aMxphz.js} +1 -1
  22. package/dist/assets/{components-D5125bx8.js → components-D8-ffQsq.js} +1 -1
  23. package/dist/assets/data-grid-overlay-editor-CFYeXDX3.js +1 -0
  24. package/dist/assets/data-grid-types-DgHVN_Zz.js +1 -0
  25. package/dist/assets/{datasource-DTtEQWz7.js → datasource-D-_jFtGs.js} +1 -1
  26. package/dist/assets/{dependency-graph-panel-DiMc1DOt.js → dependency-graph-panel-BQli6zxh.js} +1 -1
  27. package/dist/assets/{documentation-panel-ChEa8OHk.js → documentation-panel-_QE3nPOg.js} +1 -1
  28. package/dist/assets/{download-BKMVrvLm.js → download-Cqjqrhzo.js} +1 -1
  29. package/dist/assets/{edit-page-C3Uo5Msq.js → edit-page-RuyN164g.js} +4 -4
  30. package/dist/assets/{error-entries-DI864MPn.js → error-entries-c5sIySli.js} +1 -1
  31. package/dist/assets/{error-panel-COOOtEg7.js → error-panel-CXKrWK_u.js} +1 -1
  32. package/dist/assets/{file-explorer-panel-DeF49Ng1.js → file-explorer-panel-BZiAb4x_.js} +1 -1
  33. package/dist/assets/{file-icons-CmmWEUGv.js → file-icons-uUY_hGYm.js} +1 -1
  34. package/dist/assets/{file-name-input-9sqInamo.js → file-name-input-DWUKg4LS.js} +1 -1
  35. package/dist/assets/{floating-outline-BxMKEygY.js → floating-outline-DhGtHg6T.js} +1 -1
  36. package/dist/assets/glide-data-editor-DZ2Minh8.js +131 -0
  37. package/dist/assets/glide-utils-CR1sWLZU.js +2 -0
  38. package/dist/assets/{home-page-G9DBo7dA.js → home-page-Bka_7H02.js} +1 -1
  39. package/dist/assets/index-ChbtcNW2.js +29 -0
  40. package/dist/assets/{layout-RSFs10mJ.js → layout-Uv1ZP5XB.js} +3 -3
  41. package/dist/assets/{logs-panel-DaLUKPBX.js → logs-panel-ragZa1sf.js} +1 -1
  42. package/dist/assets/{markdown-renderer-Cj57nlKH.js → markdown-renderer-B-uA_6mP.js} +1 -1
  43. package/dist/assets/{useNonce-BTM6-ANi.js → marked.esm-B_3iMIUq.js} +10 -10
  44. package/dist/assets/{name-cell-input-D0M2lddE.js → name-cell-input-gC1flbyT.js} +1 -1
  45. package/dist/assets/{outline-panel-UDArUDGw.js → outline-panel-D7H7bhOh.js} +1 -1
  46. package/dist/assets/{packages-panel-B8AaXFcI.js → packages-panel-BZnsUm4S.js} +1 -1
  47. package/dist/assets/{panel-accordion-state-COnGLerF.js → panel-accordion-state-BLU7OiMD.js} +1 -1
  48. package/dist/assets/{panels-DecWMR6G.js → panels-B3Y6fue9.js} +1 -1
  49. package/dist/assets/{process-output-CUf8H__q.js → process-output-DQQ8T5gv.js} +1 -1
  50. package/dist/assets/{readonly-code-display-Bof9MzZO.js → readonly-code-display-Bc0VX9i6.js} +1 -1
  51. package/dist/assets/{readonly-python-code-GMl_IocT.js → readonly-python-code-D2HXwAfT.js} +1 -1
  52. package/dist/assets/{reveal-component-yDciCkP9.js → reveal-component-BN1C8zFu.js} +1 -1
  53. package/dist/assets/{run-page-CCk94ccj.js → run-page-ByVvsH9G.js} +1 -1
  54. package/dist/assets/{scratchpad-panel-EL1m3Huh.js → scratchpad-panel-BJHlMBZV.js} +1 -1
  55. package/dist/assets/{session-panel-DKjUcjvg.js → session-panel-CdCKWAOf.js} +1 -1
  56. package/dist/assets/{skeleton-D71npu63.js → skeleton-DirOIQrj.js} +1 -1
  57. package/dist/assets/{snippets-panel-njDHc9qT.js → snippets-panel-2jq36DdG.js} +1 -1
  58. package/dist/assets/{state-CwrzGizs.js → state-C9O-2hQL.js} +1 -1
  59. package/dist/assets/{state-mneM4Ao8.js → state-DyxPdcEa.js} +1 -1
  60. package/dist/assets/{tracing-CGZCxvWG.js → tracing-91_akEkv.js} +1 -1
  61. package/dist/assets/{tracing-panel-4lmoUwoQ.js → tracing-panel-BvE81RYx.js} +2 -2
  62. package/dist/assets/{tree-actions-Bjyy0vFK.js → tree-actions-CGkbUSfG.js} +1 -1
  63. package/dist/assets/{useAddCell-h4OWLK5y.js → useAddCell-DqvY2ulx.js} +1 -1
  64. package/dist/assets/{useCellActionButton-BvuYvLCr.js → useCellActionButton-cv6xOsnc.js} +1 -1
  65. package/dist/assets/{useDeleteCell-Buwee6vZ.js → useDeleteCell-DB-bhhnO.js} +1 -1
  66. package/dist/assets/{useDependencyPanelTab-CLR1fyXm.js → useDependencyPanelTab-BDnVzNV-.js} +1 -1
  67. package/dist/assets/{useNotebookActions-C0Uw9roU.js → useNotebookActions-Dxp4gTEa.js} +1 -1
  68. package/dist/assets/{useRunCells-C6416Qeo.js → useRunCells-GCTS1YFv.js} +1 -1
  69. package/dist/assets/{useSplitCell-Ip3d7gP0.js → useSplitCell-DhT9BA2_.js} +1 -1
  70. package/dist/index.html +22 -21
  71. package/package.json +1 -1
  72. package/src/plugins/impl/DataEditorPlugin.tsx +38 -70
  73. package/src/plugins/impl/__tests__/DataEditorPlugin.test.ts +152 -1
  74. package/src/plugins/impl/data-editor/__tests__/data-utils.test.ts +73 -78
  75. package/src/plugins/impl/data-editor/__tests__/editor-state.test.ts +113 -0
  76. package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +49 -13
  77. package/src/plugins/impl/data-editor/__tests__/glide-utils.test.ts +44 -64
  78. package/src/plugins/impl/data-editor/data-utils.ts +76 -53
  79. package/src/plugins/impl/data-editor/editor-state.ts +108 -0
  80. package/src/plugins/impl/data-editor/glide-data-editor.tsx +40 -223
  81. package/src/plugins/impl/data-editor/glide-utils.ts +71 -31
  82. package/src/plugins/impl/data-editor/types.ts +30 -16
  83. package/dist/assets/click-outside-container-n76g8dsY.js +0 -1
  84. package/dist/assets/data-grid-overlay-editor-DCmC8-tb.js +0 -1
  85. package/dist/assets/glide-data-editor-CzalkCph.js +0 -132
  86. package/dist/assets/index-BmNpxjzV.js +0 -29
  87. package/dist/assets/types-BflGgckY.js +0 -1
@@ -3,7 +3,7 @@
3
3
  import type { GridSelection } from "@glideapps/glide-data-grid";
4
4
  import { CompactSelection, GridCellKind } from "@glideapps/glide-data-grid";
5
5
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
- import { pasteCells } from "../glide-utils";
6
+ import { isValidCellValue, pasteCells } from "../glide-utils";
7
7
  import type { ModifiedGridColumn } from "../types";
8
8
 
9
9
  // Mock navigator.clipboard
@@ -16,6 +16,26 @@ Object.defineProperty(navigator, "clipboard", {
16
16
  writable: true,
17
17
  });
18
18
 
19
+ describe("isValidCellValue", () => {
20
+ it("accepts only finite numbers", () => {
21
+ expect(isValidCellValue("number", 3.5)).toBe(true);
22
+ expect(isValidCellValue("number", Number.POSITIVE_INFINITY)).toBe(false);
23
+ expect(isValidCellValue("number", Number.NEGATIVE_INFINITY)).toBe(false);
24
+ expect(isValidCellValue("number", "Infinity")).toBe(false);
25
+ expect(isValidCellValue("number", Number.NaN)).toBe(false);
26
+ });
27
+
28
+ it("preserves integer precision", () => {
29
+ expect(isValidCellValue("integer", 3)).toBe(true);
30
+ expect(isValidCellValue("integer", 3.5)).toBe(false);
31
+ expect(isValidCellValue("integer", "9007199254740993")).toBe(true);
32
+ expect(isValidCellValue("integer", "9007199254740993.0")).toBe(true);
33
+ expect(isValidCellValue("integer", "9007199254740993.1")).toBe(false);
34
+ expect(isValidCellValue("integer", "1.0000000000000001")).toBe(false);
35
+ expect(isValidCellValue("integer", "1e-324")).toBe(false);
36
+ });
37
+ });
38
+
19
39
  describe("pasteCells", () => {
20
40
  beforeEach(() => {
21
41
  vi.clearAllMocks();
@@ -76,13 +96,11 @@ describe("pasteCells", () => {
76
96
  it("should handle empty clipboard text", async () => {
77
97
  mockClipboard.readText.mockResolvedValue("");
78
98
 
79
- const mockSetLocalData = vi.fn();
80
99
  const mockOnAddEdits = vi.fn();
81
100
 
82
101
  pasteCells({
83
102
  selection: createMockSelection(0, 0),
84
103
  data: createMockData(),
85
- setData: mockSetLocalData,
86
104
  columns: createMockColumns(),
87
105
  editableColumns: "all",
88
106
  onAddEdits: mockOnAddEdits,
@@ -91,20 +109,17 @@ describe("pasteCells", () => {
91
109
  // Wait for the async operation
92
110
  await vi.waitFor(() => {
93
111
  expect(mockOnAddEdits).not.toHaveBeenCalled();
94
- expect(mockSetLocalData).not.toHaveBeenCalled();
95
112
  });
96
113
  });
97
114
 
98
115
  it("should handle whitespace-only clipboard text", async () => {
99
116
  mockClipboard.readText.mockResolvedValue(" \n\t ");
100
117
 
101
- const mockSetLocalData = vi.fn();
102
118
  const mockOnAddEdits = vi.fn();
103
119
 
104
120
  pasteCells({
105
121
  selection: createMockSelection(0, 0),
106
122
  data: createMockData(),
107
- setData: mockSetLocalData,
108
123
  columns: createMockColumns(),
109
124
  editableColumns: "all",
110
125
  onAddEdits: mockOnAddEdits,
@@ -112,20 +127,17 @@ describe("pasteCells", () => {
112
127
 
113
128
  await vi.waitFor(() => {
114
129
  expect(mockOnAddEdits).not.toHaveBeenCalled();
115
- expect(mockSetLocalData).not.toHaveBeenCalled();
116
130
  });
117
131
  });
118
132
 
119
133
  it("should paste string data correctly", async () => {
120
134
  mockClipboard.readText.mockResolvedValue("David\t40\ttrue");
121
135
 
122
- const mockSetLocalData = vi.fn();
123
136
  const mockOnAddEdits = vi.fn();
124
137
 
125
138
  pasteCells({
126
139
  selection: createMockSelection(0, 0),
127
140
  data: createMockData(),
128
- setData: mockSetLocalData,
129
141
  columns: createMockColumns(),
130
142
  editableColumns: "all",
131
143
  onAddEdits: mockOnAddEdits,
@@ -140,16 +152,14 @@ describe("pasteCells", () => {
140
152
  });
141
153
  });
142
154
 
143
- it("should convert number values correctly", async () => {
155
+ it("should skip fractional values for integer columns", async () => {
144
156
  mockClipboard.readText.mockResolvedValue("Eve\t25.5\tfalse");
145
157
 
146
- const mockSetLocalData = vi.fn();
147
158
  const mockOnAddEdits = vi.fn();
148
159
 
149
160
  pasteCells({
150
161
  selection: createMockSelection(0, 0),
151
162
  data: createMockData(),
152
- setData: mockSetLocalData,
153
163
  columns: createMockColumns(),
154
164
  editableColumns: "all",
155
165
  onAddEdits: mockOnAddEdits,
@@ -158,7 +168,6 @@ describe("pasteCells", () => {
158
168
  await vi.waitFor(() => {
159
169
  expect(mockOnAddEdits).toHaveBeenCalledWith([
160
170
  { rowIdx: 0, columnId: "name", value: "Eve" },
161
- { rowIdx: 0, columnId: "age", value: 25.5 },
162
171
  { rowIdx: 0, columnId: "active", value: false },
163
172
  ]);
164
173
  });
@@ -167,13 +176,11 @@ describe("pasteCells", () => {
167
176
  it("should handle boolean conversion with different values", async () => {
168
177
  mockClipboard.readText.mockResolvedValue("Frank\t30\t1");
169
178
 
170
- const mockSetLocalData = vi.fn();
171
179
  const mockOnAddEdits = vi.fn();
172
180
 
173
181
  pasteCells({
174
182
  selection: createMockSelection(0, 0),
175
183
  data: createMockData(),
176
- setData: mockSetLocalData,
177
184
  columns: createMockColumns(),
178
185
  editableColumns: "all",
179
186
  onAddEdits: mockOnAddEdits,
@@ -191,13 +198,11 @@ describe("pasteCells", () => {
191
198
  it("should skip invalid number values", async () => {
192
199
  mockClipboard.readText.mockResolvedValue("Grace\tinvalid\tfalse");
193
200
 
194
- const mockSetLocalData = vi.fn();
195
201
  const mockOnAddEdits = vi.fn();
196
202
 
197
203
  pasteCells({
198
204
  selection: createMockSelection(0, 0),
199
205
  data: createMockData(),
200
- setData: mockSetLocalData,
201
206
  columns: createMockColumns(),
202
207
  editableColumns: "all",
203
208
  onAddEdits: mockOnAddEdits,
@@ -211,16 +216,36 @@ describe("pasteCells", () => {
211
216
  });
212
217
  });
213
218
 
219
+ it("should preserve unsafe integer precision", async () => {
220
+ mockClipboard.readText.mockResolvedValue("Grace\t9007199254740993\tfalse");
221
+
222
+ const mockOnAddEdits = vi.fn();
223
+
224
+ pasteCells({
225
+ selection: createMockSelection(0, 0),
226
+ data: createMockData(),
227
+ columns: createMockColumns(),
228
+ editableColumns: "all",
229
+ onAddEdits: mockOnAddEdits,
230
+ });
231
+
232
+ await vi.waitFor(() => {
233
+ expect(mockOnAddEdits).toHaveBeenCalledWith([
234
+ { rowIdx: 0, columnId: "name", value: "Grace" },
235
+ { rowIdx: 0, columnId: "age", value: "9007199254740993" },
236
+ { rowIdx: 0, columnId: "active", value: false },
237
+ ]);
238
+ });
239
+ });
240
+
214
241
  it("should handle multiple rows", async () => {
215
242
  mockClipboard.readText.mockResolvedValue("Hank\t40\ttrue\nIvy\t35\tfalse");
216
243
 
217
- const mockSetLocalData = vi.fn();
218
244
  const mockOnAddEdits = vi.fn();
219
245
 
220
246
  pasteCells({
221
247
  selection: createMockSelection(0, 0),
222
248
  data: createMockData(),
223
- setData: mockSetLocalData,
224
249
  columns: createMockColumns(),
225
250
  editableColumns: "all",
226
251
  onAddEdits: mockOnAddEdits,
@@ -243,13 +268,11 @@ describe("pasteCells", () => {
243
268
  "Jack\t45\ttrue\nKate\t50\tfalse\nLiam\t55\ttrue",
244
269
  );
245
270
 
246
- const mockSetLocalData = vi.fn();
247
271
  const mockOnAddEdits = vi.fn();
248
272
 
249
273
  pasteCells({
250
274
  selection: createMockSelection(0, 0),
251
275
  data: createMockData(), // Only 3 rows
252
- setData: mockSetLocalData,
253
276
  columns: createMockColumns(),
254
277
  editableColumns: "all",
255
278
  onAddEdits: mockOnAddEdits,
@@ -273,13 +296,11 @@ describe("pasteCells", () => {
273
296
  it("should respect column bounds - not exceed column count", async () => {
274
297
  mockClipboard.readText.mockResolvedValue("Mia\t30\ttrue\textra");
275
298
 
276
- const mockSetLocalData = vi.fn();
277
299
  const mockOnAddEdits = vi.fn();
278
300
 
279
301
  pasteCells({
280
302
  selection: createMockSelection(0, 0),
281
303
  data: createMockData(),
282
- setData: mockSetLocalData,
283
304
  columns: createMockColumns(), // Only 3 columns
284
305
  editableColumns: "all",
285
306
  onAddEdits: mockOnAddEdits,
@@ -297,13 +318,11 @@ describe("pasteCells", () => {
297
318
  it("should handle starting position offset", async () => {
298
319
  mockClipboard.readText.mockResolvedValue("Noah\t25\ttrue");
299
320
 
300
- const mockSetLocalData = vi.fn();
301
321
  const mockOnAddEdits = vi.fn();
302
322
 
303
323
  pasteCells({
304
324
  selection: createMockSelection(1, 1), // Start at column 1, row 1
305
325
  data: createMockData(),
306
- setData: mockSetLocalData,
307
326
  columns: createMockColumns(),
308
327
  editableColumns: "all",
309
328
  onAddEdits: mockOnAddEdits,
@@ -317,7 +336,6 @@ describe("pasteCells", () => {
317
336
  });
318
337
 
319
338
  it("should handle no selection", async () => {
320
- const mockSetLocalData = vi.fn();
321
339
  const mockOnAddEdits = vi.fn();
322
340
 
323
341
  pasteCells({
@@ -327,7 +345,6 @@ describe("pasteCells", () => {
327
345
  columns: CompactSelection.empty(),
328
346
  },
329
347
  data: createMockData(),
330
- setData: mockSetLocalData,
331
348
  columns: createMockColumns(),
332
349
  editableColumns: "all",
333
350
  onAddEdits: mockOnAddEdits,
@@ -335,7 +352,6 @@ describe("pasteCells", () => {
335
352
 
336
353
  await vi.waitFor(() => {
337
354
  expect(mockOnAddEdits).not.toHaveBeenCalled();
338
- expect(mockSetLocalData).not.toHaveBeenCalled();
339
355
  });
340
356
  });
341
357
 
@@ -344,7 +360,6 @@ describe("pasteCells", () => {
344
360
  new Error("Clipboard access denied"),
345
361
  );
346
362
 
347
- const mockSetLocalData = vi.fn();
348
363
  const mockOnAddEdits = vi.fn();
349
364
  const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {
350
365
  // Do nothing
@@ -353,7 +368,6 @@ describe("pasteCells", () => {
353
368
  pasteCells({
354
369
  selection: createMockSelection(0, 0),
355
370
  data: createMockData(),
356
- setData: mockSetLocalData,
357
371
  columns: createMockColumns(),
358
372
  editableColumns: "all",
359
373
  onAddEdits: mockOnAddEdits,
@@ -361,53 +375,21 @@ describe("pasteCells", () => {
361
375
 
362
376
  await vi.waitFor(() => {
363
377
  expect(mockOnAddEdits).not.toHaveBeenCalled();
364
- expect(mockSetLocalData).not.toHaveBeenCalled();
365
378
  });
366
379
 
367
380
  consoleSpy.mockRestore();
368
381
  });
369
382
 
370
- it("should update local data when edits are applied", async () => {
371
- mockClipboard.readText.mockResolvedValue("Olivia\t28\ttrue");
372
-
373
- const mockSetLocalData = vi.fn();
374
- const mockOnAddEdits = vi.fn();
375
-
376
- pasteCells({
377
- selection: createMockSelection(0, 0),
378
- data: createMockData(),
379
- setData: mockSetLocalData,
380
- columns: createMockColumns(),
381
- editableColumns: "all",
382
- onAddEdits: mockOnAddEdits,
383
- });
384
-
385
- await vi.waitFor(() => {
386
- expect(mockSetLocalData).toHaveBeenCalled();
387
-
388
- // Verify the updater function is called
389
- const updater = mockSetLocalData.mock.calls[0][0];
390
- const originalData = createMockData();
391
- const updatedData = updater(originalData);
392
-
393
- expect(updatedData[0].name).toBe("Olivia");
394
- expect(updatedData[0].age).toBe(28);
395
- expect(updatedData[0].active).toBe(true);
396
- });
397
- });
398
-
399
383
  it("should handle empty rows in clipboard data", async () => {
400
384
  mockClipboard.readText.mockResolvedValue(
401
385
  "Paul\t30\ttrue\n\nRachel\t25\tfalse",
402
386
  );
403
387
 
404
- const mockSetLocalData = vi.fn();
405
388
  const mockOnAddEdits = vi.fn();
406
389
 
407
390
  pasteCells({
408
391
  selection: createMockSelection(0, 0),
409
392
  data: createMockData(),
410
- setData: mockSetLocalData,
411
393
  columns: createMockColumns(),
412
394
  editableColumns: "all",
413
395
  onAddEdits: mockOnAddEdits,
@@ -430,13 +412,11 @@ describe("pasteCells", () => {
430
412
  "Olivia\t28\ttrue\nWilliams\t35\tfalse",
431
413
  );
432
414
 
433
- const mockSetLocalData = vi.fn();
434
415
  const mockOnAddEdits = vi.fn();
435
416
 
436
417
  pasteCells({
437
418
  selection: createMockSelection(0, 0),
438
419
  data: createMockData(),
439
- setData: mockSetLocalData,
440
420
  columns: createMockColumns(),
441
421
  editableColumns: ["age"],
442
422
  onAddEdits: mockOnAddEdits,
@@ -2,79 +2,99 @@
2
2
 
3
3
  import type { FieldTypes } from "@/components/data-table/types";
4
4
  import type { DataType } from "@/core/kernel/messages";
5
+ import { logNever } from "@/utils/assertNever";
5
6
  import { Logger } from "@/utils/Logger";
6
-
7
- /**
8
- * For modifying data, we do not worry about the order of the columns.
9
- * Because we define getCellContent based on columnTitle, the order does not matter.
10
- *
11
- * For column fields, we do care about the order
12
- */
13
-
14
- export function removeColumn<T>(data: T[], columnIdx: number): T[] {
15
- return data.map((row) => {
16
- const rowData = row as Record<string, unknown>;
17
- const keys = Object.keys(rowData);
18
-
19
- // If the column index is out of bounds, return the original row
20
- if (columnIdx < 0 || columnIdx >= keys.length) {
21
- return rowData as T;
7
+ import { BulkEdit, type EditorRow } from "./types";
8
+
9
+ export function orderColumnFields(
10
+ columnFields: FieldTypes,
11
+ columnNames: string[],
12
+ ): FieldTypes {
13
+ const ordered = new Map<string, DataType>();
14
+ for (const columnName of columnNames) {
15
+ ordered.set(columnName, columnFields.get(columnName) ?? "unknown");
16
+ }
17
+ for (const [columnName, dataType] of columnFields) {
18
+ if (!ordered.has(columnName)) {
19
+ ordered.set(columnName, dataType);
22
20
  }
21
+ }
22
+ return ordered;
23
+ }
23
24
 
24
- const keyToRemove = keys[columnIdx];
25
-
26
- // Create new object without the specified key
27
- const { [keyToRemove]: _, ...rest } = rowData;
28
- return rest as T;
25
+ export function removeColumn(
26
+ data: EditorRow[],
27
+ columnName: string,
28
+ ): EditorRow[] {
29
+ return data.map((row) => {
30
+ const { [columnName]: _, ...rest } = row;
31
+ return rest;
29
32
  });
30
33
  }
31
34
 
32
- /**
33
- * Insert a new column at the end of the data.
34
- * @param data - The data to insert the column into
35
- * @param newName - The name of the new column
36
- * @returns The data with the new column inserted at the end
37
- */
38
- export function insertColumn<T>(data: T[], newName?: string): T[] {
35
+ export function insertColumn(
36
+ data: EditorRow[],
37
+ newName?: string,
38
+ columnIdx?: number,
39
+ ): EditorRow[] {
39
40
  if (!newName) {
40
41
  return data;
41
42
  }
42
43
 
43
- return data.map((row) => ({
44
- ...(row as Record<string, unknown>),
45
- [newName]: "",
46
- })) as T[];
44
+ return data.map((row) => {
45
+ const entries = Object.entries(row);
46
+ const insertAt = Math.max(
47
+ 0,
48
+ Math.min(columnIdx ?? entries.length, entries.length),
49
+ );
50
+ entries.splice(insertAt, 0, [newName, ""]);
51
+ return Object.fromEntries(entries);
52
+ });
47
53
  }
48
54
 
49
- export function renameColumn<T>(
50
- data: T[],
55
+ export function renameColumn(
56
+ data: EditorRow[],
51
57
  oldName: string,
52
58
  newName: string,
53
- ): T[] {
59
+ ): EditorRow[] {
54
60
  if (!oldName || !newName || oldName === newName) {
55
61
  return data;
56
62
  }
63
+ if (data.some((row) => Object.hasOwn(row, newName))) {
64
+ return data;
65
+ }
57
66
 
58
67
  return data.map((row) => {
59
- const rowData = row as Record<string, unknown>;
60
- const { [oldName]: _, ...rest } = rowData;
61
- return { ...rest, [newName]: rowData[oldName] } as T;
68
+ return Object.fromEntries(
69
+ Object.entries(row).map(([columnName, value]) => [
70
+ columnName === oldName ? newName : columnName,
71
+ value,
72
+ ]),
73
+ );
62
74
  });
63
75
  }
64
76
 
65
- // Order of columns is important
66
- export function modifyColumnFields(opts: {
77
+ type ModifyColumnFieldsOptions = {
67
78
  columnFields: FieldTypes;
68
79
  columnIdx: number;
69
- type: "insert" | "remove" | "rename";
70
- dataType?: DataType;
71
- newColumnName?: string;
72
- }): FieldTypes {
73
- const { columnFields, columnIdx, type, dataType, newColumnName } = opts;
80
+ } & (
81
+ | { type: typeof BulkEdit.Remove }
82
+ | {
83
+ type: typeof BulkEdit.Insert | typeof BulkEdit.Rename;
84
+ newColumnName: string;
85
+ dataType?: DataType;
86
+ }
87
+ );
74
88
 
75
- switch (type) {
76
- case "insert": {
77
- if (!newColumnName) {
89
+ // Order of columns is important
90
+ export function modifyColumnFields(
91
+ opts: ModifyColumnFieldsOptions,
92
+ ): FieldTypes {
93
+ const { columnFields, columnIdx } = opts;
94
+
95
+ switch (opts.type) {
96
+ case BulkEdit.Insert: {
97
+ if (!opts.newColumnName) {
78
98
  Logger.error("newName is required for insert");
79
99
  return columnFields;
80
100
  }
@@ -82,12 +102,12 @@ export function modifyColumnFields(opts: {
82
102
  const entries = [...columnFields.entries()];
83
103
  const newEntries: Array<[string, DataType]> = [
84
104
  ...entries.slice(0, columnIdx),
85
- [newColumnName, dataType || "string"],
105
+ [opts.newColumnName, opts.dataType ?? "string"],
86
106
  ...entries.slice(columnIdx),
87
107
  ];
88
108
  return new Map(newEntries);
89
109
  }
90
- case "remove": {
110
+ case BulkEdit.Remove: {
91
111
  if (columnIdx < 0 || columnIdx >= columnFields.size) {
92
112
  return columnFields;
93
113
  }
@@ -101,8 +121,8 @@ export function modifyColumnFields(opts: {
101
121
  }
102
122
  return columnFields;
103
123
  }
104
- case "rename": {
105
- if (!newColumnName) {
124
+ case BulkEdit.Rename: {
125
+ if (!opts.newColumnName) {
106
126
  Logger.error("newName is required for rename");
107
127
  return columnFields;
108
128
  }
@@ -115,10 +135,13 @@ export function modifyColumnFields(opts: {
115
135
  const entries = [...columnFields.entries()];
116
136
  const newEntries: Array<[string, DataType]> = [
117
137
  ...entries.slice(0, columnIdx),
118
- [newColumnName, dataType || "string"],
138
+ [opts.newColumnName, opts.dataType ?? "string"],
119
139
  ...entries.slice(columnIdx + 1),
120
140
  ];
121
141
  return new Map(newEntries);
122
142
  }
143
+ default:
144
+ logNever(opts);
145
+ return columnFields;
123
146
  }
124
147
  }
@@ -0,0 +1,108 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { FieldTypes } from "@/components/data-table/types";
4
+ import { logNever } from "@/utils/assertNever";
5
+ import {
6
+ insertColumn,
7
+ modifyColumnFields,
8
+ removeColumn,
9
+ renameColumn,
10
+ } from "./data-utils";
11
+ import { isColumnEdit, isPositionalEdit, isRowEdit } from "./glide-utils";
12
+ import { BulkEdit, type EditorState, type Edits } from "./types";
13
+
14
+ export function applyEditorEdits(
15
+ state: EditorState,
16
+ edits: Edits["edits"],
17
+ ): EditorState {
18
+ let nextData = [...state.data];
19
+ let nextColumnFields: FieldTypes = new Map(state.columnFields);
20
+
21
+ for (const edit of edits) {
22
+ if (isPositionalEdit(edit)) {
23
+ if (edit.rowIdx < 0) {
24
+ continue;
25
+ }
26
+ while (nextData.length <= edit.rowIdx) {
27
+ nextData.push({});
28
+ }
29
+ nextData[edit.rowIdx] = {
30
+ ...nextData[edit.rowIdx],
31
+ [edit.columnId]: edit.value,
32
+ };
33
+ if (!nextColumnFields.has(edit.columnId)) {
34
+ nextColumnFields.set(edit.columnId, "unknown");
35
+ }
36
+ continue;
37
+ }
38
+
39
+ if (isRowEdit(edit)) {
40
+ if (
41
+ edit.type === BulkEdit.Remove &&
42
+ edit.rowIdx >= 0 &&
43
+ edit.rowIdx < nextData.length
44
+ ) {
45
+ nextData.splice(edit.rowIdx, 1);
46
+ }
47
+ continue;
48
+ }
49
+
50
+ if (!isColumnEdit(edit)) {
51
+ continue;
52
+ }
53
+
54
+ const columnName = [...nextColumnFields.keys()][edit.columnIdx];
55
+ switch (edit.type) {
56
+ case BulkEdit.Remove:
57
+ if (columnName === undefined) {
58
+ break;
59
+ }
60
+ nextData = removeColumn(nextData, columnName);
61
+ nextColumnFields = modifyColumnFields({
62
+ columnFields: nextColumnFields,
63
+ columnIdx: edit.columnIdx,
64
+ type: "remove",
65
+ });
66
+ break;
67
+ case BulkEdit.Insert:
68
+ if (
69
+ !edit.newName ||
70
+ edit.columnIdx < 0 ||
71
+ edit.columnIdx > nextColumnFields.size ||
72
+ nextColumnFields.has(edit.newName)
73
+ ) {
74
+ break;
75
+ }
76
+ nextData = insertColumn(nextData, edit.newName, edit.columnIdx);
77
+ nextColumnFields = modifyColumnFields({
78
+ columnFields: nextColumnFields,
79
+ columnIdx: edit.columnIdx,
80
+ type: "insert",
81
+ dataType: edit.dataType,
82
+ newColumnName: edit.newName,
83
+ });
84
+ break;
85
+ case BulkEdit.Rename:
86
+ if (
87
+ columnName === undefined ||
88
+ !edit.newName ||
89
+ nextColumnFields.has(edit.newName)
90
+ ) {
91
+ break;
92
+ }
93
+ nextData = renameColumn(nextData, columnName, edit.newName);
94
+ nextColumnFields = modifyColumnFields({
95
+ columnFields: nextColumnFields,
96
+ columnIdx: edit.columnIdx,
97
+ type: "rename",
98
+ dataType: nextColumnFields.get(columnName),
99
+ newColumnName: edit.newName,
100
+ });
101
+ break;
102
+ default:
103
+ logNever(edit);
104
+ }
105
+ }
106
+
107
+ return { data: nextData, columnFields: nextColumnFields };
108
+ }