@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.
- package/dist/assets/{CellStatus-Bfn_Ysig.js → CellStatus-DMQfH9wD.js} +1 -1
- package/dist/assets/{JsonOutput-DFd7q8lz.js → JsonOutput-B7sDhGzR.js} +1 -1
- package/dist/assets/{MarimoErrorOutput-DfRv7_jc.js → MarimoErrorOutput-C0vk393J.js} +1 -1
- package/dist/assets/{RenderHTML-Di-OdQjY.js → RenderHTML-BnMaGpV8.js} +1 -1
- package/dist/assets/{RunButton-r21rSvYz.js → RunButton-nocIODpS.js} +1 -1
- package/dist/assets/{add-cell-with-ai-Cc3EENeX.js → add-cell-with-ai-B_TSaqjJ.js} +1 -1
- package/dist/assets/{add-connection-dialog-Bce8iA2c.js → add-connection-dialog-DOuojDa-.js} +1 -1
- package/dist/assets/{agent-panel-COvCNnaD.js → agent-panel-CkKCGjDp.js} +1 -1
- package/dist/assets/{ai-model-dropdown-D7nJTvh4.js → ai-model-dropdown-CfWMIzjl.js} +1 -1
- package/dist/assets/{app-config-button-C0IsSBiP.js → app-config-button-ii-Hj5CZ.js} +1 -1
- package/dist/assets/{blob-CbiWlEJN.js → blob-CYM8xQsK.js} +1 -1
- package/dist/assets/{cell-editor-DnyPiOtX.js → cell-editor-BVb6iKbw.js} +1 -1
- package/dist/assets/{cell-link-Z8JrraTv.js → cell-link-DCOAqVkE.js} +1 -1
- package/dist/assets/{cells-CD9Jbpoa.js → cells-C6lBv8kg.js} +51 -51
- package/dist/assets/{chat-display-Dxad67GU.js → chat-display-B6GBZDq8.js} +1 -1
- package/dist/assets/{chat-panel-b6eYwY8N.js → chat-panel-wWuBXhXw.js} +1 -1
- package/dist/assets/{chat-ui-DWgQGHiC.js → chat-ui-CQ7A3qdB.js} +1 -1
- package/dist/assets/click-outside-container-C5RF3h4V.js +1 -0
- package/dist/assets/{column-preview-_GkpGxG-.js → column-preview-CatwdUda.js} +1 -1
- package/dist/assets/{command-palette-BeHx267T.js → command-palette-DcPM_WiO.js} +1 -1
- package/dist/assets/{common-Bb2s_Imb.js → common-B4aMxphz.js} +1 -1
- package/dist/assets/{components-D5125bx8.js → components-D8-ffQsq.js} +1 -1
- package/dist/assets/data-grid-overlay-editor-CFYeXDX3.js +1 -0
- package/dist/assets/data-grid-types-DgHVN_Zz.js +1 -0
- package/dist/assets/{datasource-DTtEQWz7.js → datasource-D-_jFtGs.js} +1 -1
- package/dist/assets/{dependency-graph-panel-DiMc1DOt.js → dependency-graph-panel-BQli6zxh.js} +1 -1
- package/dist/assets/{documentation-panel-ChEa8OHk.js → documentation-panel-_QE3nPOg.js} +1 -1
- package/dist/assets/{download-BKMVrvLm.js → download-Cqjqrhzo.js} +1 -1
- package/dist/assets/{edit-page-C3Uo5Msq.js → edit-page-RuyN164g.js} +4 -4
- package/dist/assets/{error-entries-DI864MPn.js → error-entries-c5sIySli.js} +1 -1
- package/dist/assets/{error-panel-COOOtEg7.js → error-panel-CXKrWK_u.js} +1 -1
- package/dist/assets/{file-explorer-panel-DeF49Ng1.js → file-explorer-panel-BZiAb4x_.js} +1 -1
- package/dist/assets/{file-icons-CmmWEUGv.js → file-icons-uUY_hGYm.js} +1 -1
- package/dist/assets/{file-name-input-9sqInamo.js → file-name-input-DWUKg4LS.js} +1 -1
- package/dist/assets/{floating-outline-BxMKEygY.js → floating-outline-DhGtHg6T.js} +1 -1
- package/dist/assets/glide-data-editor-DZ2Minh8.js +131 -0
- package/dist/assets/glide-utils-CR1sWLZU.js +2 -0
- package/dist/assets/{home-page-G9DBo7dA.js → home-page-Bka_7H02.js} +1 -1
- package/dist/assets/index-ChbtcNW2.js +29 -0
- package/dist/assets/{layout-RSFs10mJ.js → layout-Uv1ZP5XB.js} +3 -3
- package/dist/assets/{logs-panel-DaLUKPBX.js → logs-panel-ragZa1sf.js} +1 -1
- package/dist/assets/{markdown-renderer-Cj57nlKH.js → markdown-renderer-B-uA_6mP.js} +1 -1
- package/dist/assets/{useNonce-BTM6-ANi.js → marked.esm-B_3iMIUq.js} +10 -10
- package/dist/assets/{name-cell-input-D0M2lddE.js → name-cell-input-gC1flbyT.js} +1 -1
- package/dist/assets/{outline-panel-UDArUDGw.js → outline-panel-D7H7bhOh.js} +1 -1
- package/dist/assets/{packages-panel-B8AaXFcI.js → packages-panel-BZnsUm4S.js} +1 -1
- package/dist/assets/{panel-accordion-state-COnGLerF.js → panel-accordion-state-BLU7OiMD.js} +1 -1
- package/dist/assets/{panels-DecWMR6G.js → panels-B3Y6fue9.js} +1 -1
- package/dist/assets/{process-output-CUf8H__q.js → process-output-DQQ8T5gv.js} +1 -1
- package/dist/assets/{readonly-code-display-Bof9MzZO.js → readonly-code-display-Bc0VX9i6.js} +1 -1
- package/dist/assets/{readonly-python-code-GMl_IocT.js → readonly-python-code-D2HXwAfT.js} +1 -1
- package/dist/assets/{reveal-component-yDciCkP9.js → reveal-component-BN1C8zFu.js} +1 -1
- package/dist/assets/{run-page-CCk94ccj.js → run-page-ByVvsH9G.js} +1 -1
- package/dist/assets/{scratchpad-panel-EL1m3Huh.js → scratchpad-panel-BJHlMBZV.js} +1 -1
- package/dist/assets/{session-panel-DKjUcjvg.js → session-panel-CdCKWAOf.js} +1 -1
- package/dist/assets/{skeleton-D71npu63.js → skeleton-DirOIQrj.js} +1 -1
- package/dist/assets/{snippets-panel-njDHc9qT.js → snippets-panel-2jq36DdG.js} +1 -1
- package/dist/assets/{state-CwrzGizs.js → state-C9O-2hQL.js} +1 -1
- package/dist/assets/{state-mneM4Ao8.js → state-DyxPdcEa.js} +1 -1
- package/dist/assets/{tracing-CGZCxvWG.js → tracing-91_akEkv.js} +1 -1
- package/dist/assets/{tracing-panel-4lmoUwoQ.js → tracing-panel-BvE81RYx.js} +2 -2
- package/dist/assets/{tree-actions-Bjyy0vFK.js → tree-actions-CGkbUSfG.js} +1 -1
- package/dist/assets/{useAddCell-h4OWLK5y.js → useAddCell-DqvY2ulx.js} +1 -1
- package/dist/assets/{useCellActionButton-BvuYvLCr.js → useCellActionButton-cv6xOsnc.js} +1 -1
- package/dist/assets/{useDeleteCell-Buwee6vZ.js → useDeleteCell-DB-bhhnO.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-CLR1fyXm.js → useDependencyPanelTab-BDnVzNV-.js} +1 -1
- package/dist/assets/{useNotebookActions-C0Uw9roU.js → useNotebookActions-Dxp4gTEa.js} +1 -1
- package/dist/assets/{useRunCells-C6416Qeo.js → useRunCells-GCTS1YFv.js} +1 -1
- package/dist/assets/{useSplitCell-Ip3d7gP0.js → useSplitCell-DhT9BA2_.js} +1 -1
- package/dist/index.html +22 -21
- package/package.json +1 -1
- package/src/plugins/impl/DataEditorPlugin.tsx +38 -70
- package/src/plugins/impl/__tests__/DataEditorPlugin.test.ts +152 -1
- package/src/plugins/impl/data-editor/__tests__/data-utils.test.ts +73 -78
- package/src/plugins/impl/data-editor/__tests__/editor-state.test.ts +113 -0
- package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +49 -13
- package/src/plugins/impl/data-editor/__tests__/glide-utils.test.ts +44 -64
- package/src/plugins/impl/data-editor/data-utils.ts +76 -53
- package/src/plugins/impl/data-editor/editor-state.ts +108 -0
- package/src/plugins/impl/data-editor/glide-data-editor.tsx +40 -223
- package/src/plugins/impl/data-editor/glide-utils.ts +71 -31
- package/src/plugins/impl/data-editor/types.ts +30 -16
- package/dist/assets/click-outside-container-n76g8dsY.js +0 -1
- package/dist/assets/data-grid-overlay-editor-DCmC8-tb.js +0 -1
- package/dist/assets/glide-data-editor-CzalkCph.js +0 -132
- package/dist/assets/index-BmNpxjzV.js +0 -29
- 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
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
50
|
-
data:
|
|
55
|
+
export function renameColumn(
|
|
56
|
+
data: EditorRow[],
|
|
51
57
|
oldName: string,
|
|
52
58
|
newName: string,
|
|
53
|
-
):
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
66
|
-
export function modifyColumnFields(opts: {
|
|
77
|
+
type ModifyColumnFieldsOptions = {
|
|
67
78
|
columnFields: FieldTypes;
|
|
68
79
|
columnIdx: number;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
105
|
+
[opts.newColumnName, opts.dataType ?? "string"],
|
|
86
106
|
...entries.slice(columnIdx),
|
|
87
107
|
];
|
|
88
108
|
return new Map(newEntries);
|
|
89
109
|
}
|
|
90
|
-
case
|
|
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
|
|
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
|
|
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
|
+
}
|