@marimo-team/frontend 0.23.9-dev3 → 0.23.9-dev4
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/{ConnectedDataExplorerComponent-D-boDGAP.js → ConnectedDataExplorerComponent-B-cElX-s.js} +1 -1
- package/dist/assets/{ImperativeModal-DEC1mXgV.js → ImperativeModal-BeQmePpG.js} +1 -1
- package/dist/assets/JsonOutput-BbkJ4c02.js +53 -0
- package/dist/assets/{MarimoErrorOutput-XWqnhvJ6.js → MarimoErrorOutput-ciRhjJM2.js} +1 -1
- package/dist/assets/{RSPContexts-Bk1r00gJ.js → RSPContexts-Clr6RnG2.js} +1 -1
- package/dist/assets/{RunButton-Dbak5hfa.js → RunButton-DySeQPd2.js} +1 -1
- package/dist/assets/{add-cell-with-ai-BbZkMqv2.js → add-cell-with-ai-DKUeTe86.js} +1 -1
- package/dist/assets/{add-connection-dialog-CzxRpS5F.js → add-connection-dialog-DwDtRq10.js} +1 -1
- package/dist/assets/{agent-panel-zPhlhkYL.js → agent-panel-C8Oqvdr5.js} +1 -1
- package/dist/assets/{ai-model-dropdown-CjhUqXgj.js → ai-model-dropdown-Cv4r0gqY.js} +1 -1
- package/dist/assets/{app-config-button-CCs8Jepz.js → app-config-button-rS9K4BOr.js} +1 -1
- package/dist/assets/{cache-panel-VL13fWgF.js → cache-panel-BSlEbX4w.js} +1 -1
- package/dist/assets/{cell-editor-ODyJXDT8.js → cell-editor-DnLtTvEM.js} +3 -3
- package/dist/assets/{cell-link-PQYiMZw1.js → cell-link-f3ZU0MzW.js} +1 -1
- package/dist/assets/{chat-display-DetTBnqK.js → chat-display-B7QE6C2q.js} +1 -1
- package/dist/assets/{chat-panel-CEgw_vg0.js → chat-panel-D11B2Q4F.js} +1 -1
- package/dist/assets/{chat-ui-D-Y7p_cT.js → chat-ui-BwVhfsq2.js} +1 -1
- package/dist/assets/{chunk-5FQGJX7Z-BSzccEgu.js → chunk-5FQGJX7Z-CbuGydc8.js} +3 -3
- package/dist/assets/{code-block-37QAKDTI-U2R1jyOo.js → code-block-37QAKDTI-BwYRrSJW.js} +1 -1
- package/dist/assets/{column-preview-BLIWbdOX.js → column-preview-8cXBINr1.js} +1 -1
- package/dist/assets/command-DUeag2QH.js +1 -0
- package/dist/assets/{command-palette-CeDe63_W.js → command-palette-BlX5QV4R.js} +1 -1
- package/dist/assets/{common-BaBE_ygg.js → common-DAjN54-N.js} +1 -1
- package/dist/assets/dates-DS_7IZoI.js +1 -0
- package/dist/assets/{dependency-graph-panel-ClI5byUa.js → dependency-graph-panel-C7UdKk5P.js} +1 -1
- package/dist/assets/{dist-CW3rweKM.js → dist-DFHp_ZJR.js} +1 -1
- package/dist/assets/{download-B1QFVDP-.js → download-eMMt69Hd.js} +1 -1
- package/dist/assets/{edit-page-ZFpn8-WM.js → edit-page-DoGaZtlD.js} +6 -6
- package/dist/assets/{error-panel-iXznkJZ1.js → error-panel-CkKXrRna.js} +1 -1
- package/dist/assets/{field-DNlzfMKW.js → field-B4CdIHa9.js} +1 -1
- package/dist/assets/{file-explorer-panel-BVBKF1SH.js → file-explorer-panel-BA0aI_q7.js} +3 -3
- package/dist/assets/{file-name-input-g2H2sY2h.js → file-name-input-BpDnZMOs.js} +1 -1
- package/dist/assets/{form-BjUJP6PJ.js → form-z4S7B_rP.js} +1 -1
- package/dist/assets/{gallery-page-MrZHjySE.js → gallery-page-BXZHpNqZ.js} +1 -1
- package/dist/assets/{glide-data-editor-4Wql6uq7.js → glide-data-editor-C5d_9QYv.js} +1 -1
- package/dist/assets/{home-page-De1W6q6f.js → home-page-Cv2KTtQj.js} +1 -1
- package/dist/assets/{hooks-jWLD3t7P.js → hooks-DeuLZEyD.js} +1 -1
- package/dist/assets/{index-ZA7t2ThT.js → index-B5z1LmJ1.js} +19 -19
- package/dist/assets/index-Cn0RBoFD.css +2 -0
- package/dist/assets/{input-CVE-gIjt.js → input-TSilD7AA.js} +1 -1
- package/dist/assets/{layout-DEU6lX-9.js → layout-Dpf6l72t.js} +3 -3
- package/dist/assets/{logs-panel-BMAfoMJg.js → logs-panel-D6VqVLlw.js} +1 -1
- package/dist/assets/{markdown-renderer-BQ-BQLiJ.js → markdown-renderer-l3qZKrTC.js} +3 -3
- package/dist/assets/mermaid-4DMBBIKO-BlSTFoRU.js +1 -0
- package/dist/assets/{mermaid-CZhfODkT.js → mermaid-BpEU2haB.js} +1 -1
- package/dist/assets/{name-cell-input-bwfAyC0i.js → name-cell-input-BmJb1jcI.js} +1 -1
- package/dist/assets/{packages-panel-B3dRYuRM.js → packages-panel-BQJiodre.js} +1 -1
- package/dist/assets/{panels-DWhhEgv4.js → panels-EIGgrBT7.js} +1 -1
- package/dist/assets/{radio-group-rsi1ibXY.js → radio-group-BE0Xe9G9.js} +1 -1
- package/dist/assets/{readonly-python-code-BKYj8PNf.js → readonly-python-code-D3NHaVFs.js} +1 -1
- package/dist/assets/{reveal-component-DNpBzX6F.js → reveal-component-Cy-Nz7JR.js} +1 -1
- package/dist/assets/{run-page-CO2X6wso.js → run-page-IrSzOAx3.js} +1 -1
- package/dist/assets/{scratchpad-panel-CWfddArs.js → scratchpad-panel-DVSB-2ZU.js} +1 -1
- package/dist/assets/{secrets-panel-DqHGq3V8.js → secrets-panel-EMyfZ0xi.js} +1 -1
- package/dist/assets/{session-panel-BP0QxaoM.js → session-panel-BSP3VdpL.js} +1 -1
- package/dist/assets/{snippets-panel-DFJd1ui5.js → snippets-panel-WZ7ZOs2t.js} +1 -1
- package/dist/assets/{state-dx303w7J.js → state-CcAGAozT.js} +1 -1
- package/dist/assets/{state-BXNNuw9g.js → state-KI8ENp1g.js} +1 -1
- package/dist/assets/{tracing-BQU8fBDM.js → tracing-CXaM8i7_.js} +1 -1
- package/dist/assets/{tracing-panel-DEVpyGX3.js → tracing-panel-DcR9ni3B.js} +2 -2
- package/dist/assets/{useCellActionButton-QaDO24oW.js → useCellActionButton-p6Ij9yyu.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-BB_XeSAg.js → useDependencyPanelTab-KDzMTAUI.js} +1 -1
- package/dist/assets/{useNotebookActions-CJEicFed.js → useNotebookActions-COhd0Ld9.js} +1 -1
- package/dist/assets/{vega-component-C9fDGx86.js → vega-component-BGC-ewWV.js} +1 -1
- package/dist/assets/{write-secret-modal-Liv_9MXS.js → write-secret-modal-DcmyNRRr.js} +1 -1
- package/dist/index.html +27 -27
- package/package.json +1 -1
- package/src/components/data-table/__tests__/column-header.test.tsx +110 -277
- package/src/components/data-table/__tests__/date-filter-inputs.test.tsx +33 -0
- package/src/components/data-table/__tests__/filter-pill-editor.test.tsx +75 -38
- package/src/components/data-table/__tests__/filter-pills.test.tsx +287 -0
- package/src/components/data-table/__tests__/filter-test-utils.ts +47 -0
- package/src/components/data-table/__tests__/filters.test.ts +5 -5
- package/src/components/data-table/add-filter-button.tsx +85 -0
- package/src/components/data-table/column-header.tsx +92 -691
- package/src/components/data-table/context-menu.tsx +26 -12
- package/src/components/data-table/data-table.tsx +89 -57
- package/src/components/data-table/date-filter-inputs.tsx +13 -10
- package/src/components/data-table/filter-by-values-picker.tsx +13 -19
- package/src/components/data-table/filter-editor-context.tsx +34 -0
- package/src/components/data-table/filter-pill-editor.tsx +152 -175
- package/src/components/data-table/filter-pills.tsx +190 -153
- package/src/components/data-table/filters/builders.ts +102 -0
- package/src/components/data-table/filters/defaults.ts +31 -0
- package/src/components/data-table/filters/format.ts +131 -0
- package/src/components/data-table/filters/guards.ts +51 -0
- package/src/components/data-table/filters/index.ts +7 -0
- package/src/components/data-table/filters/operators.ts +76 -0
- package/src/components/data-table/filters/serialize.ts +186 -0
- package/src/components/data-table/filters/types.ts +33 -0
- package/src/components/data-table/header-items.tsx +6 -83
- package/src/components/data-table/value-chips.tsx +52 -0
- package/src/components/ui/number-field.tsx +13 -1
- package/src/utils/dates.ts +39 -0
- package/dist/assets/JsonOutput-05-R3eil.js +0 -53
- package/dist/assets/command-2NPJCYDa.js +0 -1
- package/dist/assets/dates-DI1TvEEK.js +0 -1
- package/dist/assets/index-B30qjBZM.css +0 -2
- package/dist/assets/mermaid-4DMBBIKO-C0OyyVdo.js +0 -1
- package/src/components/data-table/__tests__/column-header.test.ts +0 -65
- package/src/components/data-table/filters.ts +0 -386
- /package/dist/assets/{focus-BaOnnMs-.js → focus-BLb-92ed.js} +0 -0
- /package/dist/assets/{formats-BRq458WH.js → formats-DP_z0P-n.js} +0 -0
- /package/dist/assets/{html-to-image-D6SgvARi.js → html-to-image-Ctd6Wpty.js} +0 -0
- /package/dist/assets/{micromark-factory-space-BUQpMdx2.js → micromark-factory-space-bqhKsQDn.js} +0 -0
- /package/dist/assets/{react-resizable-panels.browser.esm-Ce2ksurd.js → react-resizable-panels.browser.esm-BdtIs0E-.js} +0 -0
- /package/dist/assets/{table-DQE9hQzM.js → table-Bgc-inJs.js} +0 -0
- /package/dist/assets/{useAsyncData-C5i0IRVM.js → useAsyncData-Dg8E_bPh.js} +0 -0
|
@@ -1,308 +1,141 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import type { Column } from "@tanstack/react-table";
|
|
3
|
-
import { fireEvent, render, screen
|
|
3
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
4
4
|
import { beforeAll, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
6
|
+
import { DataTableColumnHeader } from "../column-header";
|
|
5
7
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "
|
|
10
|
-
import { Filter } from "../filters";
|
|
8
|
+
type AddFilterRequest,
|
|
9
|
+
FilterEditorProvider,
|
|
10
|
+
} from "../filter-editor-context";
|
|
11
|
+
import { buildFilterTestTable } from "./filter-test-utils";
|
|
11
12
|
|
|
12
13
|
beforeAll(() => {
|
|
13
14
|
global.HTMLElement.prototype.scrollIntoView = () => {
|
|
14
|
-
// jsdom does not implement scrollIntoView
|
|
15
|
+
// jsdom does not implement scrollIntoView
|
|
15
16
|
};
|
|
16
|
-
// Radix Select gates pointer interactions on hasPointerCapture; jsdom omits it.
|
|
17
17
|
if (!global.HTMLElement.prototype.hasPointerCapture) {
|
|
18
18
|
global.HTMLElement.prototype.hasPointerCapture = () => false;
|
|
19
19
|
}
|
|
20
20
|
if (!global.HTMLElement.prototype.releasePointerCapture) {
|
|
21
21
|
global.HTMLElement.prototype.releasePointerCapture = () => {
|
|
22
|
-
//
|
|
22
|
+
// noop for jsdom
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
function
|
|
28
|
-
unknown,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
27
|
+
function renderHeader(opts: {
|
|
28
|
+
column: Column<unknown, unknown>;
|
|
29
|
+
requestAddFilter?: (req: AddFilterRequest) => void;
|
|
30
|
+
withProvider: boolean;
|
|
31
|
+
}) {
|
|
32
|
+
const { column, requestAddFilter, withProvider } = opts;
|
|
33
|
+
const headerNode = (
|
|
34
|
+
<DataTableColumnHeader column={column} header={column.id} />
|
|
35
|
+
);
|
|
36
|
+
return render(
|
|
37
|
+
<TooltipProvider>
|
|
38
|
+
{withProvider ? (
|
|
39
|
+
<FilterEditorProvider
|
|
40
|
+
value={{ requestAddFilter: requestAddFilter ?? vi.fn() }}
|
|
41
|
+
>
|
|
42
|
+
{headerNode}
|
|
43
|
+
</FilterEditorProvider>
|
|
44
|
+
) : (
|
|
45
|
+
headerNode
|
|
46
|
+
)}
|
|
47
|
+
</TooltipProvider>,
|
|
48
|
+
);
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
fireEvent.click(trigger);
|
|
53
|
-
const listbox = screen.getByRole("listbox");
|
|
54
|
-
const labels = within(listbox)
|
|
55
|
-
.getAllByRole("option")
|
|
56
|
-
.map((o) => o.textContent);
|
|
57
|
-
expect(labels).toEqual([
|
|
58
|
-
"Between",
|
|
59
|
-
"Equals",
|
|
60
|
-
"Doesn't equal",
|
|
61
|
-
"Greater than",
|
|
62
|
-
"Greater than or equal",
|
|
63
|
-
"Less than",
|
|
64
|
-
"Less than or equal",
|
|
65
|
-
"Is null",
|
|
66
|
-
"Is not null",
|
|
67
|
-
]);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("between mode disables Apply until both min and max are defined", () => {
|
|
71
|
-
const column = mockColumn();
|
|
72
|
-
render(<NumberFilterMenu column={column} />);
|
|
73
|
-
const apply = screen.getByRole("button", { name: /apply/i });
|
|
74
|
-
expect(apply).toBeDisabled();
|
|
75
|
-
|
|
76
|
-
const min = screen.getByLabelText("min");
|
|
77
|
-
fireEvent.change(min, { target: { value: "1" } });
|
|
78
|
-
fireEvent.blur(min);
|
|
79
|
-
expect(apply).toBeDisabled();
|
|
51
|
+
const openMenu = () => {
|
|
52
|
+
const trigger = screen.getByLabelText("Column options");
|
|
53
|
+
trigger.focus();
|
|
54
|
+
fireEvent.keyDown(trigger, { key: "Enter" });
|
|
55
|
+
};
|
|
80
56
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("comparison mode shows a single value field seeded from current filter", () => {
|
|
88
|
-
const column = mockColumn(Filter.number({ operator: ">", value: 18 }));
|
|
89
|
-
render(<NumberFilterMenu column={column} />);
|
|
90
|
-
const value = screen.getByLabelText("value") as HTMLInputElement;
|
|
91
|
-
expect(value).toBeInTheDocument();
|
|
92
|
-
expect(value.value).toBe("18");
|
|
93
|
-
expect(screen.queryByLabelText("min")).not.toBeInTheDocument();
|
|
94
|
-
expect(screen.queryByLabelText("max")).not.toBeInTheDocument();
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("selecting a nullish operator hides value inputs and commits on Apply", () => {
|
|
98
|
-
const column = mockColumn();
|
|
99
|
-
render(<NumberFilterMenu column={column} />);
|
|
100
|
-
fireEvent.click(screen.getByRole("combobox"));
|
|
101
|
-
const listbox = screen.getByRole("listbox");
|
|
102
|
-
fireEvent.click(within(listbox).getByText("Is null"));
|
|
103
|
-
expect(column.setFilterValue).not.toHaveBeenCalled();
|
|
104
|
-
expect(screen.queryByLabelText("min")).not.toBeInTheDocument();
|
|
105
|
-
expect(screen.queryByLabelText("max")).not.toBeInTheDocument();
|
|
106
|
-
expect(screen.queryByLabelText("value")).not.toBeInTheDocument();
|
|
107
|
-
fireEvent.click(screen.getByRole("button", { name: /apply/i }));
|
|
108
|
-
expect(column.setFilterValue).toHaveBeenCalledWith(
|
|
109
|
-
Filter.number({ operator: "is_null" }),
|
|
110
|
-
);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
function mockTextColumn(initial?: ReturnType<typeof Filter.text>): Column<
|
|
115
|
-
unknown,
|
|
116
|
-
unknown
|
|
117
|
-
> & {
|
|
118
|
-
setFilterValue: ReturnType<typeof vi.fn>;
|
|
119
|
-
} {
|
|
120
|
-
let filterValue = initial;
|
|
121
|
-
const setFilterValue = vi.fn((next) => {
|
|
122
|
-
filterValue = next;
|
|
123
|
-
});
|
|
124
|
-
return {
|
|
125
|
-
id: "name",
|
|
126
|
-
columnDef: { meta: { dataType: "string", filterType: "text" } },
|
|
127
|
-
getFilterValue: () => filterValue,
|
|
128
|
-
setFilterValue,
|
|
129
|
-
} as unknown as Column<unknown, unknown> & {
|
|
130
|
-
setFilterValue: ReturnType<typeof vi.fn>;
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
describe("TextFilterMenu", () => {
|
|
135
|
-
it("shows all 11 text operators in the dropdown", () => {
|
|
136
|
-
const column = mockTextColumn();
|
|
137
|
-
render(<TextFilterMenu column={column} />);
|
|
138
|
-
fireEvent.click(screen.getByRole("combobox"));
|
|
139
|
-
const listbox = screen.getByRole("listbox");
|
|
140
|
-
const labels = within(listbox)
|
|
141
|
-
.getAllByRole("option")
|
|
142
|
-
.map((o) => o.textContent);
|
|
143
|
-
expect(labels).toEqual([
|
|
144
|
-
"Contains",
|
|
145
|
-
"Equals",
|
|
146
|
-
"Doesn't equal",
|
|
147
|
-
"Matches regex",
|
|
148
|
-
"Starts with",
|
|
149
|
-
"Ends with",
|
|
150
|
-
"Is in",
|
|
151
|
-
"Not in",
|
|
152
|
-
"Is empty",
|
|
153
|
-
"Is null",
|
|
154
|
-
"Is not null",
|
|
57
|
+
describe("DataTableColumnHeader — filter menu", () => {
|
|
58
|
+
it("renders Filter and Filter-by-values items for text columns inside a provider", () => {
|
|
59
|
+
const { table } = buildFilterTestTable([
|
|
60
|
+
{ id: "name", filterType: "text" },
|
|
155
61
|
]);
|
|
62
|
+
renderHeader({ column: table.getColumn("name")!, withProvider: true });
|
|
63
|
+
openMenu();
|
|
64
|
+
expect(screen.getByText("Filter")).toBeInTheDocument();
|
|
65
|
+
expect(screen.getByText("Filter by values")).toBeInTheDocument();
|
|
156
66
|
});
|
|
157
67
|
|
|
158
|
-
it("
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
expect(
|
|
165
|
-
expect(
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it("
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
expect(
|
|
194
|
-
|
|
195
|
-
);
|
|
68
|
+
it("renders Filter and Filter-by-values items for number columns", () => {
|
|
69
|
+
const { table } = buildFilterTestTable([
|
|
70
|
+
{ id: "age", filterType: "number" },
|
|
71
|
+
]);
|
|
72
|
+
renderHeader({ column: table.getColumn("age")!, withProvider: true });
|
|
73
|
+
openMenu();
|
|
74
|
+
expect(screen.getByText("Filter")).toBeInTheDocument();
|
|
75
|
+
expect(screen.getByText("Filter by values")).toBeInTheDocument();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it.each(["date", "datetime", "time", "boolean"] as const)(
|
|
79
|
+
"hides Filter-by-values for %s columns",
|
|
80
|
+
(filterType) => {
|
|
81
|
+
const { table } = buildFilterTestTable([{ id: "col", filterType }]);
|
|
82
|
+
renderHeader({ column: table.getColumn("col")!, withProvider: true });
|
|
83
|
+
openMenu();
|
|
84
|
+
expect(screen.getByText("Filter")).toBeInTheDocument();
|
|
85
|
+
expect(screen.queryByText("Filter by values")).not.toBeInTheDocument();
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
it("hides both items when filterType is missing", () => {
|
|
90
|
+
const { table } = buildFilterTestTable([{ id: "opaque" }]);
|
|
91
|
+
renderHeader({ column: table.getColumn("opaque")!, withProvider: true });
|
|
92
|
+
openMenu();
|
|
93
|
+
expect(screen.queryByText("Filter")).not.toBeInTheDocument();
|
|
94
|
+
expect(screen.queryByText("Filter by values")).not.toBeInTheDocument();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("hides both items when no FilterEditorProvider is present", () => {
|
|
98
|
+
const { table } = buildFilterTestTable([
|
|
99
|
+
{ id: "name", filterType: "text" },
|
|
100
|
+
]);
|
|
101
|
+
renderHeader({ column: table.getColumn("name")!, withProvider: false });
|
|
102
|
+
openMenu();
|
|
103
|
+
expect(screen.queryByText("Filter")).not.toBeInTheDocument();
|
|
104
|
+
expect(screen.queryByText("Filter by values")).not.toBeInTheDocument();
|
|
196
105
|
});
|
|
197
106
|
|
|
198
|
-
it("
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
107
|
+
it("invokes requestAddFilter with columnId when Filter is clicked", () => {
|
|
108
|
+
const requestAddFilter = vi.fn();
|
|
109
|
+
const { table } = buildFilterTestTable([
|
|
110
|
+
{ id: "name", filterType: "text" },
|
|
111
|
+
]);
|
|
112
|
+
renderHeader({
|
|
113
|
+
column: table.getColumn("name")!,
|
|
114
|
+
requestAddFilter,
|
|
115
|
+
withProvider: true,
|
|
204
116
|
});
|
|
205
|
-
|
|
117
|
+
openMenu();
|
|
118
|
+
fireEvent.click(screen.getByText("Filter"));
|
|
119
|
+
expect(requestAddFilter).toHaveBeenCalledTimes(1);
|
|
120
|
+
expect(requestAddFilter).toHaveBeenCalledWith({ columnId: "name" });
|
|
206
121
|
});
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
type DateFilterValue = ReturnType<typeof Filter.date>;
|
|
210
122
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
setFilterValue: ReturnType<typeof vi.fn>;
|
|
216
|
-
} {
|
|
217
|
-
let filterValue = initial;
|
|
218
|
-
const setFilterValue = vi.fn((next) => {
|
|
219
|
-
filterValue = next;
|
|
220
|
-
});
|
|
221
|
-
return {
|
|
222
|
-
id: "when",
|
|
223
|
-
columnDef: { meta: { dataType: filterType, filterType } },
|
|
224
|
-
getFilterValue: () => filterValue,
|
|
225
|
-
setFilterValue,
|
|
226
|
-
} as unknown as Column<unknown, unknown> & {
|
|
227
|
-
setFilterValue: ReturnType<typeof vi.fn>;
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
describe("DateFilterMenu", () => {
|
|
232
|
-
it("shows all expected operators in the dropdown", () => {
|
|
233
|
-
const column = mockDateColumn("date");
|
|
234
|
-
render(<DateFilterMenu column={column} filterType="date" />);
|
|
235
|
-
fireEvent.click(screen.getByRole("combobox"));
|
|
236
|
-
const listbox = screen.getByRole("listbox");
|
|
237
|
-
const labels = within(listbox)
|
|
238
|
-
.getAllByRole("option")
|
|
239
|
-
.map((o) => o.textContent);
|
|
240
|
-
expect(labels).toEqual([
|
|
241
|
-
"Between",
|
|
242
|
-
"Equals",
|
|
243
|
-
"Doesn't equal",
|
|
244
|
-
"Greater than",
|
|
245
|
-
"Greater than or equal",
|
|
246
|
-
"Less than",
|
|
247
|
-
"Less than or equal",
|
|
248
|
-
"Is null",
|
|
249
|
-
"Is not null",
|
|
123
|
+
it("invokes requestAddFilter with operator='in' when Filter by values is clicked", () => {
|
|
124
|
+
const requestAddFilter = vi.fn();
|
|
125
|
+
const { table } = buildFilterTestTable([
|
|
126
|
+
{ id: "name", filterType: "text" },
|
|
250
127
|
]);
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
expect(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
"date",
|
|
264
|
-
Filter.date({
|
|
265
|
-
operator: "between",
|
|
266
|
-
min: new Date("2024-01-01T00:00:00Z"),
|
|
267
|
-
max: new Date("2024-06-01T00:00:00Z"),
|
|
268
|
-
}),
|
|
269
|
-
);
|
|
270
|
-
render(<DateFilterMenu column={column} filterType="date" />);
|
|
271
|
-
expect(screen.getByLabelText("range")).toBeInTheDocument();
|
|
272
|
-
expect(screen.getByRole("button", { name: /apply/i })).not.toBeDisabled();
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it("comparison operator swaps range for a single value picker", () => {
|
|
276
|
-
const column = mockDateColumn(
|
|
277
|
-
"date",
|
|
278
|
-
Filter.date({
|
|
279
|
-
operator: ">",
|
|
280
|
-
value: new Date("2024-01-01T00:00:00Z"),
|
|
281
|
-
}),
|
|
282
|
-
);
|
|
283
|
-
render(<DateFilterMenu column={column} filterType="date" />);
|
|
284
|
-
expect(screen.getByLabelText("value")).toBeInTheDocument();
|
|
285
|
-
expect(screen.queryByLabelText("range")).not.toBeInTheDocument();
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it("time filter type renders two TimeFields for between", () => {
|
|
289
|
-
const column = mockDateColumn("time");
|
|
290
|
-
render(<DateFilterMenu column={column} filterType="time" />);
|
|
291
|
-
expect(screen.getByLabelText("min")).toBeInTheDocument();
|
|
292
|
-
expect(screen.getByLabelText("max")).toBeInTheDocument();
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it("selecting a nullish operator hides value inputs and commits on Apply", () => {
|
|
296
|
-
const column = mockDateColumn("date");
|
|
297
|
-
render(<DateFilterMenu column={column} filterType="date" />);
|
|
298
|
-
fireEvent.click(screen.getByRole("combobox"));
|
|
299
|
-
const listbox = screen.getByRole("listbox");
|
|
300
|
-
fireEvent.click(within(listbox).getByText("Is null"));
|
|
301
|
-
expect(screen.queryByLabelText("range")).not.toBeInTheDocument();
|
|
302
|
-
expect(screen.queryByLabelText("value")).not.toBeInTheDocument();
|
|
303
|
-
fireEvent.click(screen.getByRole("button", { name: /apply/i }));
|
|
304
|
-
expect(column.setFilterValue).toHaveBeenCalledWith(
|
|
305
|
-
Filter.date({ operator: "is_null" }),
|
|
306
|
-
);
|
|
128
|
+
renderHeader({
|
|
129
|
+
column: table.getColumn("name")!,
|
|
130
|
+
requestAddFilter,
|
|
131
|
+
withProvider: true,
|
|
132
|
+
});
|
|
133
|
+
openMenu();
|
|
134
|
+
fireEvent.click(screen.getByText("Filter by values"));
|
|
135
|
+
expect(requestAddFilter).toHaveBeenCalledTimes(1);
|
|
136
|
+
expect(requestAddFilter).toHaveBeenCalledWith({
|
|
137
|
+
columnId: "name",
|
|
138
|
+
operator: "in",
|
|
139
|
+
});
|
|
307
140
|
});
|
|
308
141
|
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { dateToLocalISODate } from "@/utils/dates";
|
|
4
|
+
import { parsePastedRange } from "../date-filter-inputs";
|
|
5
|
+
|
|
6
|
+
describe("parsePastedRange", () => {
|
|
7
|
+
it.each([
|
|
8
|
+
["hyphen", "2026-02-01 - 2026-04-01"],
|
|
9
|
+
["en dash", "2026-02-01 – 2026-04-01"],
|
|
10
|
+
["em dash", "2026-02-01 — 2026-04-01"],
|
|
11
|
+
["to", "2026-02-01 to 2026-04-01"],
|
|
12
|
+
["TO (uppercase)", "2026-02-01 TO 2026-04-01"],
|
|
13
|
+
["and", "2026-02-01 and 2026-04-01"],
|
|
14
|
+
["AND (uppercase)", "2026-02-01 AND 2026-04-01"],
|
|
15
|
+
])("splits a date range pasted with %s separator", (_, text) => {
|
|
16
|
+
const result = parsePastedRange("date", text);
|
|
17
|
+
expect(result).toBeDefined();
|
|
18
|
+
expect(result && dateToLocalISODate(result.min)).toBe("2026-02-01");
|
|
19
|
+
expect(result && dateToLocalISODate(result.max)).toBe("2026-04-01");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("returns a degenerate range for a single pasted date", () => {
|
|
23
|
+
const result = parsePastedRange("date", "2026-03-15");
|
|
24
|
+
expect(result).toBeDefined();
|
|
25
|
+
expect(result && dateToLocalISODate(result.min)).toBe("2026-03-15");
|
|
26
|
+
expect(result && dateToLocalISODate(result.max)).toBe("2026-03-15");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns undefined for unparsable input", () => {
|
|
30
|
+
expect(parsePastedRange("date", "Sunday and Monday")).toBeUndefined();
|
|
31
|
+
expect(parsePastedRange("date", "not a date")).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
import type { Column, Table } from "@tanstack/react-table";
|
|
3
2
|
import { fireEvent, render, screen } from "@testing-library/react";
|
|
4
3
|
import { beforeAll, describe, expect, it, vi } from "vitest";
|
|
5
4
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
6
|
-
import {
|
|
7
|
-
|
|
5
|
+
import {
|
|
6
|
+
buildEditorSnapshot,
|
|
7
|
+
buildEmptyFilterValue,
|
|
8
|
+
FilterPillEditor,
|
|
9
|
+
} from "../filter-pill-editor";
|
|
10
|
+
import { defaultFilterValueFor, Filter } from "../filters";
|
|
11
|
+
import {
|
|
12
|
+
buildFilterTestTable,
|
|
13
|
+
type FilterColumnSpec,
|
|
14
|
+
} from "./filter-test-utils";
|
|
8
15
|
|
|
9
16
|
const renderWithProviders = (ui: React.ReactElement) =>
|
|
10
17
|
render(<TooltipProvider>{ui}</TooltipProvider>);
|
|
@@ -24,37 +31,15 @@ beforeAll(() => {
|
|
|
24
31
|
}
|
|
25
32
|
});
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
id:
|
|
29
|
-
filterType:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
| "date"
|
|
35
|
-
| "datetime"
|
|
36
|
-
| "time",
|
|
37
|
-
): Column<unknown, unknown> {
|
|
38
|
-
return {
|
|
39
|
-
id,
|
|
40
|
-
columnDef: { meta: { filterType, dataType: "string" } },
|
|
41
|
-
} as unknown as Column<unknown, unknown>;
|
|
42
|
-
}
|
|
34
|
+
const DEFAULT_COLUMNS: FilterColumnSpec[] = [
|
|
35
|
+
{ id: "name", filterType: "text" },
|
|
36
|
+
{ id: "age", filterType: "number" },
|
|
37
|
+
{ id: "when", filterType: "date" },
|
|
38
|
+
{ id: "at", filterType: "datetime" },
|
|
39
|
+
{ id: "clock", filterType: "time" },
|
|
40
|
+
];
|
|
43
41
|
|
|
44
|
-
|
|
45
|
-
const columns = [
|
|
46
|
-
makeColumn("name", "text"),
|
|
47
|
-
makeColumn("age", "number"),
|
|
48
|
-
makeColumn("when", "date"),
|
|
49
|
-
makeColumn("at", "datetime"),
|
|
50
|
-
makeColumn("clock", "time"),
|
|
51
|
-
];
|
|
52
|
-
return {
|
|
53
|
-
getAllColumns: () => columns,
|
|
54
|
-
getColumn: (id: string) => columns.find((c) => c.id === id),
|
|
55
|
-
setColumnFilters: vi.fn(),
|
|
56
|
-
} as unknown as Table<unknown>;
|
|
57
|
-
}
|
|
42
|
+
const mockTable = () => buildFilterTestTable(DEFAULT_COLUMNS).table;
|
|
58
43
|
|
|
59
44
|
async function calculateTopK() {
|
|
60
45
|
return {
|
|
@@ -111,7 +96,8 @@ describe("FilterPillEditor — snapshot rehydration", () => {
|
|
|
111
96
|
onClose={vi.fn()}
|
|
112
97
|
/>,
|
|
113
98
|
);
|
|
114
|
-
expect(await screen.findByText("
|
|
99
|
+
expect(await screen.findByText("a")).toBeInTheDocument();
|
|
100
|
+
expect(screen.getByText("b")).toBeInTheDocument();
|
|
115
101
|
});
|
|
116
102
|
|
|
117
103
|
it("rehydrates a text contains snapshot with seeded text", () => {
|
|
@@ -232,7 +218,7 @@ describe("FilterPillEditor — date/datetime/time", () => {
|
|
|
232
218
|
|
|
233
219
|
describe("FilterPillEditor — apply", () => {
|
|
234
220
|
it("commits a number > filter via setColumnFilters", () => {
|
|
235
|
-
const table =
|
|
221
|
+
const { table, setColumnFilters } = buildFilterTestTable(DEFAULT_COLUMNS);
|
|
236
222
|
const onClose = vi.fn();
|
|
237
223
|
renderWithProviders(
|
|
238
224
|
<FilterPillEditor
|
|
@@ -245,11 +231,10 @@ describe("FilterPillEditor — apply", () => {
|
|
|
245
231
|
/>,
|
|
246
232
|
);
|
|
247
233
|
fireEvent.click(screen.getByLabelText("Apply filter"));
|
|
248
|
-
expect(
|
|
234
|
+
expect(setColumnFilters).toHaveBeenCalledTimes(1);
|
|
249
235
|
expect(onClose).toHaveBeenCalledTimes(1);
|
|
250
236
|
|
|
251
|
-
const updater =
|
|
252
|
-
.calls[0][0];
|
|
237
|
+
const updater = setColumnFilters.mock.calls[0][0];
|
|
253
238
|
const next = updater([]);
|
|
254
239
|
expect(next).toEqual([
|
|
255
240
|
{
|
|
@@ -259,3 +244,55 @@ describe("FilterPillEditor — apply", () => {
|
|
|
259
244
|
]);
|
|
260
245
|
});
|
|
261
246
|
});
|
|
247
|
+
|
|
248
|
+
describe("defaultFilterValueFor", () => {
|
|
249
|
+
it.each([
|
|
250
|
+
["number", "between", { type: "number", operator: "between" }],
|
|
251
|
+
["number", ">", { type: "number", operator: ">" }],
|
|
252
|
+
["text", "contains", { type: "text", operator: "contains" }],
|
|
253
|
+
["text", "in", { type: "text", operator: "in", values: [] }],
|
|
254
|
+
["text", "not_in", { type: "text", operator: "not_in", values: [] }],
|
|
255
|
+
["boolean", "is_true", { type: "boolean", operator: "is_true" }],
|
|
256
|
+
["number", "in", { type: "number", operator: "in", values: [] }],
|
|
257
|
+
["number", "not_in", { type: "number", operator: "not_in", values: [] }],
|
|
258
|
+
["date", "between", { type: "date", operator: "between" }],
|
|
259
|
+
["datetime", "between", { type: "datetime", operator: "between" }],
|
|
260
|
+
["time", "between", { type: "time", operator: "between" }],
|
|
261
|
+
] as const)("seeds %s + %s", (type, operator, expected) => {
|
|
262
|
+
expect(defaultFilterValueFor(type, operator)).toEqual(expected);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe("buildEmptyFilterValue", () => {
|
|
267
|
+
it.each([
|
|
268
|
+
["name", { type: "text", operator: "contains" }],
|
|
269
|
+
["age", { type: "number", operator: "between" }],
|
|
270
|
+
["when", { type: "date", operator: "==" }],
|
|
271
|
+
["at", { type: "datetime", operator: "==" }],
|
|
272
|
+
["clock", { type: "time", operator: "between" }],
|
|
273
|
+
] as const)(
|
|
274
|
+
"picks the dtype-default operator for %s",
|
|
275
|
+
(columnId, expected) => {
|
|
276
|
+
const column = mockTable().getColumn(columnId)!;
|
|
277
|
+
expect(buildEmptyFilterValue(column)).toEqual(expected);
|
|
278
|
+
},
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe("buildEditorSnapshot", () => {
|
|
283
|
+
it("uses dtype-default operator when none provided", () => {
|
|
284
|
+
const column = mockTable().getColumn("name")!;
|
|
285
|
+
expect(buildEditorSnapshot(column)).toEqual({
|
|
286
|
+
columnId: "name",
|
|
287
|
+
value: { type: "text", operator: "contains" },
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("honors an explicit operator override", () => {
|
|
292
|
+
const column = mockTable().getColumn("name")!;
|
|
293
|
+
expect(buildEditorSnapshot(column, { operator: "in" })).toEqual({
|
|
294
|
+
columnId: "name",
|
|
295
|
+
value: { type: "text", operator: "in", values: [] },
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
});
|