@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
|
@@ -1,9 +1,55 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import React, { Suspense } from "react";
|
|
4
|
+
import { act, render, screen, waitFor } from "@testing-library/react";
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import type { z } from "zod";
|
|
7
|
+
import type { IPluginProps } from "../../types";
|
|
8
|
+
import type { EditorRow, Edits } from "../data-editor/types";
|
|
4
9
|
import { DataEditorPlugin } from "../DataEditorPlugin";
|
|
5
10
|
|
|
11
|
+
type PluginData = z.infer<typeof DataEditorPlugin.validator>;
|
|
12
|
+
|
|
13
|
+
const mocks = vi.hoisted(() => ({
|
|
14
|
+
loadData: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock("../vega/loader", () => ({
|
|
18
|
+
vegaLoadData: mocks.loadData,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("../data-editor/glide-data-editor", async () => {
|
|
22
|
+
const React = await import("react");
|
|
23
|
+
return {
|
|
24
|
+
default: (props: { data: EditorRow[] }) =>
|
|
25
|
+
React.createElement(
|
|
26
|
+
"pre",
|
|
27
|
+
{ "data-testid": "editor-data" },
|
|
28
|
+
JSON.stringify(props.data),
|
|
29
|
+
),
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function renderPlugin(data: PluginData, value: Edits) {
|
|
34
|
+
const props: IPluginProps<Edits, PluginData> = {
|
|
35
|
+
host: document.createElement("div"),
|
|
36
|
+
data,
|
|
37
|
+
value,
|
|
38
|
+
setValue: vi.fn(),
|
|
39
|
+
functions: {},
|
|
40
|
+
};
|
|
41
|
+
return React.createElement(
|
|
42
|
+
Suspense,
|
|
43
|
+
{ fallback: null },
|
|
44
|
+
DataEditorPlugin.render(props),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
6
48
|
describe("DataEditorPlugin", () => {
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
mocks.loadData.mockReset();
|
|
51
|
+
});
|
|
52
|
+
|
|
7
53
|
it("normalizes an unrecognized field type", () => {
|
|
8
54
|
const result = DataEditorPlugin.validator.parse({
|
|
9
55
|
initialValue: { edits: [] },
|
|
@@ -27,4 +73,109 @@ describe("DataEditorPlugin", () => {
|
|
|
27
73
|
|
|
28
74
|
expect(result.fieldTypes).toEqual([["geom", ["geometry", "geometry"]]]);
|
|
29
75
|
});
|
|
76
|
+
|
|
77
|
+
it("applies the latest edits when an async data load completes", async () => {
|
|
78
|
+
let resolveLoad: ((data: EditorRow[]) => void) | undefined;
|
|
79
|
+
mocks.loadData.mockImplementationOnce(
|
|
80
|
+
() =>
|
|
81
|
+
new Promise<EditorRow[]>((resolve) => {
|
|
82
|
+
resolveLoad = resolve;
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const data = DataEditorPlugin.validator.parse({
|
|
87
|
+
initialValue: { edits: [] },
|
|
88
|
+
label: null,
|
|
89
|
+
data: "data.csv",
|
|
90
|
+
fieldTypes: null,
|
|
91
|
+
columnNames: ["name"],
|
|
92
|
+
editableColumns: "all",
|
|
93
|
+
});
|
|
94
|
+
const result = render(renderPlugin(data, { edits: [] }));
|
|
95
|
+
result.rerender(
|
|
96
|
+
renderPlugin(data, {
|
|
97
|
+
edits: [{ rowIdx: 0, columnId: "name", value: "latest" }],
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
expect(resolveLoad).toBeDefined();
|
|
101
|
+
resolveLoad?.([{ name: "original" }]);
|
|
102
|
+
|
|
103
|
+
await waitFor(() => {
|
|
104
|
+
expect(screen.getByTestId("editor-data")).toHaveTextContent(
|
|
105
|
+
JSON.stringify([{ name: "latest" }]),
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("applies edit updates after data has loaded", async () => {
|
|
111
|
+
const data = DataEditorPlugin.validator.parse({
|
|
112
|
+
initialValue: { edits: [] },
|
|
113
|
+
label: null,
|
|
114
|
+
data: [{ name: "original" }],
|
|
115
|
+
fieldTypes: null,
|
|
116
|
+
columnNames: ["name"],
|
|
117
|
+
editableColumns: "all",
|
|
118
|
+
});
|
|
119
|
+
const result = render(renderPlugin(data, { edits: [] }));
|
|
120
|
+
await waitFor(() => {
|
|
121
|
+
expect(screen.getByTestId("editor-data")).toHaveTextContent(
|
|
122
|
+
JSON.stringify([{ name: "original" }]),
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
result.rerender(
|
|
127
|
+
renderPlugin(data, {
|
|
128
|
+
edits: [{ rowIdx: 0, columnId: "name", value: "updated" }],
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
await waitFor(() => {
|
|
133
|
+
expect(screen.getByTestId("editor-data")).toHaveTextContent(
|
|
134
|
+
JSON.stringify([{ name: "updated" }]),
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("ignores a superseded async data load", async () => {
|
|
140
|
+
let resolveFirst: ((data: EditorRow[]) => void) | undefined;
|
|
141
|
+
let resolveSecond: ((data: EditorRow[]) => void) | undefined;
|
|
142
|
+
mocks.loadData.mockImplementation((source: string) => {
|
|
143
|
+
return new Promise<EditorRow[]>((resolve) => {
|
|
144
|
+
if (source === "first.csv") {
|
|
145
|
+
resolveFirst = resolve;
|
|
146
|
+
} else {
|
|
147
|
+
resolveSecond = resolve;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const makeData = (source: string) =>
|
|
153
|
+
DataEditorPlugin.validator.parse({
|
|
154
|
+
initialValue: { edits: [] },
|
|
155
|
+
label: null,
|
|
156
|
+
data: source,
|
|
157
|
+
fieldTypes: null,
|
|
158
|
+
columnNames: ["source"],
|
|
159
|
+
editableColumns: "all",
|
|
160
|
+
});
|
|
161
|
+
const result = render(renderPlugin(makeData("first.csv"), { edits: [] }));
|
|
162
|
+
result.rerender(renderPlugin(makeData("second.csv"), { edits: [] }));
|
|
163
|
+
|
|
164
|
+
expect(resolveFirst).toBeDefined();
|
|
165
|
+
expect(resolveSecond).toBeDefined();
|
|
166
|
+
resolveSecond?.([{ source: "second" }]);
|
|
167
|
+
await waitFor(() => {
|
|
168
|
+
expect(screen.getByTestId("editor-data")).toHaveTextContent(
|
|
169
|
+
JSON.stringify([{ source: "second" }]),
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await act(async () => {
|
|
174
|
+
resolveFirst?.([{ source: "first" }]);
|
|
175
|
+
await Promise.resolve();
|
|
176
|
+
});
|
|
177
|
+
expect(screen.getByTestId("editor-data")).toHaveTextContent(
|
|
178
|
+
JSON.stringify([{ source: "second" }]),
|
|
179
|
+
);
|
|
180
|
+
});
|
|
30
181
|
});
|
|
@@ -6,6 +6,7 @@ import type { DataType } from "@/core/kernel/messages";
|
|
|
6
6
|
import {
|
|
7
7
|
insertColumn,
|
|
8
8
|
modifyColumnFields,
|
|
9
|
+
orderColumnFields,
|
|
9
10
|
removeColumn,
|
|
10
11
|
renameColumn,
|
|
11
12
|
} from "../data-utils";
|
|
@@ -15,16 +16,43 @@ import {
|
|
|
15
16
|
const asFieldTypes = (obj: Record<string, DataType>): FieldTypes =>
|
|
16
17
|
new Map(Object.entries(obj));
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
const testData = [
|
|
20
|
+
{ int: 1, string: "a", bool: "True", datetime: "2025-07-12 00:07:13" },
|
|
21
|
+
{ int: 2, string: "b", bool: "False", datetime: null },
|
|
22
|
+
{ int: 3, string: "c", bool: null, datetime: "2025-07-12 00:07:13" },
|
|
23
|
+
{ int: 0, string: "", bool: "", datetime: "" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const mixedData = [{ a: 1, b: 2 }, { a: 3, b: 4, c: 5 }, { a: 6 }];
|
|
27
|
+
|
|
28
|
+
const dataWithNulls = [
|
|
29
|
+
{ a: 1, b: null, c: undefined },
|
|
30
|
+
{ a: null, b: 2, c: 3 },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
describe("orderColumnFields", () => {
|
|
34
|
+
it("uses explicit order for numeric-looking column names", () => {
|
|
35
|
+
const inferred = new Map([
|
|
36
|
+
["2", "string"],
|
|
37
|
+
["10", "string"],
|
|
38
|
+
]) as FieldTypes;
|
|
39
|
+
|
|
40
|
+
expect([...orderColumnFields(inferred, ["10", "2"])]).toEqual([
|
|
41
|
+
["10", "string"],
|
|
42
|
+
["2", "string"],
|
|
43
|
+
]);
|
|
44
|
+
});
|
|
25
45
|
|
|
26
|
-
it("
|
|
27
|
-
|
|
46
|
+
it("creates unknown fields for an empty table", () => {
|
|
47
|
+
expect([...orderColumnFields(new Map(), ["value"])]).toEqual([
|
|
48
|
+
["value", "unknown"],
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("removeColumn", () => {
|
|
54
|
+
it("should remove the first column", () => {
|
|
55
|
+
const result = removeColumn(testData, "int");
|
|
28
56
|
|
|
29
57
|
expect(result).toEqual([
|
|
30
58
|
{ string: "a", bool: "True", datetime: "2025-07-12 00:07:13" },
|
|
@@ -34,8 +62,8 @@ describe("removeColumn", () => {
|
|
|
34
62
|
]);
|
|
35
63
|
});
|
|
36
64
|
|
|
37
|
-
it("should remove
|
|
38
|
-
const result = removeColumn(testData,
|
|
65
|
+
it("should remove the second column", () => {
|
|
66
|
+
const result = removeColumn(testData, "string");
|
|
39
67
|
|
|
40
68
|
expect(result).toEqual([
|
|
41
69
|
{ int: 1, bool: "True", datetime: "2025-07-12 00:07:13" },
|
|
@@ -45,8 +73,8 @@ describe("removeColumn", () => {
|
|
|
45
73
|
]);
|
|
46
74
|
});
|
|
47
75
|
|
|
48
|
-
it("should remove
|
|
49
|
-
const result = removeColumn(testData,
|
|
76
|
+
it("should remove the third column", () => {
|
|
77
|
+
const result = removeColumn(testData, "bool");
|
|
50
78
|
|
|
51
79
|
expect(result).toEqual([
|
|
52
80
|
{ int: 1, string: "a", datetime: "2025-07-12 00:07:13" },
|
|
@@ -56,8 +84,8 @@ describe("removeColumn", () => {
|
|
|
56
84
|
]);
|
|
57
85
|
});
|
|
58
86
|
|
|
59
|
-
it("should remove
|
|
60
|
-
const result = removeColumn(testData,
|
|
87
|
+
it("should remove the last column", () => {
|
|
88
|
+
const result = removeColumn(testData, "datetime");
|
|
61
89
|
|
|
62
90
|
expect(result).toEqual([
|
|
63
91
|
{ int: 1, string: "a", bool: "True" },
|
|
@@ -67,40 +95,26 @@ describe("removeColumn", () => {
|
|
|
67
95
|
]);
|
|
68
96
|
});
|
|
69
97
|
|
|
70
|
-
it("should handle removing non-existent column
|
|
71
|
-
const result = removeColumn(testData,
|
|
72
|
-
// Should return the original data since the index doesn't exist
|
|
73
|
-
expect(result).toEqual(testData);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("should handle negative index", () => {
|
|
77
|
-
const result = removeColumn(testData, -1);
|
|
78
|
-
// Should return the original data since negative index is invalid
|
|
98
|
+
it("should handle removing a non-existent column", () => {
|
|
99
|
+
const result = removeColumn(testData, "missing");
|
|
79
100
|
expect(result).toEqual(testData);
|
|
80
101
|
});
|
|
81
102
|
|
|
82
103
|
it("should preserve original data structure", () => {
|
|
83
104
|
const originalData = [...testData];
|
|
84
|
-
removeColumn(testData,
|
|
105
|
+
removeColumn(testData, "string");
|
|
85
106
|
// Original data should remain unchanged
|
|
86
107
|
expect(testData).toEqual(originalData);
|
|
87
108
|
});
|
|
88
109
|
|
|
89
110
|
it("should handle objects with different property counts", () => {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
const result = removeColumn(mixedData, 1);
|
|
111
|
+
const result = removeColumn(mixedData, "b");
|
|
93
112
|
|
|
94
113
|
expect(result).toEqual([{ a: 1 }, { a: 3, c: 5 }, { a: 6 }]);
|
|
95
114
|
});
|
|
96
115
|
|
|
97
116
|
it("should handle null and undefined values in data", () => {
|
|
98
|
-
const
|
|
99
|
-
{ a: 1, b: null, c: undefined },
|
|
100
|
-
{ a: null, b: 2, c: 3 },
|
|
101
|
-
];
|
|
102
|
-
|
|
103
|
-
const result = removeColumn(dataWithNulls, 1);
|
|
117
|
+
const result = removeColumn(dataWithNulls, "b");
|
|
104
118
|
|
|
105
119
|
expect(result).toEqual([
|
|
106
120
|
{ a: 1, c: undefined },
|
|
@@ -110,15 +124,8 @@ describe("removeColumn", () => {
|
|
|
110
124
|
});
|
|
111
125
|
|
|
112
126
|
describe("insertColumn", () => {
|
|
113
|
-
const testData = [
|
|
114
|
-
{ int: 1, string: "a", bool: "True", datetime: "2025-07-12 00:07:13" },
|
|
115
|
-
{ int: 2, string: "b", bool: "False", datetime: null },
|
|
116
|
-
{ int: 3, string: "c", bool: null, datetime: "2025-07-12 00:07:13" },
|
|
117
|
-
{ int: 0, string: "", bool: "", datetime: "" },
|
|
118
|
-
];
|
|
119
|
-
|
|
120
127
|
it("should insert column at index 0", () => {
|
|
121
|
-
const result = insertColumn(testData, "newColumn");
|
|
128
|
+
const result = insertColumn(testData, "newColumn", 0);
|
|
122
129
|
|
|
123
130
|
const expected = [
|
|
124
131
|
{
|
|
@@ -139,10 +146,17 @@ describe("insertColumn", () => {
|
|
|
139
146
|
{ newColumn: "", int: 0, string: "", bool: "", datetime: "" },
|
|
140
147
|
];
|
|
141
148
|
expect(result).toEqual(expected);
|
|
149
|
+
expect(Object.keys(result[0])).toEqual([
|
|
150
|
+
"newColumn",
|
|
151
|
+
"int",
|
|
152
|
+
"string",
|
|
153
|
+
"bool",
|
|
154
|
+
"datetime",
|
|
155
|
+
]);
|
|
142
156
|
});
|
|
143
157
|
|
|
144
158
|
it("should insert column at index 1", () => {
|
|
145
|
-
const result = insertColumn(testData, "newColumn");
|
|
159
|
+
const result = insertColumn(testData, "newColumn", 1);
|
|
146
160
|
|
|
147
161
|
expect(result).toEqual([
|
|
148
162
|
{
|
|
@@ -162,10 +176,17 @@ describe("insertColumn", () => {
|
|
|
162
176
|
},
|
|
163
177
|
{ int: 0, newColumn: "", string: "", bool: "", datetime: "" },
|
|
164
178
|
]);
|
|
179
|
+
expect(Object.keys(result[0])).toEqual([
|
|
180
|
+
"int",
|
|
181
|
+
"newColumn",
|
|
182
|
+
"string",
|
|
183
|
+
"bool",
|
|
184
|
+
"datetime",
|
|
185
|
+
]);
|
|
165
186
|
});
|
|
166
187
|
|
|
167
188
|
it("should insert column at index 2", () => {
|
|
168
|
-
const result = insertColumn(testData, "newColumn");
|
|
189
|
+
const result = insertColumn(testData, "newColumn", 2);
|
|
169
190
|
|
|
170
191
|
expect(result).toEqual([
|
|
171
192
|
{
|
|
@@ -188,7 +209,7 @@ describe("insertColumn", () => {
|
|
|
188
209
|
});
|
|
189
210
|
|
|
190
211
|
it("should insert column at index 3", () => {
|
|
191
|
-
const result = insertColumn(testData, "newColumn");
|
|
212
|
+
const result = insertColumn(testData, "newColumn", 3);
|
|
192
213
|
|
|
193
214
|
expect(result).toEqual([
|
|
194
215
|
{
|
|
@@ -211,7 +232,7 @@ describe("insertColumn", () => {
|
|
|
211
232
|
});
|
|
212
233
|
|
|
213
234
|
it("should insert column at the end when index equals length", () => {
|
|
214
|
-
const result = insertColumn(testData, "newColumn");
|
|
235
|
+
const result = insertColumn(testData, "newColumn", 4);
|
|
215
236
|
|
|
216
237
|
expect(result).toEqual([
|
|
217
238
|
{
|
|
@@ -234,7 +255,7 @@ describe("insertColumn", () => {
|
|
|
234
255
|
});
|
|
235
256
|
|
|
236
257
|
it("should handle inserting at index beyond array length", () => {
|
|
237
|
-
const result = insertColumn(testData, "newColumn");
|
|
258
|
+
const result = insertColumn(testData, "newColumn", 999);
|
|
238
259
|
// Should add the column at the end
|
|
239
260
|
expect(result).toEqual([
|
|
240
261
|
{
|
|
@@ -270,14 +291,12 @@ describe("insertColumn", () => {
|
|
|
270
291
|
|
|
271
292
|
it("should handle array with single object", () => {
|
|
272
293
|
const singleRowData = [{ a: 1, b: 2, c: 3 }];
|
|
273
|
-
const result = insertColumn(singleRowData, "newColumn");
|
|
294
|
+
const result = insertColumn(singleRowData, "newColumn", 1);
|
|
274
295
|
expect(result).toEqual([{ a: 1, newColumn: "", b: 2, c: 3 }]);
|
|
275
296
|
});
|
|
276
297
|
|
|
277
298
|
it("should handle objects with different property counts", () => {
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
const result = insertColumn(mixedData, "newColumn");
|
|
299
|
+
const result = insertColumn(mixedData, "newColumn", 1);
|
|
281
300
|
|
|
282
301
|
expect(result).toEqual([
|
|
283
302
|
{ a: 1, newColumn: "", b: 2 },
|
|
@@ -287,12 +306,7 @@ describe("insertColumn", () => {
|
|
|
287
306
|
});
|
|
288
307
|
|
|
289
308
|
it("should handle null and undefined values in data", () => {
|
|
290
|
-
const
|
|
291
|
-
{ a: 1, b: null, c: undefined },
|
|
292
|
-
{ a: null, b: 2, c: 3 },
|
|
293
|
-
];
|
|
294
|
-
|
|
295
|
-
const result = insertColumn(dataWithNulls, "newColumn");
|
|
309
|
+
const result = insertColumn(dataWithNulls, "newColumn", 1);
|
|
296
310
|
|
|
297
311
|
expect(result).toEqual([
|
|
298
312
|
{ a: 1, newColumn: "", b: null, c: undefined },
|
|
@@ -301,7 +315,7 @@ describe("insertColumn", () => {
|
|
|
301
315
|
});
|
|
302
316
|
|
|
303
317
|
it("should handle special characters in column name", () => {
|
|
304
|
-
const result = insertColumn(testData, "new-column_with_123");
|
|
318
|
+
const result = insertColumn(testData, "new-column_with_123", 1);
|
|
305
319
|
|
|
306
320
|
expect(result).toEqual([
|
|
307
321
|
{
|
|
@@ -331,13 +345,6 @@ describe("insertColumn", () => {
|
|
|
331
345
|
});
|
|
332
346
|
|
|
333
347
|
describe("renameColumn", () => {
|
|
334
|
-
const testData = [
|
|
335
|
-
{ int: 1, string: "a", bool: "True", datetime: "2025-07-12 00:07:13" },
|
|
336
|
-
{ int: 2, string: "b", bool: "False", datetime: null },
|
|
337
|
-
{ int: 3, string: "c", bool: null, datetime: "2025-07-12 00:07:13" },
|
|
338
|
-
{ int: 0, string: "", bool: "", datetime: "" },
|
|
339
|
-
];
|
|
340
|
-
|
|
341
348
|
it("should rename a column successfully", () => {
|
|
342
349
|
const result = renameColumn(testData, "int", "number");
|
|
343
350
|
|
|
@@ -373,12 +380,7 @@ describe("renameColumn", () => {
|
|
|
373
380
|
it("should handle renaming to an existing column name", () => {
|
|
374
381
|
const result = renameColumn(testData, "int", "string");
|
|
375
382
|
|
|
376
|
-
expect(result).toEqual(
|
|
377
|
-
{ string: 1, bool: "True", datetime: "2025-07-12 00:07:13" },
|
|
378
|
-
{ string: 2, bool: "False", datetime: null },
|
|
379
|
-
{ string: 3, bool: null, datetime: "2025-07-12 00:07:13" },
|
|
380
|
-
{ string: 0, bool: "", datetime: "" },
|
|
381
|
-
]);
|
|
383
|
+
expect(result).toEqual(testData);
|
|
382
384
|
});
|
|
383
385
|
|
|
384
386
|
it("should handle non-existent column name gracefully", () => {
|
|
@@ -421,8 +423,6 @@ describe("renameColumn", () => {
|
|
|
421
423
|
});
|
|
422
424
|
|
|
423
425
|
it("should handle objects with different property counts", () => {
|
|
424
|
-
const mixedData = [{ a: 1, b: 2 }, { a: 3, b: 4, c: 5 }, { a: 6 }];
|
|
425
|
-
|
|
426
426
|
const result = renameColumn(mixedData, "a", "alpha");
|
|
427
427
|
|
|
428
428
|
expect(result).toEqual([
|
|
@@ -433,11 +433,6 @@ describe("renameColumn", () => {
|
|
|
433
433
|
});
|
|
434
434
|
|
|
435
435
|
it("should handle null and undefined values in data", () => {
|
|
436
|
-
const dataWithNulls = [
|
|
437
|
-
{ a: 1, b: null, c: undefined },
|
|
438
|
-
{ a: null, b: 2, c: 3 },
|
|
439
|
-
];
|
|
440
|
-
|
|
441
436
|
const result = renameColumn(dataWithNulls, "b", "beta");
|
|
442
437
|
|
|
443
438
|
expect(result).toEqual([
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import type { FieldTypes } from "@/components/data-table/types";
|
|
5
|
+
import { applyEditorEdits } from "../editor-state";
|
|
6
|
+
|
|
7
|
+
describe("applyEditorEdits", () => {
|
|
8
|
+
it("replays edits against an empty table", () => {
|
|
9
|
+
const result = applyEditorEdits(
|
|
10
|
+
{ data: [], columnFields: new Map([["value", "unknown"]]) },
|
|
11
|
+
[{ rowIdx: 0, columnId: "value", value: "first" }],
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
expect(result.data).toEqual([{ value: "first" }]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("replays an append after deleting every row", () => {
|
|
18
|
+
const result = applyEditorEdits(
|
|
19
|
+
{
|
|
20
|
+
data: [{ A: 1, B: "a" }],
|
|
21
|
+
columnFields: new Map([
|
|
22
|
+
["A", "number"],
|
|
23
|
+
["B", "string"],
|
|
24
|
+
]) as FieldTypes,
|
|
25
|
+
},
|
|
26
|
+
[
|
|
27
|
+
{ rowIdx: 0, type: "remove" },
|
|
28
|
+
{ rowIdx: 0, columnId: "A", value: 2 },
|
|
29
|
+
{ rowIdx: 0, columnId: "B", value: "b" },
|
|
30
|
+
],
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(result.data).toEqual([{ A: 2, B: "b" }]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("uses the evolving column order", () => {
|
|
37
|
+
const result = applyEditorEdits(
|
|
38
|
+
{
|
|
39
|
+
data: [{ A: "a", B: "b" }],
|
|
40
|
+
columnFields: new Map([
|
|
41
|
+
["A", "string"],
|
|
42
|
+
["B", "string"],
|
|
43
|
+
]) as FieldTypes,
|
|
44
|
+
},
|
|
45
|
+
[
|
|
46
|
+
{ columnIdx: 0, type: "remove" },
|
|
47
|
+
{ columnIdx: 0, type: "rename", newName: "D" },
|
|
48
|
+
],
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(result.data).toEqual([{ D: "b" }]);
|
|
52
|
+
expect([...result.columnFields]).toEqual([["D", "string"]]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("preserves inserted column order through later edits", () => {
|
|
56
|
+
const result = applyEditorEdits(
|
|
57
|
+
{
|
|
58
|
+
data: [{ A: "a", C: "c" }],
|
|
59
|
+
columnFields: new Map([
|
|
60
|
+
["A", "string"],
|
|
61
|
+
["C", "string"],
|
|
62
|
+
]) as FieldTypes,
|
|
63
|
+
},
|
|
64
|
+
[
|
|
65
|
+
{
|
|
66
|
+
columnIdx: 1,
|
|
67
|
+
type: "insert",
|
|
68
|
+
newName: "B",
|
|
69
|
+
dataType: "boolean",
|
|
70
|
+
},
|
|
71
|
+
{ columnIdx: 1, type: "rename", newName: "D" },
|
|
72
|
+
{ columnIdx: 2, type: "remove" },
|
|
73
|
+
],
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(result.data).toEqual([{ A: "a", D: "" }]);
|
|
77
|
+
expect(Object.keys(result.data[0])).toEqual(["A", "D"]);
|
|
78
|
+
expect([...result.columnFields]).toEqual([
|
|
79
|
+
["A", "string"],
|
|
80
|
+
["D", "boolean"],
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("does not mutate the previous state", () => {
|
|
85
|
+
const state = {
|
|
86
|
+
data: [{ A: "before" }],
|
|
87
|
+
columnFields: new Map([["A", "string"]]) as FieldTypes,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const result = applyEditorEdits(state, [
|
|
91
|
+
{ rowIdx: 0, columnId: "A", value: "after" },
|
|
92
|
+
{ columnIdx: 1, type: "insert", newName: "B" },
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
expect(state.data).toEqual([{ A: "before" }]);
|
|
96
|
+
expect([...state.columnFields]).toEqual([["A", "string"]]);
|
|
97
|
+
expect(result.data).toEqual([{ A: "after", B: "" }]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("ignores negative row indices", () => {
|
|
101
|
+
const state = {
|
|
102
|
+
data: [{ A: "unchanged" }],
|
|
103
|
+
columnFields: new Map([["A", "string"]]) as FieldTypes,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
expect(
|
|
107
|
+
applyEditorEdits(state, [
|
|
108
|
+
{ rowIdx: -1, columnId: "A", value: "invalid" },
|
|
109
|
+
{ rowIdx: -1, type: "remove" },
|
|
110
|
+
]).data,
|
|
111
|
+
).toEqual(state.data);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -6,18 +6,31 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
6
6
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
7
7
|
import { GlideDataEditor } from "../glide-data-editor";
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const capturedDataEditor = vi.hoisted(() => ({
|
|
10
10
|
ref: undefined as React.RefObject<HTMLElement> | undefined,
|
|
11
|
+
onCellEdited: undefined as
|
|
12
|
+
| ((cell: [number, number], value: { data: unknown }) => void)
|
|
13
|
+
| undefined,
|
|
14
|
+
onRowAppended: undefined as (() => void) | undefined,
|
|
11
15
|
}));
|
|
12
16
|
|
|
13
17
|
vi.mock("@glideapps/glide-data-grid", async () => {
|
|
14
18
|
const React = await import("react");
|
|
15
19
|
return {
|
|
16
20
|
default: React.forwardRef(function MockDataEditor(
|
|
17
|
-
props: {
|
|
21
|
+
props: {
|
|
22
|
+
portalElementRef?: React.RefObject<HTMLElement>;
|
|
23
|
+
onCellEdited?: (
|
|
24
|
+
cell: [number, number],
|
|
25
|
+
value: { data: unknown },
|
|
26
|
+
) => void;
|
|
27
|
+
onRowAppended?: () => void;
|
|
28
|
+
},
|
|
18
29
|
_ref: React.Ref<HTMLDivElement>,
|
|
19
30
|
) {
|
|
20
|
-
|
|
31
|
+
capturedDataEditor.ref = props.portalElementRef;
|
|
32
|
+
capturedDataEditor.onCellEdited = props.onCellEdited;
|
|
33
|
+
capturedDataEditor.onRowAppended = props.onRowAppended;
|
|
21
34
|
return <div data-testid="mock-data-editor" />;
|
|
22
35
|
}),
|
|
23
36
|
CompactSelection: {
|
|
@@ -40,23 +53,16 @@ vi.mock("@/theme/useTheme", () => ({
|
|
|
40
53
|
|
|
41
54
|
const editorProps = {
|
|
42
55
|
data: [{ name: "alice" }],
|
|
43
|
-
setData: vi.fn(),
|
|
44
56
|
columnFields: new Map([["name", "string"]]) as Map<string, "string">,
|
|
45
|
-
setColumnFields: vi.fn(),
|
|
46
57
|
editableColumns: "all" as const,
|
|
47
|
-
edits: [],
|
|
48
58
|
onAddEdits: vi.fn(),
|
|
49
|
-
onAddRows: vi.fn(),
|
|
50
|
-
onDeleteRows: vi.fn(),
|
|
51
|
-
onRenameColumn: vi.fn(),
|
|
52
|
-
onDeleteColumn: vi.fn(),
|
|
53
|
-
onAddColumn: vi.fn(),
|
|
54
59
|
};
|
|
55
60
|
|
|
56
61
|
describe("GlideDataEditor portal", () => {
|
|
57
62
|
let fullscreenElement: Element | null;
|
|
58
63
|
|
|
59
64
|
beforeEach(() => {
|
|
65
|
+
vi.clearAllMocks();
|
|
60
66
|
fullscreenElement = null;
|
|
61
67
|
Object.defineProperty(document, "fullscreenElement", {
|
|
62
68
|
get: () => fullscreenElement,
|
|
@@ -79,12 +85,42 @@ describe("GlideDataEditor portal", () => {
|
|
|
79
85
|
const portal = screen.getByTestId("glide-data-editor-portal");
|
|
80
86
|
expect(portal.parentElement).toBe(document.body);
|
|
81
87
|
expect(container.contains(portal)).toBe(false);
|
|
82
|
-
expect(
|
|
88
|
+
expect(capturedDataEditor.ref).toBeDefined();
|
|
83
89
|
await waitFor(() => {
|
|
84
|
-
expect(
|
|
90
|
+
expect(capturedDataEditor.ref?.current).toBe(portal);
|
|
85
91
|
});
|
|
86
92
|
});
|
|
87
93
|
|
|
94
|
+
it("emits cell edits", () => {
|
|
95
|
+
const onAddEdits = vi.fn();
|
|
96
|
+
render(
|
|
97
|
+
<TooltipProvider>
|
|
98
|
+
<GlideDataEditor {...editorProps} onAddEdits={onAddEdits} />
|
|
99
|
+
</TooltipProvider>,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
act(() => capturedDataEditor.onCellEdited?.([0, 0], { data: "bob" }));
|
|
103
|
+
|
|
104
|
+
expect(onAddEdits).toHaveBeenCalledWith([
|
|
105
|
+
{ rowIdx: 0, columnId: "name", value: "bob" },
|
|
106
|
+
]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("emits one positional edit per cell when appending a row", () => {
|
|
110
|
+
const onAddEdits = vi.fn();
|
|
111
|
+
render(
|
|
112
|
+
<TooltipProvider>
|
|
113
|
+
<GlideDataEditor {...editorProps} onAddEdits={onAddEdits} />
|
|
114
|
+
</TooltipProvider>,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
act(() => capturedDataEditor.onRowAppended?.());
|
|
118
|
+
|
|
119
|
+
expect(onAddEdits).toHaveBeenCalledWith([
|
|
120
|
+
{ rowIdx: 1, columnId: "name", value: "" },
|
|
121
|
+
]);
|
|
122
|
+
});
|
|
123
|
+
|
|
88
124
|
it("mounts into the fullscreen element when fullscreen is already active", () => {
|
|
89
125
|
const fullscreenContainer = document.createElement("div");
|
|
90
126
|
document.body.appendChild(fullscreenContainer);
|