@marimo-team/islands 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/{chat-ui-DM12Y9is.js → chat-ui-Dcb8pQhM.js} +8 -8
- package/dist/click-outside-container-D_URcm3D.js +167 -0
- package/dist/{common-DuuFqqGa.js → common-C8LWIius.js} +571 -571
- package/dist/{data-grid-overlay-editor-D-p0q7YM.js → data-grid-overlay-editor-DZM4BAaI.js} +49 -48
- package/dist/{click-outside-container-CVJZE0x6.js → data-grid-types-BnZwWG8z.js} +150 -312
- package/dist/{dropdown-menu-C8N-kZzD.js → dropdown-menu-B39I9ARA.js} +20 -31
- package/dist/{glide-data-editor-64-L_DON.js → glide-data-editor-DEJqXbly.js} +2598 -2780
- package/dist/glide-utils-C0dRuWI_.js +201 -0
- package/dist/{html-to-image-B7RQgQQy.js → html-to-image-CvCnIqTI.js} +4476 -4465
- package/dist/main.js +5971 -5913
- package/dist/{process-output-BCqcNNll.js → process-output-C2joqP7d.js} +1 -1
- package/dist/{reveal-component-BDQwrD89.js → reveal-component-CdEcs6WV.js} +3 -3
- 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/types-eyYyHvz7.js +0 -72
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { O as e, et as t, tt as n } from "./html-to-image-
|
|
1
|
+
import { O as e, et as t, tt as n } from "./html-to-image-CvCnIqTI.js";
|
|
2
2
|
import { u as r } from "./dist-wdOJ5T7-.js";
|
|
3
3
|
import { b as i } from "./select-DQ9jU_B_.js";
|
|
4
4
|
import { n as a } from "./semaphore-89jsoHtS.js";
|
|
@@ -2,11 +2,11 @@ import { a as e } from "./rolldown-runtime-Cb76FRpK.js";
|
|
|
2
2
|
import { c as t, g as n, h as r, m as i, t as a } from "./button-DlqUC8W-.js";
|
|
3
3
|
import { t as o } from "./react-DMWcHin-.js";
|
|
4
4
|
import { t as s } from "./compiler-runtime-CAH9RLw2.js";
|
|
5
|
-
import { it as c, wt as l } from "./html-to-image-
|
|
5
|
+
import { it as c, wt as l } from "./html-to-image-CvCnIqTI.js";
|
|
6
6
|
import { u } from "./dist-wdOJ5T7-.js";
|
|
7
|
-
import { J as d, X as f, Y as p, in as m, l as h, n as g, rn as _, sn as v, t as y, u as b } from "./common-
|
|
7
|
+
import { J as d, X as f, Y as p, in as m, l as h, n as g, rn as _, sn as v, t as y, u as b } from "./common-C8LWIius.js";
|
|
8
8
|
import { R as x } from "./select-DQ9jU_B_.js";
|
|
9
|
-
import {
|
|
9
|
+
import { Y as S } from "./dropdown-menu-B39I9ARA.js";
|
|
10
10
|
import { t as C } from "./jsx-runtime-CpoKNRgu.js";
|
|
11
11
|
import { Tc as w } from "./schemas-JTyiKlcH.js";
|
|
12
12
|
import { t as T } from "./tooltip-7p_32zdw.js";
|
package/package.json
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import glideCss from "@glideapps/glide-data-grid/dist/index.css?inline";
|
|
4
|
-
import React, { useState } from "react";
|
|
4
|
+
import React, { useEffect, useState } from "react";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { inferFieldTypes } from "@/components/data-table/columns";
|
|
7
7
|
import { LoadingTable } from "@/components/data-table/loading-table";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
type FieldTypesWithExternalType,
|
|
10
|
+
toFieldTypes,
|
|
11
|
+
} from "@/components/data-table/types";
|
|
9
12
|
import { Alert, AlertTitle } from "@/components/ui/alert";
|
|
10
13
|
import { DelayMount } from "@/components/utils/delay-mount";
|
|
11
14
|
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
12
15
|
import { createPlugin } from "../core/builder";
|
|
13
16
|
import type { Setter } from "../types";
|
|
14
17
|
import { columnToFieldTypesSchema } from "./data-frames/schema";
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
type Edits,
|
|
19
|
-
} from "./data-editor/types";
|
|
18
|
+
import { orderColumnFields } from "./data-editor/data-utils";
|
|
19
|
+
import { applyEditorEdits } from "./data-editor/editor-state";
|
|
20
|
+
import type { EditorRow, EditorState, Edits } from "./data-editor/types";
|
|
20
21
|
import { vegaLoadData } from "./vega/loader";
|
|
21
22
|
import { getVegaFieldTypes } from "./vega/utils";
|
|
22
23
|
|
|
23
24
|
type CsvURL = string;
|
|
24
|
-
type TableData
|
|
25
|
+
type TableData = EditorRow[] | CsvURL;
|
|
25
26
|
|
|
26
27
|
// Lazy load the data editor since it brings in glide-data-grid
|
|
27
28
|
const LazyDataEditor = React.lazy(
|
|
@@ -45,6 +46,7 @@ export const DataEditorPlugin = createPlugin<Edits>("marimo-data-editor", {
|
|
|
45
46
|
label: z.string().nullable(),
|
|
46
47
|
data: z.union([z.string(), z.array(z.object({}).passthrough())]),
|
|
47
48
|
fieldTypes: columnToFieldTypesSchema.nullish(),
|
|
49
|
+
columnNames: z.array(z.string()).default([]),
|
|
48
50
|
editableColumns: z.union([z.array(z.string()), z.literal("all")]),
|
|
49
51
|
columnSizingMode: z.enum(["auto", "fit"]).default("auto"), // TODO: Remove this
|
|
50
52
|
}),
|
|
@@ -55,31 +57,28 @@ export const DataEditorPlugin = createPlugin<Edits>("marimo-data-editor", {
|
|
|
55
57
|
<LoadingDataEditor
|
|
56
58
|
data={props.data.data}
|
|
57
59
|
fieldTypes={props.data.fieldTypes}
|
|
60
|
+
columnNames={props.data.columnNames}
|
|
58
61
|
edits={props.value}
|
|
59
62
|
onEdits={props.setValue}
|
|
60
|
-
host={props.host}
|
|
61
63
|
editableColumns={props.data.editableColumns}
|
|
62
64
|
/>
|
|
63
65
|
);
|
|
64
66
|
});
|
|
65
67
|
|
|
66
|
-
interface Props
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
> {
|
|
70
|
-
data: TableData<object>;
|
|
68
|
+
interface Props {
|
|
69
|
+
data: TableData;
|
|
70
|
+
fieldTypes: FieldTypesWithExternalType | null | undefined;
|
|
71
71
|
edits: Edits;
|
|
72
72
|
onEdits: Setter<Edits>;
|
|
73
|
-
host: HTMLElement;
|
|
74
73
|
editableColumns: string[] | "all";
|
|
74
|
+
columnNames: string[];
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
const LoadingDataEditor = (props: Props) => {
|
|
78
|
-
const [
|
|
79
|
-
const [columnFields, setColumnFields] = useState<FieldTypes>(new Map());
|
|
78
|
+
const [editorState, setEditorState] = useState<EditorState | null>(null);
|
|
80
79
|
|
|
81
80
|
// Load the data
|
|
82
|
-
const { error } = useAsyncData(async () => {
|
|
81
|
+
const { data: loadedState, error } = useAsyncData(async () => {
|
|
83
82
|
const withoutExternalTypes = toFieldTypes(props.fieldTypes ?? []);
|
|
84
83
|
|
|
85
84
|
// If we already have the data, return it
|
|
@@ -87,7 +86,7 @@ const LoadingDataEditor = (props: Props) => {
|
|
|
87
86
|
// plain `Record`; column order doesn't matter for parsing.
|
|
88
87
|
const localData = Array.isArray(props.data)
|
|
89
88
|
? props.data
|
|
90
|
-
: await vegaLoadData(
|
|
89
|
+
: await vegaLoadData<EditorRow>(
|
|
91
90
|
props.data,
|
|
92
91
|
{
|
|
93
92
|
type: "csv",
|
|
@@ -96,11 +95,20 @@ const LoadingDataEditor = (props: Props) => {
|
|
|
96
95
|
{ handleBigIntAndNumberLike: true },
|
|
97
96
|
);
|
|
98
97
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
98
|
+
return {
|
|
99
|
+
data: localData,
|
|
100
|
+
columnFields: orderColumnFields(
|
|
101
|
+
toFieldTypes(props.fieldTypes ?? inferFieldTypes(localData)),
|
|
102
|
+
props.columnNames,
|
|
103
|
+
),
|
|
104
|
+
} satisfies EditorState;
|
|
105
|
+
}, [props.fieldTypes, props.columnNames, props.data]);
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (loadedState !== undefined) {
|
|
109
|
+
setEditorState(applyEditorEdits(loadedState, props.edits.edits));
|
|
110
|
+
}
|
|
111
|
+
}, [loadedState, props.edits.edits]);
|
|
104
112
|
|
|
105
113
|
if (error) {
|
|
106
114
|
return (
|
|
@@ -113,7 +121,7 @@ const LoadingDataEditor = (props: Props) => {
|
|
|
113
121
|
);
|
|
114
122
|
}
|
|
115
123
|
|
|
116
|
-
if (
|
|
124
|
+
if (editorState === null) {
|
|
117
125
|
return (
|
|
118
126
|
<DelayMount milliseconds={200}>
|
|
119
127
|
<LoadingTable pageSize={10} />
|
|
@@ -123,54 +131,14 @@ const LoadingDataEditor = (props: Props) => {
|
|
|
123
131
|
|
|
124
132
|
return (
|
|
125
133
|
<LazyDataEditor
|
|
126
|
-
data={data}
|
|
127
|
-
|
|
128
|
-
columnFields={columnFields}
|
|
129
|
-
setColumnFields={setColumnFields}
|
|
134
|
+
data={editorState.data}
|
|
135
|
+
columnFields={editorState.columnFields}
|
|
130
136
|
editableColumns={props.editableColumns}
|
|
131
|
-
edits={props.edits.edits}
|
|
132
137
|
onAddEdits={(edits) => {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
onAddRows={(rows) => {
|
|
136
|
-
const newEdits = rows.flatMap((row, rowIndex) =>
|
|
137
|
-
Object.entries(row).map(([columnId, value]) => ({
|
|
138
|
-
rowIdx: data.length + rowIndex,
|
|
139
|
-
columnId,
|
|
140
|
-
value,
|
|
141
|
-
})),
|
|
138
|
+
setEditorState((state) =>
|
|
139
|
+
state === null ? null : applyEditorEdits(state, edits),
|
|
142
140
|
);
|
|
143
|
-
props.onEdits((v) => ({ ...v, edits: [...v.edits, ...
|
|
144
|
-
}}
|
|
145
|
-
onDeleteRows={(rowIndexes) => {
|
|
146
|
-
props.onEdits((v) => {
|
|
147
|
-
const newEdits = rowIndexes.map((rowIdx, index) => ({
|
|
148
|
-
rowIdx: rowIdx - index,
|
|
149
|
-
type: BulkEdit.Remove,
|
|
150
|
-
}));
|
|
151
|
-
return {
|
|
152
|
-
...v,
|
|
153
|
-
edits: [...v.edits, ...newEdits],
|
|
154
|
-
};
|
|
155
|
-
});
|
|
156
|
-
}}
|
|
157
|
-
onRenameColumn={(columnIdx: number, newName: string) => {
|
|
158
|
-
props.onEdits((v) => ({
|
|
159
|
-
...v,
|
|
160
|
-
edits: [...v.edits, { columnIdx, newName, type: BulkEdit.Rename }],
|
|
161
|
-
}));
|
|
162
|
-
}}
|
|
163
|
-
onDeleteColumn={(columnIdx: number) => {
|
|
164
|
-
props.onEdits((v) => ({
|
|
165
|
-
...v,
|
|
166
|
-
edits: [...v.edits, { columnIdx, type: BulkEdit.Remove }],
|
|
167
|
-
}));
|
|
168
|
-
}}
|
|
169
|
-
onAddColumn={(columnIdx: number, newName: string) => {
|
|
170
|
-
props.onEdits((v) => ({
|
|
171
|
-
...v,
|
|
172
|
-
edits: [...v.edits, { columnIdx, newName, type: BulkEdit.Insert }],
|
|
173
|
-
}));
|
|
141
|
+
props.onEdits((v) => ({ ...v, edits: [...v.edits, ...edits] }));
|
|
174
142
|
}}
|
|
175
143
|
/>
|
|
176
144
|
);
|
|
@@ -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([
|