@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
|
@@ -2,62 +2,154 @@
|
|
|
2
2
|
"use no memo";
|
|
3
3
|
|
|
4
4
|
import type { ColumnFiltersState, Table } from "@tanstack/react-table";
|
|
5
|
-
import { XIcon } from "lucide-react";
|
|
6
|
-
import { useState } from "react";
|
|
7
|
-
import {
|
|
5
|
+
import { MoreHorizontalIcon, XIcon } from "lucide-react";
|
|
6
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
7
|
+
import { useLocale } from "react-aria";
|
|
8
8
|
import type { CalculateTopKRows } from "@/plugins/impl/DataTablePlugin";
|
|
9
|
-
import { logNever } from "@/utils/assertNever";
|
|
10
9
|
import { cn } from "@/utils/cn";
|
|
11
10
|
import { Badge } from "../ui/badge";
|
|
12
11
|
import { Button } from "../ui/button";
|
|
13
|
-
import {
|
|
14
|
-
import { FilterPillEditor } from "./filter-pill-editor";
|
|
12
|
+
import { DraggablePopover } from "../ui/draggable-popover";
|
|
15
13
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
import {
|
|
14
|
+
Popover,
|
|
15
|
+
PopoverClose,
|
|
16
|
+
PopoverContent,
|
|
17
|
+
PopoverTrigger,
|
|
18
|
+
} from "../ui/popover";
|
|
19
|
+
import { Tooltip } from "../ui/tooltip";
|
|
20
|
+
import { AddFilterButton } from "./add-filter-button";
|
|
21
|
+
import { FilterPillEditor } from "./filter-pill-editor";
|
|
22
|
+
import { type ColumnFilterValue, formatValue, type Snapshot } from "./filters";
|
|
23
|
+
import { extractTimezone } from "./types";
|
|
24
|
+
import { ChipWithComma, CompactChipRow } from "./value-chips";
|
|
22
25
|
|
|
23
26
|
interface Props<TData> {
|
|
24
27
|
filters: ColumnFiltersState | undefined;
|
|
25
28
|
table: Table<TData>;
|
|
26
29
|
calculateTopKRows?: CalculateTopKRows;
|
|
30
|
+
addFilterSnapshot: Snapshot | null;
|
|
31
|
+
onAddFilterSnapshotChange: (snapshot: Snapshot | null) => void;
|
|
27
32
|
}
|
|
28
33
|
|
|
34
|
+
const useHasOverflow = (
|
|
35
|
+
ref: React.RefObject<HTMLElement | null>,
|
|
36
|
+
signature: string,
|
|
37
|
+
): boolean => {
|
|
38
|
+
const [hasOverflow, setHasOverflow] = useState(false);
|
|
39
|
+
useLayoutEffect(() => {
|
|
40
|
+
const el = ref.current;
|
|
41
|
+
if (!el) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const measure = () => setHasOverflow(el.scrollWidth > el.clientWidth);
|
|
45
|
+
measure();
|
|
46
|
+
const ro = new ResizeObserver(measure);
|
|
47
|
+
ro.observe(el);
|
|
48
|
+
return () => ro.disconnect();
|
|
49
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: signature triggers re-measure on filter content change
|
|
50
|
+
}, [ref, signature]);
|
|
51
|
+
return hasOverflow;
|
|
52
|
+
};
|
|
53
|
+
|
|
29
54
|
export const FilterPills = <TData,>({
|
|
30
55
|
filters,
|
|
31
56
|
table,
|
|
32
57
|
calculateTopKRows,
|
|
58
|
+
addFilterSnapshot,
|
|
59
|
+
onAddFilterSnapshotChange,
|
|
33
60
|
}: Props<TData>) => {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
61
|
+
const { locale } = useLocale();
|
|
62
|
+
|
|
63
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
64
|
+
const [overflowOpen, setOverflowOpen] = useState(false);
|
|
65
|
+
const allFilters = filters ?? [];
|
|
66
|
+
const signature = allFilters
|
|
67
|
+
.map((f) => `${f.id}:${JSON.stringify(f.value)}`)
|
|
68
|
+
.join("|");
|
|
69
|
+
const hasOverflow = useHasOverflow(containerRef, signature);
|
|
39
70
|
|
|
40
|
-
if (
|
|
71
|
+
if (allFilters.length === 0 && addFilterSnapshot === null) {
|
|
41
72
|
return null;
|
|
42
73
|
}
|
|
43
74
|
|
|
75
|
+
const renderPill = (filter: ColumnFiltersState[number], index: number) => (
|
|
76
|
+
<FilterPill
|
|
77
|
+
key={`${filter.id}:${index}`}
|
|
78
|
+
columnId={filter.id}
|
|
79
|
+
value={filter.value as ColumnFilterValue}
|
|
80
|
+
index={index}
|
|
81
|
+
locale={locale}
|
|
82
|
+
table={table}
|
|
83
|
+
calculateTopKRows={calculateTopKRows}
|
|
84
|
+
onRemove={() =>
|
|
85
|
+
table.setColumnFilters((current) =>
|
|
86
|
+
current.filter((_, i) => i !== index),
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
|
|
44
92
|
return (
|
|
45
|
-
<div
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
93
|
+
<div
|
|
94
|
+
part="filter-pills"
|
|
95
|
+
className="relative flex items-center gap-2 px-1 py-2"
|
|
96
|
+
>
|
|
97
|
+
<AddFilterButton
|
|
98
|
+
table={table}
|
|
99
|
+
calculateTopKRows={calculateTopKRows}
|
|
100
|
+
snapshot={addFilterSnapshot}
|
|
101
|
+
onSnapshotChange={onAddFilterSnapshotChange}
|
|
102
|
+
/>
|
|
103
|
+
<div
|
|
104
|
+
ref={containerRef}
|
|
105
|
+
className={cn(
|
|
106
|
+
"flex flex-nowrap items-center gap-2 overflow-hidden min-w-0 flex-1",
|
|
107
|
+
hasOverflow &&
|
|
108
|
+
"mask-[linear-gradient(to_right,black_calc(100%-2rem),transparent)]",
|
|
109
|
+
)}
|
|
110
|
+
>
|
|
111
|
+
{allFilters.map((filter, index) => renderPill(filter, index))}
|
|
112
|
+
</div>
|
|
113
|
+
{hasOverflow && (
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
onClick={() => setOverflowOpen(true)}
|
|
117
|
+
className="shrink-0 inline-flex items-center gap-0.5 rounded-full border border-border bg-background px-2 py-0.5 text-xs text-foreground hover:bg-accent hover:text-accent-foreground"
|
|
118
|
+
aria-label="See all filters"
|
|
119
|
+
>
|
|
120
|
+
<MoreHorizontalIcon className="h-3.5 w-3.5" aria-hidden={true} />
|
|
121
|
+
<span>See all</span>
|
|
122
|
+
</button>
|
|
123
|
+
)}
|
|
124
|
+
{hasOverflow && (
|
|
125
|
+
<DraggablePopover
|
|
126
|
+
open={overflowOpen}
|
|
127
|
+
onOpenChange={setOverflowOpen}
|
|
128
|
+
className="w-fit max-w-[min(90vw,40rem)] p-0"
|
|
129
|
+
>
|
|
130
|
+
<PopoverClose className="absolute top-2 right-2" aria-label="Close">
|
|
131
|
+
<XIcon className="h-4 w-4" aria-hidden={true} />
|
|
132
|
+
</PopoverClose>
|
|
133
|
+
<div className="flex flex-col pt-7">
|
|
134
|
+
<div className="max-h-80 overflow-y-auto flex flex-col items-start gap-2 px-3 pb-2">
|
|
135
|
+
{allFilters.map((filter, index) => renderPill(filter, index))}
|
|
136
|
+
</div>
|
|
137
|
+
<div className="flex justify-end border-t border-border px-3 py-2">
|
|
138
|
+
<Button
|
|
139
|
+
type="button"
|
|
140
|
+
size="sm"
|
|
141
|
+
variant="ghost"
|
|
142
|
+
onClick={() => {
|
|
143
|
+
table.setColumnFilters([]);
|
|
144
|
+
setOverflowOpen(false);
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
Clear all
|
|
148
|
+
</Button>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</DraggablePopover>
|
|
152
|
+
)}
|
|
61
153
|
</div>
|
|
62
154
|
);
|
|
63
155
|
};
|
|
@@ -65,7 +157,8 @@ export const FilterPills = <TData,>({
|
|
|
65
157
|
interface FilterPillProps<TData> {
|
|
66
158
|
columnId: string;
|
|
67
159
|
value: ColumnFilterValue;
|
|
68
|
-
|
|
160
|
+
index: number;
|
|
161
|
+
locale: string;
|
|
69
162
|
table: Table<TData>;
|
|
70
163
|
calculateTopKRows?: CalculateTopKRows;
|
|
71
164
|
onRemove: () => void;
|
|
@@ -74,28 +167,63 @@ interface FilterPillProps<TData> {
|
|
|
74
167
|
const FilterPill = <TData,>({
|
|
75
168
|
columnId,
|
|
76
169
|
value,
|
|
77
|
-
|
|
170
|
+
index,
|
|
171
|
+
locale,
|
|
78
172
|
table,
|
|
79
173
|
calculateTopKRows,
|
|
80
174
|
onRemove,
|
|
81
175
|
}: FilterPillProps<TData>) => {
|
|
82
176
|
const [open, setOpen] = useState(false);
|
|
83
177
|
|
|
84
|
-
const
|
|
178
|
+
const timezone = extractTimezone(
|
|
179
|
+
table.getColumn(columnId)?.columnDef.meta?.dtype,
|
|
180
|
+
);
|
|
181
|
+
const formatted = formatValue(value, { locale, timezone });
|
|
85
182
|
if (!formatted) {
|
|
86
183
|
return null;
|
|
87
184
|
}
|
|
88
185
|
|
|
89
|
-
const twoSegment =
|
|
186
|
+
const twoSegment =
|
|
187
|
+
formatted.kind === "scalar" && formatted.value === undefined;
|
|
90
188
|
|
|
91
189
|
const handleRemove = (e: React.MouseEvent) => {
|
|
92
190
|
e.stopPropagation();
|
|
93
191
|
onRemove();
|
|
94
192
|
};
|
|
95
193
|
|
|
96
|
-
const
|
|
194
|
+
const renderValue = (truncateValue: boolean) => {
|
|
195
|
+
if (formatted.kind === "scalar") {
|
|
196
|
+
return (
|
|
197
|
+
<span
|
|
198
|
+
className={cn(
|
|
199
|
+
"font-semibold text-foreground",
|
|
200
|
+
truncateValue &&
|
|
201
|
+
"inline-block max-w-[24ch] overflow-hidden text-ellipsis whitespace-nowrap align-middle",
|
|
202
|
+
)}
|
|
203
|
+
>
|
|
204
|
+
{formatted.value}
|
|
205
|
+
</span>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
if (truncateValue) {
|
|
209
|
+
return <CompactChipRow items={formatted.items} max={3} />;
|
|
210
|
+
}
|
|
211
|
+
return (
|
|
212
|
+
<span className="grid grid-cols-[repeat(5,max-content)] gap-1">
|
|
213
|
+
{formatted.items.map((item, i) => (
|
|
214
|
+
<ChipWithComma
|
|
215
|
+
key={i}
|
|
216
|
+
value={item}
|
|
217
|
+
showComma={i < formatted.items.length - 1}
|
|
218
|
+
/>
|
|
219
|
+
))}
|
|
220
|
+
</span>
|
|
221
|
+
);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const renderSegments = (truncateValue: boolean) => (
|
|
97
225
|
<>
|
|
98
|
-
<span className="text-foreground">{columnId}</span>
|
|
226
|
+
<span className="font-semibold text-foreground">{columnId}</span>
|
|
99
227
|
<Separator />
|
|
100
228
|
<span
|
|
101
229
|
className={cn(
|
|
@@ -108,12 +236,17 @@ const FilterPill = <TData,>({
|
|
|
108
236
|
{!twoSegment && (
|
|
109
237
|
<>
|
|
110
238
|
<Separator />
|
|
111
|
-
|
|
239
|
+
{renderValue(truncateValue)}
|
|
112
240
|
</>
|
|
113
241
|
)}
|
|
114
242
|
</>
|
|
115
243
|
);
|
|
116
244
|
|
|
245
|
+
const tooltip = (
|
|
246
|
+
<span className="inline-flex items-center">{renderSegments(false)}</span>
|
|
247
|
+
);
|
|
248
|
+
const segments = renderSegments(true);
|
|
249
|
+
|
|
117
250
|
const removeButton = (
|
|
118
251
|
<Button
|
|
119
252
|
type="button"
|
|
@@ -138,16 +271,20 @@ const FilterPill = <TData,>({
|
|
|
138
271
|
"transition-colors",
|
|
139
272
|
)}
|
|
140
273
|
>
|
|
141
|
-
<
|
|
142
|
-
<
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
274
|
+
<Tooltip content={tooltip} delayDuration={600}>
|
|
275
|
+
<span className="inline-flex items-center">
|
|
276
|
+
<PopoverTrigger asChild={true}>
|
|
277
|
+
<button
|
|
278
|
+
type="button"
|
|
279
|
+
className="inline-flex items-center whitespace-nowrap cursor-pointer bg-transparent border-0 p-0 [font:inherit] text-inherit"
|
|
280
|
+
aria-label={`Edit filter on ${columnId}`}
|
|
281
|
+
>
|
|
282
|
+
{segments}
|
|
283
|
+
</button>
|
|
284
|
+
</PopoverTrigger>
|
|
285
|
+
{removeButton}
|
|
286
|
+
</span>
|
|
287
|
+
</Tooltip>
|
|
151
288
|
</Badge>
|
|
152
289
|
<PopoverContent
|
|
153
290
|
className="w-auto p-0"
|
|
@@ -156,9 +293,11 @@ const FilterPill = <TData,>({
|
|
|
156
293
|
sideOffset={10}
|
|
157
294
|
avoidCollisions={true}
|
|
158
295
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
296
|
+
onCloseAutoFocus={(e) => e.preventDefault()}
|
|
159
297
|
>
|
|
160
298
|
<FilterPillEditor
|
|
161
299
|
snapshot={{ columnId, value }}
|
|
300
|
+
editIndex={index}
|
|
162
301
|
table={table}
|
|
163
302
|
calculateTopKRows={calculateTopKRows}
|
|
164
303
|
onClose={() => setOpen(false)}
|
|
@@ -173,105 +312,3 @@ function Separator() {
|
|
|
173
312
|
<span aria-hidden={true} className="mx-1.5 w-px h-3 bg-foreground/30" />
|
|
174
313
|
);
|
|
175
314
|
}
|
|
176
|
-
|
|
177
|
-
interface FormattedFilter {
|
|
178
|
-
operator: string;
|
|
179
|
-
value?: string;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function formatValue(
|
|
183
|
-
value: ColumnFilterValue,
|
|
184
|
-
timeFormatter: DateFormatter,
|
|
185
|
-
): FormattedFilter | undefined {
|
|
186
|
-
if (!("type" in value)) {
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (value.operator === "is_null") {
|
|
191
|
-
return { operator: "is null" };
|
|
192
|
-
}
|
|
193
|
-
if (value.operator === "is_not_null") {
|
|
194
|
-
return { operator: "is not null" };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (value.type === "number") {
|
|
198
|
-
switch (value.operator) {
|
|
199
|
-
case "between":
|
|
200
|
-
return {
|
|
201
|
-
operator: OPERATOR_LABELS.between.toLowerCase(),
|
|
202
|
-
value: `${value.min} - ${value.max}`,
|
|
203
|
-
};
|
|
204
|
-
case "==":
|
|
205
|
-
case "!=":
|
|
206
|
-
case ">":
|
|
207
|
-
case ">=":
|
|
208
|
-
case "<":
|
|
209
|
-
case "<=":
|
|
210
|
-
return { operator: value.operator, value: String(value.value) };
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
if (value.type === "text") {
|
|
214
|
-
switch (value.operator) {
|
|
215
|
-
case "in":
|
|
216
|
-
case "not_in": {
|
|
217
|
-
const items = value.values.map((v) => `"${v}"`);
|
|
218
|
-
return {
|
|
219
|
-
operator: value.operator === "in" ? "is in" : "not in",
|
|
220
|
-
value: `[${items.join(", ")}]`,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
case "is_empty":
|
|
224
|
-
return { operator: "is empty" };
|
|
225
|
-
case "contains":
|
|
226
|
-
case "equals":
|
|
227
|
-
case "does_not_equal":
|
|
228
|
-
case "regex":
|
|
229
|
-
case "starts_with":
|
|
230
|
-
case "ends_with":
|
|
231
|
-
return {
|
|
232
|
-
operator: OPERATOR_LABELS[value.operator].toLowerCase(),
|
|
233
|
-
value: `"${value.text}"`,
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
if (
|
|
238
|
-
value.type === "date" ||
|
|
239
|
-
value.type === "datetime" ||
|
|
240
|
-
value.type === "time"
|
|
241
|
-
) {
|
|
242
|
-
const format =
|
|
243
|
-
value.type === "time"
|
|
244
|
-
? (d: Date) => timeFormatter.format(d)
|
|
245
|
-
: value.type === "date"
|
|
246
|
-
? dateToISODate
|
|
247
|
-
: dateToISODateTime;
|
|
248
|
-
switch (value.operator) {
|
|
249
|
-
case "between":
|
|
250
|
-
return {
|
|
251
|
-
operator: OPERATOR_LABELS.between.toLowerCase(),
|
|
252
|
-
value: `${format(value.min)} - ${format(value.max)}`,
|
|
253
|
-
};
|
|
254
|
-
case "==":
|
|
255
|
-
case "!=":
|
|
256
|
-
case ">":
|
|
257
|
-
case ">=":
|
|
258
|
-
case "<":
|
|
259
|
-
case "<=":
|
|
260
|
-
return { operator: value.operator, value: format(value.value) };
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
if (value.type === "boolean") {
|
|
264
|
-
return { operator: `is ${value.value ? "True" : "False"}` };
|
|
265
|
-
}
|
|
266
|
-
if (value.type === "select") {
|
|
267
|
-
const stringifiedOptions = value.options.map((o) =>
|
|
268
|
-
stringifyUnknownValue({ value: o }),
|
|
269
|
-
);
|
|
270
|
-
return {
|
|
271
|
-
operator: value.operator === "in" ? "is in" : "not in",
|
|
272
|
-
value: `[${stringifiedOptions.join(", ")}]`,
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
logNever(value);
|
|
276
|
-
return undefined;
|
|
277
|
-
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
BooleanOp,
|
|
5
|
+
ComparisonOp,
|
|
6
|
+
MembershipOp,
|
|
7
|
+
NullishOp,
|
|
8
|
+
TextScalarOp,
|
|
9
|
+
} from "./operators";
|
|
10
|
+
import type { FilterType } from "./types";
|
|
11
|
+
|
|
12
|
+
interface NullishOpts {
|
|
13
|
+
operator: NullishOp;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface MembershipOpts {
|
|
17
|
+
operator: MembershipOp;
|
|
18
|
+
values: unknown[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface BetweenRangeOpts<T> {
|
|
22
|
+
operator: "between";
|
|
23
|
+
min: T;
|
|
24
|
+
max: T;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type NumberFilterOpts =
|
|
28
|
+
| { operator: ComparisonOp; value: number }
|
|
29
|
+
| BetweenRangeOpts<number>
|
|
30
|
+
| MembershipOpts
|
|
31
|
+
| NullishOpts;
|
|
32
|
+
|
|
33
|
+
type TextFilterOpts =
|
|
34
|
+
| { operator: TextScalarOp; text: string }
|
|
35
|
+
| { operator: "is_empty" }
|
|
36
|
+
| MembershipOpts
|
|
37
|
+
| NullishOpts;
|
|
38
|
+
|
|
39
|
+
type DateLikeFilterOpts =
|
|
40
|
+
| { operator: ComparisonOp; value: Date }
|
|
41
|
+
| BetweenRangeOpts<Date>
|
|
42
|
+
| NullishOpts;
|
|
43
|
+
|
|
44
|
+
interface BooleanFilterOpts {
|
|
45
|
+
operator: BooleanOp;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Filter is a factory function that creates a filter object
|
|
49
|
+
export const Filter = {
|
|
50
|
+
number(opts: NumberFilterOpts) {
|
|
51
|
+
return {
|
|
52
|
+
type: "number",
|
|
53
|
+
...opts,
|
|
54
|
+
} as const;
|
|
55
|
+
},
|
|
56
|
+
text(opts: TextFilterOpts) {
|
|
57
|
+
return {
|
|
58
|
+
type: "text",
|
|
59
|
+
...opts,
|
|
60
|
+
} as const;
|
|
61
|
+
},
|
|
62
|
+
date(opts: DateLikeFilterOpts) {
|
|
63
|
+
return {
|
|
64
|
+
type: "date",
|
|
65
|
+
...opts,
|
|
66
|
+
} as const;
|
|
67
|
+
},
|
|
68
|
+
datetime(opts: DateLikeFilterOpts) {
|
|
69
|
+
return {
|
|
70
|
+
type: "datetime",
|
|
71
|
+
...opts,
|
|
72
|
+
} as const;
|
|
73
|
+
},
|
|
74
|
+
time(opts: DateLikeFilterOpts) {
|
|
75
|
+
return {
|
|
76
|
+
type: "time",
|
|
77
|
+
...opts,
|
|
78
|
+
} as const;
|
|
79
|
+
},
|
|
80
|
+
boolean(opts: BooleanFilterOpts) {
|
|
81
|
+
return {
|
|
82
|
+
type: "boolean",
|
|
83
|
+
...opts,
|
|
84
|
+
} as const;
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type ColumnFilterValue = ReturnType<
|
|
89
|
+
(typeof Filter)[keyof typeof Filter]
|
|
90
|
+
>;
|
|
91
|
+
export type ColumnFilterForType<T extends FilterType> = T extends FilterType
|
|
92
|
+
? Extract<ColumnFilterValue, { type: T }>
|
|
93
|
+
: never;
|
|
94
|
+
export type MembershipFilterType = Extract<
|
|
95
|
+
ColumnFilterValue,
|
|
96
|
+
{ operator: "in" | "not_in" }
|
|
97
|
+
>["type"];
|
|
98
|
+
|
|
99
|
+
export interface Snapshot {
|
|
100
|
+
columnId: string;
|
|
101
|
+
value: ColumnFilterValue;
|
|
102
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
"use no memo";
|
|
3
|
+
|
|
4
|
+
import type { Column } from "@tanstack/react-table";
|
|
5
|
+
import type { OperatorType } from "@/plugins/impl/data-frames/utils/operators";
|
|
6
|
+
import type { ColumnFilterValue } from "./builders";
|
|
7
|
+
import { isMembershipFilterType, isMembershipOp } from "./guards";
|
|
8
|
+
import { EDITABLE_FILTER_TYPES, type FilterType } from "./types";
|
|
9
|
+
|
|
10
|
+
export function columnEditableType<TData, TValue>(
|
|
11
|
+
column: Column<TData, TValue>,
|
|
12
|
+
): FilterType {
|
|
13
|
+
const ft = column.columnDef.meta?.filterType;
|
|
14
|
+
if (!ft || !EDITABLE_FILTER_TYPES.has(ft)) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`Invalid or missing filterType for column ${column.id}: ${ft}`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return ft;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Minimal ColumnFilterValue for a (type, operator) pair, seeding extra fields for shapes that require them. */
|
|
23
|
+
export function defaultFilterValueFor(
|
|
24
|
+
type: FilterType,
|
|
25
|
+
operator: OperatorType,
|
|
26
|
+
): ColumnFilterValue {
|
|
27
|
+
if (isMembershipFilterType(type) && isMembershipOp(operator)) {
|
|
28
|
+
return { type, operator, values: [] };
|
|
29
|
+
}
|
|
30
|
+
return { type, operator } as ColumnFilterValue;
|
|
31
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { logNever } from "@/utils/assertNever";
|
|
4
|
+
import {
|
|
5
|
+
dateToLocalISODate,
|
|
6
|
+
dateToLocalISOTime,
|
|
7
|
+
exactDateTime,
|
|
8
|
+
} from "@/utils/dates";
|
|
9
|
+
import { OPERATOR_LABELS } from "../operator-labels";
|
|
10
|
+
import { stringifyUnknownValue } from "../utils";
|
|
11
|
+
import type { ColumnFilterValue } from "./builders";
|
|
12
|
+
import type { FormattedFilter } from "./types";
|
|
13
|
+
|
|
14
|
+
interface FormatContext {
|
|
15
|
+
locale: string;
|
|
16
|
+
timezone: string | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatValue(
|
|
20
|
+
value: ColumnFilterValue,
|
|
21
|
+
ctx: FormatContext,
|
|
22
|
+
): FormattedFilter | undefined {
|
|
23
|
+
if (!("type" in value)) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (value.operator === "is_null") {
|
|
28
|
+
return { kind: "scalar", operator: "is null" };
|
|
29
|
+
}
|
|
30
|
+
if (value.operator === "is_not_null") {
|
|
31
|
+
return { kind: "scalar", operator: "is not null" };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (value.type === "number") {
|
|
35
|
+
switch (value.operator) {
|
|
36
|
+
case "between":
|
|
37
|
+
return {
|
|
38
|
+
kind: "scalar",
|
|
39
|
+
operator: OPERATOR_LABELS.between.toLowerCase(),
|
|
40
|
+
value: `${value.min} - ${value.max}`,
|
|
41
|
+
};
|
|
42
|
+
case "==":
|
|
43
|
+
case "!=":
|
|
44
|
+
case ">":
|
|
45
|
+
case ">=":
|
|
46
|
+
case "<":
|
|
47
|
+
case "<=":
|
|
48
|
+
return {
|
|
49
|
+
kind: "scalar",
|
|
50
|
+
operator: value.operator,
|
|
51
|
+
value: String(value.value),
|
|
52
|
+
};
|
|
53
|
+
case "in":
|
|
54
|
+
case "not_in":
|
|
55
|
+
return {
|
|
56
|
+
kind: "list",
|
|
57
|
+
operator: value.operator === "in" ? "is in" : "not in",
|
|
58
|
+
items: value.values
|
|
59
|
+
.map((v) => stringifyUnknownValue({ value: v }))
|
|
60
|
+
.toSorted((a, b) => a.localeCompare(b)),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (value.type === "text") {
|
|
65
|
+
switch (value.operator) {
|
|
66
|
+
case "in":
|
|
67
|
+
case "not_in":
|
|
68
|
+
return {
|
|
69
|
+
kind: "list",
|
|
70
|
+
operator: value.operator === "in" ? "is in" : "not in",
|
|
71
|
+
items: value.values
|
|
72
|
+
.map((v) => stringifyUnknownValue({ value: v }))
|
|
73
|
+
.toSorted((a, b) => a.localeCompare(b)),
|
|
74
|
+
};
|
|
75
|
+
case "is_empty":
|
|
76
|
+
return { kind: "scalar", operator: "is empty" };
|
|
77
|
+
case "contains":
|
|
78
|
+
case "equals":
|
|
79
|
+
case "does_not_equal":
|
|
80
|
+
case "regex":
|
|
81
|
+
case "starts_with":
|
|
82
|
+
case "ends_with":
|
|
83
|
+
return {
|
|
84
|
+
kind: "scalar",
|
|
85
|
+
operator: OPERATOR_LABELS[value.operator].toLowerCase(),
|
|
86
|
+
value: `"${value.text}"`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (
|
|
91
|
+
value.type === "date" ||
|
|
92
|
+
value.type === "datetime" ||
|
|
93
|
+
value.type === "time"
|
|
94
|
+
) {
|
|
95
|
+
const format =
|
|
96
|
+
value.type === "date"
|
|
97
|
+
? dateToLocalISODate
|
|
98
|
+
: value.type === "time"
|
|
99
|
+
? dateToLocalISOTime
|
|
100
|
+
: (d: Date) => exactDateTime(d, ctx.timezone, ctx.locale);
|
|
101
|
+
switch (value.operator) {
|
|
102
|
+
case "between":
|
|
103
|
+
return {
|
|
104
|
+
kind: "scalar",
|
|
105
|
+
operator: OPERATOR_LABELS.between.toLowerCase(),
|
|
106
|
+
value: `${format(value.min)} - ${format(value.max)}`,
|
|
107
|
+
};
|
|
108
|
+
case "==":
|
|
109
|
+
case "!=":
|
|
110
|
+
case ">":
|
|
111
|
+
case ">=":
|
|
112
|
+
case "<":
|
|
113
|
+
case "<=":
|
|
114
|
+
return {
|
|
115
|
+
kind: "scalar",
|
|
116
|
+
operator: value.operator,
|
|
117
|
+
value: format(value.value),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (value.type === "boolean") {
|
|
122
|
+
switch (value.operator) {
|
|
123
|
+
case "is_true":
|
|
124
|
+
return { kind: "scalar", operator: "is True" };
|
|
125
|
+
case "is_false":
|
|
126
|
+
return { kind: "scalar", operator: "is False" };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
logNever(value);
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|