@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
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import type { ColumnFiltersState } from "@tanstack/react-table";
|
|
3
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
4
|
+
import { beforeAll, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
6
|
+
import { AddFilterButton } from "../add-filter-button";
|
|
7
|
+
import { FilterPills } from "../filter-pills";
|
|
8
|
+
import { buildEditorSnapshot } from "../filter-pill-editor";
|
|
9
|
+
import { Filter, type Snapshot } from "../filters";
|
|
10
|
+
import {
|
|
11
|
+
buildFilterTestTable,
|
|
12
|
+
type FilterColumnSpec,
|
|
13
|
+
} from "./filter-test-utils";
|
|
14
|
+
|
|
15
|
+
const renderWithProviders = (ui: React.ReactElement) =>
|
|
16
|
+
render(<TooltipProvider>{ui}</TooltipProvider>);
|
|
17
|
+
|
|
18
|
+
beforeAll(() => {
|
|
19
|
+
global.HTMLElement.prototype.scrollIntoView = () => {
|
|
20
|
+
// jsdom does not implement scrollIntoView; cmdk calls it on selection.
|
|
21
|
+
};
|
|
22
|
+
if (!global.HTMLElement.prototype.hasPointerCapture) {
|
|
23
|
+
global.HTMLElement.prototype.hasPointerCapture = () => false;
|
|
24
|
+
}
|
|
25
|
+
if (!global.HTMLElement.prototype.scrollTo) {
|
|
26
|
+
global.HTMLElement.prototype.scrollTo = () => {
|
|
27
|
+
// noop for jsdom
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const DEFAULT_COLUMNS: FilterColumnSpec[] = [
|
|
33
|
+
{ id: "name", filterType: "text" },
|
|
34
|
+
{ id: "age", filterType: "number" },
|
|
35
|
+
{ id: "when", filterType: "date" },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const mockTable = (specs: FilterColumnSpec[] = DEFAULT_COLUMNS) =>
|
|
39
|
+
buildFilterTestTable(specs).table;
|
|
40
|
+
|
|
41
|
+
describe("FilterPills — strip gating", () => {
|
|
42
|
+
it("renders nothing when there are no filters and no pending add-snapshot", () => {
|
|
43
|
+
const { container } = renderWithProviders(
|
|
44
|
+
<FilterPills
|
|
45
|
+
filters={[]}
|
|
46
|
+
table={mockTable()}
|
|
47
|
+
addFilterSnapshot={null}
|
|
48
|
+
onAddFilterSnapshotChange={vi.fn()}
|
|
49
|
+
/>,
|
|
50
|
+
);
|
|
51
|
+
expect(container.firstChild).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("renders the strip and + button when at least one filter exists", () => {
|
|
55
|
+
const filters: ColumnFiltersState = [
|
|
56
|
+
{
|
|
57
|
+
id: "age",
|
|
58
|
+
value: Filter.number({ operator: ">", value: 18 }),
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
renderWithProviders(
|
|
62
|
+
<FilterPills
|
|
63
|
+
filters={filters}
|
|
64
|
+
table={mockTable()}
|
|
65
|
+
addFilterSnapshot={null}
|
|
66
|
+
onAddFilterSnapshotChange={vi.fn()}
|
|
67
|
+
/>,
|
|
68
|
+
);
|
|
69
|
+
expect(screen.getByLabelText("Add filter")).toBeInTheDocument();
|
|
70
|
+
expect(screen.getByLabelText("Edit filter on age")).toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("mounts the strip with no filters when a pending add-snapshot is set", () => {
|
|
74
|
+
const table = mockTable();
|
|
75
|
+
const snapshot: Snapshot = buildEditorSnapshot(table.getAllColumns()[0]);
|
|
76
|
+
renderWithProviders(
|
|
77
|
+
<FilterPills
|
|
78
|
+
filters={[]}
|
|
79
|
+
table={table}
|
|
80
|
+
addFilterSnapshot={snapshot}
|
|
81
|
+
onAddFilterSnapshotChange={vi.fn()}
|
|
82
|
+
/>,
|
|
83
|
+
);
|
|
84
|
+
expect(screen.getByLabelText("Add filter")).toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("AddFilterButton", () => {
|
|
89
|
+
it("does not render when there are no editable columns", () => {
|
|
90
|
+
const table = mockTable([{ id: "opaque" }]);
|
|
91
|
+
const { container } = renderWithProviders(
|
|
92
|
+
<AddFilterButton
|
|
93
|
+
table={table}
|
|
94
|
+
snapshot={null}
|
|
95
|
+
onSnapshotChange={vi.fn()}
|
|
96
|
+
/>,
|
|
97
|
+
);
|
|
98
|
+
expect(container.firstChild).toBeNull();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("seeds the editor with the first editable column's dtype default on click", () => {
|
|
102
|
+
const table = mockTable();
|
|
103
|
+
const onSnapshotChange = vi.fn();
|
|
104
|
+
renderWithProviders(
|
|
105
|
+
<AddFilterButton
|
|
106
|
+
table={table}
|
|
107
|
+
snapshot={null}
|
|
108
|
+
onSnapshotChange={onSnapshotChange}
|
|
109
|
+
/>,
|
|
110
|
+
);
|
|
111
|
+
fireEvent.click(screen.getByLabelText("Add filter"));
|
|
112
|
+
expect(onSnapshotChange).toHaveBeenCalledTimes(1);
|
|
113
|
+
expect(onSnapshotChange).toHaveBeenCalledWith({
|
|
114
|
+
columnId: "name",
|
|
115
|
+
value: { type: "text", operator: "contains" },
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("clears the snapshot when the popover closes", () => {
|
|
120
|
+
const onSnapshotChange = vi.fn();
|
|
121
|
+
const snapshot: Snapshot = {
|
|
122
|
+
columnId: "age",
|
|
123
|
+
value: Filter.number({ operator: ">", value: 5 }),
|
|
124
|
+
};
|
|
125
|
+
renderWithProviders(
|
|
126
|
+
<AddFilterButton
|
|
127
|
+
table={mockTable()}
|
|
128
|
+
snapshot={snapshot}
|
|
129
|
+
onSnapshotChange={onSnapshotChange}
|
|
130
|
+
/>,
|
|
131
|
+
);
|
|
132
|
+
// Pressing Escape closes the popover via radix.
|
|
133
|
+
fireEvent.keyDown(document.body, { key: "Escape" });
|
|
134
|
+
expect(onSnapshotChange).toHaveBeenCalledWith(null);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("renders the editor when a snapshot is provided", () => {
|
|
138
|
+
renderWithProviders(
|
|
139
|
+
<AddFilterButton
|
|
140
|
+
table={mockTable()}
|
|
141
|
+
snapshot={{
|
|
142
|
+
columnId: "age",
|
|
143
|
+
value: Filter.number({ operator: ">", value: 7 }),
|
|
144
|
+
}}
|
|
145
|
+
onSnapshotChange={vi.fn()}
|
|
146
|
+
/>,
|
|
147
|
+
);
|
|
148
|
+
expect(screen.getByLabelText("Apply filter")).toBeInTheDocument();
|
|
149
|
+
expect(screen.getByDisplayValue("7")).toBeInTheDocument();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("FilterPills — pill edit", () => {
|
|
154
|
+
it("opens the editor with the pill's snapshot when clicked", () => {
|
|
155
|
+
const filters: ColumnFiltersState = [
|
|
156
|
+
{
|
|
157
|
+
id: "age",
|
|
158
|
+
value: Filter.number({ operator: ">", value: 42 }),
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
renderWithProviders(
|
|
162
|
+
<FilterPills
|
|
163
|
+
filters={filters}
|
|
164
|
+
table={mockTable()}
|
|
165
|
+
addFilterSnapshot={null}
|
|
166
|
+
onAddFilterSnapshotChange={vi.fn()}
|
|
167
|
+
/>,
|
|
168
|
+
);
|
|
169
|
+
fireEvent.click(screen.getByLabelText("Edit filter on age"));
|
|
170
|
+
expect(screen.getByLabelText("Apply filter")).toBeInTheDocument();
|
|
171
|
+
expect(screen.getByDisplayValue("42")).toBeInTheDocument();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("splices in place via editIndex when applying an edit", () => {
|
|
175
|
+
const { table, setColumnFilters } = buildFilterTestTable(DEFAULT_COLUMNS);
|
|
176
|
+
const filters: ColumnFiltersState = [
|
|
177
|
+
{
|
|
178
|
+
id: "name",
|
|
179
|
+
value: Filter.text({ operator: "contains", text: "foo" }),
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: "age",
|
|
183
|
+
value: Filter.number({ operator: ">", value: 1 }),
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
renderWithProviders(
|
|
187
|
+
<FilterPills
|
|
188
|
+
filters={filters}
|
|
189
|
+
table={table}
|
|
190
|
+
addFilterSnapshot={null}
|
|
191
|
+
onAddFilterSnapshotChange={vi.fn()}
|
|
192
|
+
/>,
|
|
193
|
+
);
|
|
194
|
+
fireEvent.click(screen.getByLabelText("Edit filter on age"));
|
|
195
|
+
fireEvent.click(screen.getByLabelText("Apply filter"));
|
|
196
|
+
|
|
197
|
+
const updater = setColumnFilters.mock.calls[0][0];
|
|
198
|
+
const next = updater(filters);
|
|
199
|
+
expect(next).toHaveLength(2);
|
|
200
|
+
expect(next[0].id).toBe("name");
|
|
201
|
+
expect(next[1]).toEqual({
|
|
202
|
+
id: "age",
|
|
203
|
+
value: { type: "number", operator: ">", value: 1 },
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("FilterPills — column-header trigger integration", () => {
|
|
209
|
+
it("opens the editor under the + button with a column-header-supplied snapshot", () => {
|
|
210
|
+
// Simulates what DataTable does when column-header calls
|
|
211
|
+
// requestAddFilter({ columnId: "name" }): it computes a snapshot via
|
|
212
|
+
// buildEditorSnapshot and passes it down as addFilterSnapshot.
|
|
213
|
+
const table = mockTable();
|
|
214
|
+
const snapshot = buildEditorSnapshot(table.getColumn("name")!);
|
|
215
|
+
renderWithProviders(
|
|
216
|
+
<FilterPills
|
|
217
|
+
filters={[]}
|
|
218
|
+
table={table}
|
|
219
|
+
addFilterSnapshot={snapshot}
|
|
220
|
+
onAddFilterSnapshotChange={vi.fn()}
|
|
221
|
+
/>,
|
|
222
|
+
);
|
|
223
|
+
expect(screen.getByLabelText("Apply filter")).toBeInTheDocument();
|
|
224
|
+
// Default text operator is "contains" — the text input renders.
|
|
225
|
+
expect(screen.getByPlaceholderText("Text…")).toBeInTheDocument();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("pre-selects 'in' operator when column-header 'Filter by values' is used", () => {
|
|
229
|
+
const table = mockTable();
|
|
230
|
+
const snapshot = buildEditorSnapshot(table.getColumn("name")!, {
|
|
231
|
+
operator: "in",
|
|
232
|
+
});
|
|
233
|
+
renderWithProviders(
|
|
234
|
+
<FilterPills
|
|
235
|
+
filters={[]}
|
|
236
|
+
table={table}
|
|
237
|
+
addFilterSnapshot={snapshot}
|
|
238
|
+
onAddFilterSnapshotChange={vi.fn()}
|
|
239
|
+
/>,
|
|
240
|
+
);
|
|
241
|
+
expect(snapshot.value).toEqual({
|
|
242
|
+
type: "text",
|
|
243
|
+
operator: "in",
|
|
244
|
+
values: [],
|
|
245
|
+
});
|
|
246
|
+
expect(screen.getByLabelText("Apply filter")).toBeInTheDocument();
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe("FilterPills — overflow", () => {
|
|
251
|
+
it("does not render the 'See all' button when there is no overflow", () => {
|
|
252
|
+
const filters: ColumnFiltersState = [
|
|
253
|
+
{ id: "age", value: Filter.number({ operator: ">", value: 5 }) },
|
|
254
|
+
];
|
|
255
|
+
renderWithProviders(
|
|
256
|
+
<FilterPills
|
|
257
|
+
filters={filters}
|
|
258
|
+
table={mockTable()}
|
|
259
|
+
addFilterSnapshot={null}
|
|
260
|
+
onAddFilterSnapshotChange={vi.fn()}
|
|
261
|
+
/>,
|
|
262
|
+
);
|
|
263
|
+
expect(screen.queryByLabelText("See all filters")).toBeNull();
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe("FilterPill — value truncation", () => {
|
|
268
|
+
it("renders the value with truncation classes inside a tooltip trigger", () => {
|
|
269
|
+
const filters: ColumnFiltersState = [
|
|
270
|
+
{
|
|
271
|
+
id: "name",
|
|
272
|
+
value: Filter.text({ operator: "contains", text: "x".repeat(100) }),
|
|
273
|
+
},
|
|
274
|
+
];
|
|
275
|
+
renderWithProviders(
|
|
276
|
+
<FilterPills
|
|
277
|
+
filters={filters}
|
|
278
|
+
table={mockTable()}
|
|
279
|
+
addFilterSnapshot={null}
|
|
280
|
+
onAddFilterSnapshotChange={vi.fn()}
|
|
281
|
+
/>,
|
|
282
|
+
);
|
|
283
|
+
const valueSpan = screen.getByText(/^"x{100}"$/);
|
|
284
|
+
expect(valueSpan.className).toMatch(/overflow-hidden/);
|
|
285
|
+
expect(valueSpan.className).toMatch(/text-ellipsis/);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type ColumnDef,
|
|
5
|
+
getCoreRowModel,
|
|
6
|
+
type Table,
|
|
7
|
+
useReactTable,
|
|
8
|
+
} from "@tanstack/react-table";
|
|
9
|
+
import { renderHook } from "@testing-library/react";
|
|
10
|
+
import { vi } from "vitest";
|
|
11
|
+
import type { FilterType } from "../filters";
|
|
12
|
+
|
|
13
|
+
export interface FilterColumnSpec {
|
|
14
|
+
id: string;
|
|
15
|
+
filterType?: FilterType;
|
|
16
|
+
dtype?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface FilterTestHarness {
|
|
20
|
+
table: Table<unknown>;
|
|
21
|
+
setColumnFilters: ReturnType<typeof vi.fn>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function buildFilterTestTable(
|
|
25
|
+
specs: FilterColumnSpec[],
|
|
26
|
+
): FilterTestHarness {
|
|
27
|
+
const setColumnFilters = vi.fn();
|
|
28
|
+
const columns: Array<ColumnDef<unknown>> = specs.map((spec) => ({
|
|
29
|
+
id: spec.id,
|
|
30
|
+
accessorFn: () => undefined,
|
|
31
|
+
header: spec.id,
|
|
32
|
+
meta: {
|
|
33
|
+
...(spec.filterType !== undefined ? { filterType: spec.filterType } : {}),
|
|
34
|
+
...(spec.dtype !== undefined ? { dtype: spec.dtype } : {}),
|
|
35
|
+
},
|
|
36
|
+
}));
|
|
37
|
+
const { result } = renderHook(() =>
|
|
38
|
+
useReactTable<unknown>({
|
|
39
|
+
data: [],
|
|
40
|
+
columns,
|
|
41
|
+
locale: "en-US",
|
|
42
|
+
getCoreRowModel: getCoreRowModel(),
|
|
43
|
+
onColumnFiltersChange: setColumnFilters,
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
return { table: result.current, setColumnFilters };
|
|
47
|
+
}
|
|
@@ -172,7 +172,7 @@ describe("filterToFilterCondition", () => {
|
|
|
172
172
|
it("handles boolean true filter", () => {
|
|
173
173
|
const result = filterToFilterCondition(
|
|
174
174
|
"active",
|
|
175
|
-
Filter.boolean({
|
|
175
|
+
Filter.boolean({ operator: "is_true" }),
|
|
176
176
|
);
|
|
177
177
|
expect(result).toEqual([
|
|
178
178
|
{
|
|
@@ -187,7 +187,7 @@ describe("filterToFilterCondition", () => {
|
|
|
187
187
|
it("handles boolean false filter", () => {
|
|
188
188
|
const result = filterToFilterCondition(
|
|
189
189
|
"active",
|
|
190
|
-
Filter.boolean({
|
|
190
|
+
Filter.boolean({ operator: "is_false" }),
|
|
191
191
|
);
|
|
192
192
|
expect(result).toEqual([
|
|
193
193
|
{
|
|
@@ -199,16 +199,16 @@ describe("filterToFilterCondition", () => {
|
|
|
199
199
|
]);
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
-
it("handles
|
|
202
|
+
it("handles number in filter", () => {
|
|
203
203
|
const result = filterToFilterCondition(
|
|
204
204
|
"status",
|
|
205
|
-
Filter.
|
|
205
|
+
Filter.number({ operator: "in", values: [1, 2] }),
|
|
206
206
|
);
|
|
207
207
|
expect(result).toEqual([
|
|
208
208
|
{
|
|
209
209
|
column_id: "status",
|
|
210
210
|
operator: "in",
|
|
211
|
-
value: [
|
|
211
|
+
value: [1, 2],
|
|
212
212
|
type: "condition",
|
|
213
213
|
negate: false,
|
|
214
214
|
},
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
"use no memo";
|
|
3
|
+
|
|
4
|
+
import type { Column, Table } from "@tanstack/react-table";
|
|
5
|
+
import { PlusIcon } from "lucide-react";
|
|
6
|
+
import type { CalculateTopKRows } from "@/plugins/impl/DataTablePlugin";
|
|
7
|
+
import { Button } from "../ui/button";
|
|
8
|
+
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
|
9
|
+
import {
|
|
10
|
+
buildEditorSnapshot,
|
|
11
|
+
editableColumns,
|
|
12
|
+
FilterPillEditor,
|
|
13
|
+
} from "./filter-pill-editor";
|
|
14
|
+
import type { Snapshot } from "./filters";
|
|
15
|
+
|
|
16
|
+
interface AddFilterButtonProps<TData> {
|
|
17
|
+
table: Table<TData>;
|
|
18
|
+
calculateTopKRows?: CalculateTopKRows;
|
|
19
|
+
snapshot: Snapshot | null;
|
|
20
|
+
onSnapshotChange: (snapshot: Snapshot | null) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const AddFilterButton = <TData,>({
|
|
24
|
+
table,
|
|
25
|
+
calculateTopKRows,
|
|
26
|
+
snapshot,
|
|
27
|
+
onSnapshotChange,
|
|
28
|
+
}: AddFilterButtonProps<TData>) => {
|
|
29
|
+
const columns = editableColumns(table);
|
|
30
|
+
|
|
31
|
+
if (columns.length === 0) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const handleOpenChange = (open: boolean) => {
|
|
36
|
+
if (!open) {
|
|
37
|
+
onSnapshotChange(null);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (snapshot === null) {
|
|
41
|
+
onSnapshotChange(
|
|
42
|
+
buildEditorSnapshot(columns[0] as Column<unknown, unknown>),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Popover
|
|
49
|
+
open={snapshot !== null}
|
|
50
|
+
onOpenChange={handleOpenChange}
|
|
51
|
+
modal={false}
|
|
52
|
+
>
|
|
53
|
+
<PopoverTrigger asChild={true}>
|
|
54
|
+
<Button
|
|
55
|
+
type="button"
|
|
56
|
+
size="icon"
|
|
57
|
+
variant="ghost"
|
|
58
|
+
className="h-5 w-5 -mr-1 rounded-full text-muted-foreground hover:text-foreground"
|
|
59
|
+
aria-label="Add filter"
|
|
60
|
+
>
|
|
61
|
+
<PlusIcon className="h-3.5 w-3.5" aria-hidden={true} />
|
|
62
|
+
</Button>
|
|
63
|
+
</PopoverTrigger>
|
|
64
|
+
{snapshot !== null && (
|
|
65
|
+
<PopoverContent
|
|
66
|
+
className="w-auto p-0"
|
|
67
|
+
align="start"
|
|
68
|
+
alignOffset={-10}
|
|
69
|
+
sideOffset={10}
|
|
70
|
+
avoidCollisions={true}
|
|
71
|
+
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
72
|
+
onCloseAutoFocus={(e) => e.preventDefault()}
|
|
73
|
+
>
|
|
74
|
+
<FilterPillEditor
|
|
75
|
+
key={`${snapshot.columnId}:${snapshot.value.operator}`}
|
|
76
|
+
snapshot={snapshot}
|
|
77
|
+
table={table}
|
|
78
|
+
calculateTopKRows={calculateTopKRows}
|
|
79
|
+
onClose={() => onSnapshotChange(null)}
|
|
80
|
+
/>
|
|
81
|
+
</PopoverContent>
|
|
82
|
+
)}
|
|
83
|
+
</Popover>
|
|
84
|
+
);
|
|
85
|
+
};
|