@marimo-team/islands 0.23.7-dev47 → 0.23.7-dev48
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-CufH8sfF.js → chat-ui-D8ZxPNTR.js} +3 -3
- package/dist/{code-visibility-C4KEMmUK.js → code-visibility-An0P9cL_.js} +1265 -935
- package/dist/{glide-data-editor-BK9s_dqy.js → glide-data-editor-DucgdjRo.js} +1 -1
- package/dist/{html-to-image-DxWM1HVj.js → html-to-image-DaPPaVDP.js} +1 -1
- package/dist/{input-Cc1Vvw9A.js → input-D4kjoQUB.js} +2 -0
- package/dist/main.js +8 -8
- package/dist/{process-output-DBYxXdrN.js → process-output-n0RJTxcC.js} +1 -1
- package/dist/{reveal-component-Dx7r_prC.js → reveal-component-B23qYh6r.js} +3 -3
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/data-table/__tests__/column-header.test.ts +3 -1
- package/src/components/data-table/__tests__/column-header.test.tsx +203 -0
- package/src/components/data-table/__tests__/filter-by-values-picker.test.tsx +112 -0
- package/src/components/data-table/__tests__/filter-pill-editor.test.tsx +175 -0
- package/src/components/data-table/__tests__/filters.test.ts +112 -36
- package/src/components/data-table/column-header.tsx +210 -157
- package/src/components/data-table/filter-by-values-picker.tsx +70 -9
- package/src/components/data-table/filter-pill-editor.tsx +289 -144
- package/src/components/data-table/filter-pills.tsx +49 -8
- package/src/components/data-table/filters.ts +131 -36
- package/src/components/data-table/header-items.tsx +8 -1
- package/src/components/data-table/operator-labels.ts +25 -0
- package/src/components/data-table/regex-input.tsx +61 -0
- package/src/components/ui/combobox.tsx +3 -2
- package/src/components/ui/number-field.tsx +2 -0
- package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +24 -24
- package/src/plugins/impl/data-frames/schema.ts +4 -1
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
import type { Column, Table } from "@tanstack/react-table";
|
|
5
5
|
import { CheckIcon, MinusIcon, Trash2Icon, XIcon } from "lucide-react";
|
|
6
|
-
import { useId, useState } from "react";
|
|
6
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
7
7
|
import type { CalculateTopKRows } from "@/plugins/impl/DataTablePlugin";
|
|
8
|
+
import type { OperatorType } from "@/plugins/impl/data-frames/utils/operators";
|
|
8
9
|
import { Combobox, ComboboxItem } from "../ui/combobox";
|
|
9
10
|
import { Input } from "../ui/input";
|
|
10
11
|
import { NumberField } from "../ui/number-field";
|
|
@@ -17,67 +18,68 @@ import {
|
|
|
17
18
|
} from "../ui/select";
|
|
18
19
|
import { Button } from "../ui/button";
|
|
19
20
|
import { FilterByValuesPicker } from "./filter-by-values-picker";
|
|
20
|
-
import {
|
|
21
|
+
import { RegexInput } from "./regex-input";
|
|
22
|
+
import {
|
|
23
|
+
type ColumnFilterValue,
|
|
24
|
+
Filter,
|
|
25
|
+
MEMBERSHIP_OPS,
|
|
26
|
+
NUMBER_COMPARISON_OPS,
|
|
27
|
+
type NumberComparisonOp,
|
|
28
|
+
NUMBER_OPS,
|
|
29
|
+
TEXT_OPS,
|
|
30
|
+
TEXT_SCALAR_OPS,
|
|
31
|
+
type TextScalarOp,
|
|
32
|
+
} from "./filters";
|
|
33
|
+
import { OPERATOR_LABELS } from "./operator-labels";
|
|
21
34
|
import { Tooltip } from "../ui/tooltip";
|
|
22
35
|
|
|
23
|
-
// Editable filter types in this editor — date/datetime/time are read-only
|
|
24
|
-
// Will add support for rest in next PR
|
|
25
36
|
type EditableFilterType = "number" | "text" | "boolean" | "select";
|
|
26
37
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
| "is_not_null"
|
|
39
|
-
| "in"
|
|
40
|
-
| "not_in";
|
|
41
|
-
|
|
42
|
-
// will be expanded by a follow up PR
|
|
43
|
-
const OPERATORS_BY_TYPE: Record<EditableFilterType, UiOperator[]> = {
|
|
44
|
-
number: ["between", "is_null", "is_not_null"],
|
|
45
|
-
text: ["contains", "is_null", "is_not_null"],
|
|
46
|
-
boolean: ["is_true", "is_false", "is_null", "is_not_null"],
|
|
47
|
-
select: ["in", "not_in"],
|
|
38
|
+
const BOOLEAN_OPS = ["is_true", "is_false", "is_null", "is_not_null"] as const;
|
|
39
|
+
const SELECT_OPS = MEMBERSHIP_OPS;
|
|
40
|
+
|
|
41
|
+
const OPERATORS_BY_TYPE: Record<
|
|
42
|
+
EditableFilterType,
|
|
43
|
+
ReadonlyArray<OperatorType>
|
|
44
|
+
> = {
|
|
45
|
+
number: NUMBER_OPS,
|
|
46
|
+
text: TEXT_OPS,
|
|
47
|
+
boolean: BOOLEAN_OPS,
|
|
48
|
+
select: SELECT_OPS,
|
|
48
49
|
};
|
|
49
50
|
|
|
50
|
-
const DEFAULT_OPERATOR: Record<EditableFilterType,
|
|
51
|
+
const DEFAULT_OPERATOR: Record<EditableFilterType, OperatorType> = {
|
|
51
52
|
number: "between",
|
|
52
53
|
text: "contains",
|
|
53
54
|
boolean: "is_true",
|
|
54
55
|
select: "in",
|
|
55
56
|
};
|
|
56
57
|
|
|
57
|
-
const
|
|
58
|
-
between: "Between",
|
|
59
|
-
contains: "Contains",
|
|
60
|
-
is_true: "Is true",
|
|
61
|
-
is_false: "Is false",
|
|
62
|
-
is_null: "Is null",
|
|
63
|
-
is_not_null: "Is not null",
|
|
64
|
-
in: "Is in",
|
|
65
|
-
not_in: "Not in",
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const OPERATORS_WITHOUT_VALUE = new Set<UiOperator>([
|
|
58
|
+
const OPERATORS_WITHOUT_VALUE = new Set<OperatorType>([
|
|
69
59
|
"is_true",
|
|
70
60
|
"is_false",
|
|
71
61
|
"is_null",
|
|
72
62
|
"is_not_null",
|
|
63
|
+
"is_empty",
|
|
73
64
|
]);
|
|
74
65
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
66
|
+
const NUMBER_COMPARISON_SET: ReadonlySet<OperatorType> = new Set(
|
|
67
|
+
NUMBER_COMPARISON_OPS,
|
|
68
|
+
);
|
|
69
|
+
const TEXT_SCALAR_SET: ReadonlySet<OperatorType> = new Set(TEXT_SCALAR_OPS);
|
|
70
|
+
|
|
71
|
+
const isNumberComparisonOp = (op: OperatorType): op is NumberComparisonOp =>
|
|
72
|
+
NUMBER_COMPARISON_SET.has(op);
|
|
73
|
+
const isTextScalarOp = (op: OperatorType): op is TextScalarOp =>
|
|
74
|
+
TEXT_SCALAR_SET.has(op);
|
|
75
|
+
|
|
76
|
+
type DraftValue =
|
|
77
|
+
| { kind: "between"; min?: number; max?: number }
|
|
78
|
+
| { kind: "single-number"; value?: number }
|
|
79
|
+
| { kind: "single-text"; text?: string }
|
|
80
|
+
| { kind: "multi-text"; values?: string[] }
|
|
81
|
+
| { kind: "options"; options?: unknown[] }
|
|
82
|
+
| { kind: "none" };
|
|
81
83
|
|
|
82
84
|
interface Snapshot {
|
|
83
85
|
columnId: string;
|
|
@@ -92,7 +94,7 @@ interface FilterPillEditorProps<TData> {
|
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
export const FilterPillEditor = <TData,>({
|
|
95
|
-
snapshot,
|
|
97
|
+
snapshot,
|
|
96
98
|
table,
|
|
97
99
|
calculateTopKRows,
|
|
98
100
|
onClose,
|
|
@@ -102,13 +104,13 @@ export const FilterPillEditor = <TData,>({
|
|
|
102
104
|
const valueId = useId();
|
|
103
105
|
|
|
104
106
|
const snapshotType = getEditableType(snapshot.value);
|
|
105
|
-
const snapshotOperator =
|
|
107
|
+
const snapshotOperator = snapshot.value.operator as OperatorType;
|
|
106
108
|
const snapshotDraft = toDraftValue(snapshot.value);
|
|
107
109
|
|
|
108
110
|
const [draftColumnId, setDraftColumnId] = useState<string>(snapshot.columnId);
|
|
109
111
|
const [draftType, setDraftType] = useState<EditableFilterType>(snapshotType);
|
|
110
112
|
const [draftOperator, setDraftOperator] =
|
|
111
|
-
useState<
|
|
113
|
+
useState<OperatorType>(snapshotOperator);
|
|
112
114
|
const [draftValue, setDraftValue] = useState<DraftValue>(snapshotDraft);
|
|
113
115
|
|
|
114
116
|
const editableColumns = table.getAllColumns().filter((c) => {
|
|
@@ -118,18 +120,11 @@ export const FilterPillEditor = <TData,>({
|
|
|
118
120
|
);
|
|
119
121
|
});
|
|
120
122
|
|
|
121
|
-
// if we switch back to pre-edit column+operator
|
|
122
|
-
// restore the original value as well
|
|
123
123
|
const rehydrateIfMatchesSnapshot = (args: {
|
|
124
124
|
id: string;
|
|
125
|
-
|
|
126
|
-
operator: UiOperator;
|
|
125
|
+
operator: OperatorType;
|
|
127
126
|
}) => {
|
|
128
|
-
if (
|
|
129
|
-
args.id === snapshot.columnId &&
|
|
130
|
-
args.type === snapshotType &&
|
|
131
|
-
args.operator === snapshotOperator
|
|
132
|
-
) {
|
|
127
|
+
if (args.id === snapshot.columnId && args.operator === snapshotOperator) {
|
|
133
128
|
setDraftValue(snapshotDraft);
|
|
134
129
|
}
|
|
135
130
|
};
|
|
@@ -147,34 +142,42 @@ export const FilterPillEditor = <TData,>({
|
|
|
147
142
|
nextOperator = DEFAULT_OPERATOR[nextColumnType];
|
|
148
143
|
setDraftType(nextColumnType);
|
|
149
144
|
setDraftOperator(nextOperator);
|
|
150
|
-
setDraftValue(
|
|
145
|
+
setDraftValue(emptyDraftFor(nextColumnType, nextOperator));
|
|
151
146
|
}
|
|
152
147
|
setDraftColumnId(nextColumnId);
|
|
153
148
|
rehydrateIfMatchesSnapshot({
|
|
154
149
|
id: nextColumnId,
|
|
155
|
-
type: nextColumnType,
|
|
156
150
|
operator: nextOperator,
|
|
157
151
|
});
|
|
158
152
|
};
|
|
159
153
|
|
|
160
|
-
const handleOperatorChange = (nextOp:
|
|
154
|
+
const handleOperatorChange = (nextOp: OperatorType) => {
|
|
161
155
|
setDraftOperator(nextOp);
|
|
156
|
+
const nextEmpty = emptyDraftFor(draftType, nextOp);
|
|
157
|
+
if (nextEmpty.kind !== draftValue.kind) {
|
|
158
|
+
setDraftValue(nextEmpty);
|
|
159
|
+
}
|
|
162
160
|
rehydrateIfMatchesSnapshot({
|
|
163
161
|
id: draftColumnId,
|
|
164
|
-
type: draftType,
|
|
165
162
|
operator: nextOp,
|
|
166
163
|
});
|
|
167
164
|
};
|
|
168
165
|
|
|
166
|
+
const pendingValue = buildFilterValue({
|
|
167
|
+
type: draftType,
|
|
168
|
+
operator: draftOperator,
|
|
169
|
+
draft: draftValue,
|
|
170
|
+
});
|
|
171
|
+
const applyDisabled = pendingValue === undefined;
|
|
172
|
+
const applyTooltip = applyDisabled
|
|
173
|
+
? getMissingValueMessage(draftType, draftOperator)
|
|
174
|
+
: "Apply filter";
|
|
175
|
+
|
|
169
176
|
const handleApply = () => {
|
|
170
|
-
|
|
171
|
-
type: draftType,
|
|
172
|
-
operator: draftOperator,
|
|
173
|
-
draft: draftValue,
|
|
174
|
-
});
|
|
175
|
-
if (!value) {
|
|
177
|
+
if (!pendingValue) {
|
|
176
178
|
return;
|
|
177
179
|
}
|
|
180
|
+
const value = pendingValue;
|
|
178
181
|
table.setColumnFilters((filters) => {
|
|
179
182
|
const dropIds = new Set([snapshot.columnId, draftColumnId]);
|
|
180
183
|
const filtered = filters.filter((f) => !dropIds.has(f.id));
|
|
@@ -191,9 +194,29 @@ export const FilterPillEditor = <TData,>({
|
|
|
191
194
|
};
|
|
192
195
|
|
|
193
196
|
const showValueSlot = !OPERATORS_WITHOUT_VALUE.has(draftOperator);
|
|
197
|
+
const operatorOptions = OPERATORS_BY_TYPE[draftType];
|
|
198
|
+
|
|
199
|
+
const valueSlotRef = useRef<HTMLDivElement>(null);
|
|
200
|
+
const operatorTriggerRef = useRef<HTMLButtonElement>(null);
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
const firstInput = valueSlotRef.current?.querySelector<HTMLElement>(
|
|
203
|
+
'input, [role="combobox"], button',
|
|
204
|
+
);
|
|
205
|
+
if (firstInput) {
|
|
206
|
+
firstInput.focus();
|
|
207
|
+
} else {
|
|
208
|
+
operatorTriggerRef.current?.focus();
|
|
209
|
+
}
|
|
210
|
+
}, [draftType, draftOperator]);
|
|
194
211
|
|
|
195
212
|
return (
|
|
196
|
-
<
|
|
213
|
+
<form
|
|
214
|
+
className="flex flex-row gap-4 items-end p-3"
|
|
215
|
+
onSubmit={(e) => {
|
|
216
|
+
e.preventDefault();
|
|
217
|
+
handleApply();
|
|
218
|
+
}}
|
|
219
|
+
>
|
|
197
220
|
<div className="flex flex-col gap-1">
|
|
198
221
|
<label className="text-xs text-muted-foreground" htmlFor={columnId}>
|
|
199
222
|
Column
|
|
@@ -218,14 +241,19 @@ export const FilterPillEditor = <TData,>({
|
|
|
218
241
|
Operator
|
|
219
242
|
</label>
|
|
220
243
|
<Select
|
|
244
|
+
key={draftType}
|
|
221
245
|
value={draftOperator}
|
|
222
|
-
onValueChange={(v) => handleOperatorChange(v as
|
|
246
|
+
onValueChange={(v) => handleOperatorChange(v as OperatorType)}
|
|
223
247
|
>
|
|
224
|
-
<SelectTrigger
|
|
248
|
+
<SelectTrigger
|
|
249
|
+
ref={operatorTriggerRef}
|
|
250
|
+
id={operatorId}
|
|
251
|
+
className="h-6 mb-1 bg-transparent"
|
|
252
|
+
>
|
|
225
253
|
<SelectValue />
|
|
226
254
|
</SelectTrigger>
|
|
227
255
|
<SelectContent>
|
|
228
|
-
{
|
|
256
|
+
{operatorOptions.map((op) => (
|
|
229
257
|
<SelectItem key={op} value={op}>
|
|
230
258
|
{OPERATOR_LABELS[op]}
|
|
231
259
|
</SelectItem>
|
|
@@ -234,13 +262,14 @@ export const FilterPillEditor = <TData,>({
|
|
|
234
262
|
</Select>
|
|
235
263
|
</div>
|
|
236
264
|
{showValueSlot && (
|
|
237
|
-
<div className="flex flex-col gap-1">
|
|
265
|
+
<div ref={valueSlotRef} className="flex flex-col gap-1">
|
|
238
266
|
<label htmlFor={valueId} className="text-xs text-muted-foreground">
|
|
239
267
|
Value
|
|
240
268
|
</label>
|
|
241
269
|
<ValueSlot
|
|
242
270
|
id={valueId}
|
|
243
271
|
type={draftType}
|
|
272
|
+
operator={draftOperator}
|
|
244
273
|
value={draftValue}
|
|
245
274
|
onChange={setDraftValue}
|
|
246
275
|
column={table.getColumn(draftColumnId) ?? null}
|
|
@@ -249,17 +278,19 @@ export const FilterPillEditor = <TData,>({
|
|
|
249
278
|
</div>
|
|
250
279
|
)}
|
|
251
280
|
<div className="flex gap-1 mb-1">
|
|
252
|
-
<Tooltip content=
|
|
253
|
-
<
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
281
|
+
<Tooltip content={applyTooltip}>
|
|
282
|
+
<span className="inline-flex">
|
|
283
|
+
<Button
|
|
284
|
+
type="submit"
|
|
285
|
+
size="icon"
|
|
286
|
+
variant="ghost"
|
|
287
|
+
disabled={applyDisabled}
|
|
288
|
+
className="rounded-full text-primary hover:text-primary hover:bg-primary/10"
|
|
289
|
+
aria-label="Apply filter"
|
|
290
|
+
>
|
|
291
|
+
<CheckIcon className="h-3.5 w-3.5" aria-hidden={true} />
|
|
292
|
+
</Button>
|
|
293
|
+
</span>
|
|
263
294
|
</Tooltip>
|
|
264
295
|
<Tooltip content="Close without saving">
|
|
265
296
|
<Button
|
|
@@ -286,13 +317,14 @@ export const FilterPillEditor = <TData,>({
|
|
|
286
317
|
</Button>
|
|
287
318
|
</Tooltip>
|
|
288
319
|
</div>
|
|
289
|
-
</
|
|
320
|
+
</form>
|
|
290
321
|
);
|
|
291
322
|
};
|
|
292
323
|
|
|
293
324
|
interface ValueSlotProps<TData, TValue> {
|
|
294
325
|
id?: string;
|
|
295
326
|
type: EditableFilterType;
|
|
327
|
+
operator: OperatorType;
|
|
296
328
|
value: DraftValue;
|
|
297
329
|
onChange: (next: DraftValue) => void;
|
|
298
330
|
column: Column<TData, TValue> | null;
|
|
@@ -302,26 +334,28 @@ interface ValueSlotProps<TData, TValue> {
|
|
|
302
334
|
const ValueSlot = <TData, TValue>({
|
|
303
335
|
id,
|
|
304
336
|
type,
|
|
337
|
+
operator,
|
|
305
338
|
value,
|
|
306
339
|
onChange,
|
|
307
340
|
column,
|
|
308
341
|
calculateTopKRows,
|
|
309
342
|
}: ValueSlotProps<TData, TValue>) => {
|
|
310
|
-
if (type === "number") {
|
|
343
|
+
if (type === "number" && operator === "between") {
|
|
344
|
+
const v = value.kind === "between" ? value : { kind: "between" as const };
|
|
311
345
|
return (
|
|
312
|
-
<div className="flex gap-1 items-center w-
|
|
346
|
+
<div className="flex gap-1 items-center w-44">
|
|
313
347
|
<NumberField
|
|
314
348
|
id={id}
|
|
315
|
-
value={
|
|
316
|
-
onChange={(
|
|
349
|
+
value={v.min}
|
|
350
|
+
onChange={(n) => onChange({ kind: "between", min: n, max: v.max })}
|
|
317
351
|
aria-label="min"
|
|
318
352
|
placeholder="min"
|
|
319
353
|
className="border-input flex-1 min-w-0"
|
|
320
354
|
/>
|
|
321
355
|
<MinusIcon className="h-5 w-5 text-muted-foreground shrink-0" />
|
|
322
356
|
<NumberField
|
|
323
|
-
value={
|
|
324
|
-
onChange={(
|
|
357
|
+
value={v.max}
|
|
358
|
+
onChange={(n) => onChange({ kind: "between", min: v.min, max: n })}
|
|
325
359
|
aria-label="max"
|
|
326
360
|
placeholder="max"
|
|
327
361
|
className="border-input flex-1 min-w-0"
|
|
@@ -329,26 +363,78 @@ const ValueSlot = <TData, TValue>({
|
|
|
329
363
|
</div>
|
|
330
364
|
);
|
|
331
365
|
}
|
|
332
|
-
if (type === "
|
|
366
|
+
if (type === "number" && isNumberComparisonOp(operator)) {
|
|
367
|
+
const v =
|
|
368
|
+
value.kind === "single-number"
|
|
369
|
+
? value
|
|
370
|
+
: { kind: "single-number" as const };
|
|
371
|
+
return (
|
|
372
|
+
<NumberField
|
|
373
|
+
id={id}
|
|
374
|
+
value={v.value}
|
|
375
|
+
onChange={(n) => onChange({ kind: "single-number", value: n })}
|
|
376
|
+
aria-label="value"
|
|
377
|
+
placeholder="value"
|
|
378
|
+
className="border-input w-24 min-w-0"
|
|
379
|
+
/>
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
if (
|
|
383
|
+
type === "text" &&
|
|
384
|
+
(operator === "in" || operator === "not_in") &&
|
|
385
|
+
column
|
|
386
|
+
) {
|
|
387
|
+
const v =
|
|
388
|
+
value.kind === "multi-text" ? value : { kind: "multi-text" as const };
|
|
389
|
+
return (
|
|
390
|
+
<div className="w-48">
|
|
391
|
+
<FilterByValuesPicker
|
|
392
|
+
column={column}
|
|
393
|
+
calculateTopKRows={calculateTopKRows}
|
|
394
|
+
chosenValues={v.values ?? []}
|
|
395
|
+
onChange={(next) =>
|
|
396
|
+
onChange({ kind: "multi-text", values: next.map(String) })
|
|
397
|
+
}
|
|
398
|
+
creatable={true}
|
|
399
|
+
/>
|
|
400
|
+
</div>
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
if (type === "text" && isTextScalarOp(operator)) {
|
|
404
|
+
const v =
|
|
405
|
+
value.kind === "single-text" ? value : { kind: "single-text" as const };
|
|
406
|
+
if (operator === "regex") {
|
|
407
|
+
return (
|
|
408
|
+
<RegexInput
|
|
409
|
+
id={id}
|
|
410
|
+
value={v.text ?? ""}
|
|
411
|
+
onChange={(text) => onChange({ kind: "single-text", text })}
|
|
412
|
+
className="w-40"
|
|
413
|
+
/>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
333
416
|
return (
|
|
334
417
|
<Input
|
|
335
418
|
id={id}
|
|
336
419
|
type="text"
|
|
337
|
-
value={
|
|
338
|
-
onChange={(e) =>
|
|
420
|
+
value={v.text ?? ""}
|
|
421
|
+
onChange={(e) =>
|
|
422
|
+
onChange({ kind: "single-text", text: e.target.value })
|
|
423
|
+
}
|
|
339
424
|
placeholder="Text…"
|
|
340
|
-
className="border-input min-w-0"
|
|
425
|
+
className="border-input w-40 min-w-0"
|
|
341
426
|
/>
|
|
342
427
|
);
|
|
343
428
|
}
|
|
344
429
|
if (type === "select" && column) {
|
|
430
|
+
const v = value.kind === "options" ? value : { kind: "options" as const };
|
|
345
431
|
return (
|
|
346
432
|
<div className="flex w-48">
|
|
347
433
|
<FilterByValuesPicker
|
|
348
434
|
column={column}
|
|
349
435
|
calculateTopKRows={calculateTopKRows}
|
|
350
|
-
chosenValues={
|
|
351
|
-
onChange={(values) => onChange({
|
|
436
|
+
chosenValues={v.options ?? []}
|
|
437
|
+
onChange={(values) => onChange({ kind: "options", options: values })}
|
|
352
438
|
/>
|
|
353
439
|
</div>
|
|
354
440
|
);
|
|
@@ -369,43 +455,74 @@ function getEditableType(value: ColumnFilterValue): EditableFilterType {
|
|
|
369
455
|
if (value.type === "select") {
|
|
370
456
|
return "select";
|
|
371
457
|
}
|
|
372
|
-
// date/datetime/time fall back to text; callers should guard. supported in future
|
|
373
458
|
return "text";
|
|
374
459
|
}
|
|
375
460
|
|
|
376
|
-
function
|
|
377
|
-
if (value.operator === "is_null") {
|
|
378
|
-
return "is_null";
|
|
379
|
-
}
|
|
380
|
-
if (value.operator === "is_not_null") {
|
|
381
|
-
return "is_not_null";
|
|
382
|
-
}
|
|
461
|
+
function toDraftValue(value: ColumnFilterValue): DraftValue {
|
|
383
462
|
if (value.type === "number") {
|
|
384
|
-
|
|
463
|
+
switch (value.operator) {
|
|
464
|
+
case "between":
|
|
465
|
+
return { kind: "between", min: value.min, max: value.max };
|
|
466
|
+
case "is_null":
|
|
467
|
+
case "is_not_null":
|
|
468
|
+
return { kind: "none" };
|
|
469
|
+
default:
|
|
470
|
+
return { kind: "single-number", value: value.value };
|
|
471
|
+
}
|
|
385
472
|
}
|
|
386
473
|
if (value.type === "text") {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
474
|
+
switch (value.operator) {
|
|
475
|
+
case "in":
|
|
476
|
+
case "not_in":
|
|
477
|
+
return { kind: "multi-text", values: [...value.values] };
|
|
478
|
+
case "is_null":
|
|
479
|
+
case "is_not_null":
|
|
480
|
+
case "is_empty":
|
|
481
|
+
return { kind: "none" };
|
|
482
|
+
default:
|
|
483
|
+
return { kind: "single-text", text: value.text };
|
|
484
|
+
}
|
|
391
485
|
}
|
|
392
486
|
if (value.type === "select") {
|
|
393
|
-
return
|
|
487
|
+
return { kind: "options", options: [...value.options] };
|
|
394
488
|
}
|
|
395
|
-
return "
|
|
489
|
+
return { kind: "none" };
|
|
396
490
|
}
|
|
397
491
|
|
|
398
|
-
function
|
|
399
|
-
|
|
400
|
-
|
|
492
|
+
function emptyDraftFor(
|
|
493
|
+
type: EditableFilterType,
|
|
494
|
+
operator: OperatorType,
|
|
495
|
+
): DraftValue {
|
|
496
|
+
if (OPERATORS_WITHOUT_VALUE.has(operator)) {
|
|
497
|
+
return { kind: "none" };
|
|
401
498
|
}
|
|
402
|
-
if (
|
|
403
|
-
return
|
|
499
|
+
if (type === "number") {
|
|
500
|
+
return operator === "between"
|
|
501
|
+
? { kind: "between" }
|
|
502
|
+
: { kind: "single-number" };
|
|
404
503
|
}
|
|
405
|
-
if (
|
|
406
|
-
return
|
|
504
|
+
if (type === "text") {
|
|
505
|
+
return operator === "in" || operator === "not_in"
|
|
506
|
+
? { kind: "multi-text", values: [] }
|
|
507
|
+
: { kind: "single-text" };
|
|
407
508
|
}
|
|
408
|
-
|
|
509
|
+
if (type === "select") {
|
|
510
|
+
return { kind: "options", options: [] };
|
|
511
|
+
}
|
|
512
|
+
return { kind: "none" };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function getMissingValueMessage(
|
|
516
|
+
type: EditableFilterType,
|
|
517
|
+
operator: OperatorType,
|
|
518
|
+
): string {
|
|
519
|
+
if (type === "number" && operator === "between") {
|
|
520
|
+
return "Min and max are required";
|
|
521
|
+
}
|
|
522
|
+
if (type === "text" && (operator === "in" || operator === "not_in")) {
|
|
523
|
+
return "Pick at least one value";
|
|
524
|
+
}
|
|
525
|
+
return "Value is required";
|
|
409
526
|
}
|
|
410
527
|
|
|
411
528
|
function buildFilterValue({
|
|
@@ -414,51 +531,79 @@ function buildFilterValue({
|
|
|
414
531
|
draft,
|
|
415
532
|
}: {
|
|
416
533
|
type: EditableFilterType;
|
|
417
|
-
operator:
|
|
534
|
+
operator: OperatorType;
|
|
418
535
|
draft: DraftValue;
|
|
419
536
|
}): ColumnFilterValue | undefined {
|
|
420
|
-
if (
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
return Filter.number({ operator: op });
|
|
537
|
+
if (type === "number") {
|
|
538
|
+
if (operator === "is_null" || operator === "is_not_null") {
|
|
539
|
+
return Filter.number({ operator });
|
|
424
540
|
}
|
|
425
|
-
if (
|
|
426
|
-
|
|
541
|
+
if (operator === "between") {
|
|
542
|
+
if (
|
|
543
|
+
draft.kind !== "between" ||
|
|
544
|
+
draft.min === undefined ||
|
|
545
|
+
draft.max === undefined
|
|
546
|
+
) {
|
|
547
|
+
return undefined;
|
|
548
|
+
}
|
|
549
|
+
return Filter.number({
|
|
550
|
+
operator: "between",
|
|
551
|
+
min: draft.min,
|
|
552
|
+
max: draft.max,
|
|
553
|
+
});
|
|
427
554
|
}
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
if (type === "number") {
|
|
431
|
-
if (draft.min === undefined && draft.max === undefined) {
|
|
555
|
+
if (!isNumberComparisonOp(operator)) {
|
|
432
556
|
return undefined;
|
|
433
557
|
}
|
|
434
|
-
|
|
558
|
+
if (draft.kind !== "single-number" || draft.value === undefined) {
|
|
559
|
+
return undefined;
|
|
560
|
+
}
|
|
561
|
+
return Filter.number({ operator, value: draft.value });
|
|
435
562
|
}
|
|
436
563
|
if (type === "text") {
|
|
437
|
-
if (
|
|
564
|
+
if (
|
|
565
|
+
operator === "is_null" ||
|
|
566
|
+
operator === "is_not_null" ||
|
|
567
|
+
operator === "is_empty"
|
|
568
|
+
) {
|
|
569
|
+
return Filter.text({ operator });
|
|
570
|
+
}
|
|
571
|
+
if (operator === "in" || operator === "not_in") {
|
|
572
|
+
if (
|
|
573
|
+
draft.kind !== "multi-text" ||
|
|
574
|
+
!draft.values ||
|
|
575
|
+
draft.values.length === 0
|
|
576
|
+
) {
|
|
577
|
+
return undefined;
|
|
578
|
+
}
|
|
579
|
+
return Filter.text({ operator, values: draft.values });
|
|
580
|
+
}
|
|
581
|
+
if (!isTextScalarOp(operator)) {
|
|
438
582
|
return undefined;
|
|
439
583
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
});
|
|
584
|
+
if (draft.kind !== "single-text" || !draft.text) {
|
|
585
|
+
return undefined;
|
|
586
|
+
}
|
|
587
|
+
return Filter.text({ operator, text: draft.text });
|
|
444
588
|
}
|
|
445
589
|
if (type === "boolean") {
|
|
446
590
|
if (operator === "is_true") {
|
|
447
|
-
return Filter.boolean({
|
|
448
|
-
value: true,
|
|
449
|
-
operator: "is_true",
|
|
450
|
-
});
|
|
591
|
+
return Filter.boolean({ value: true, operator: "is_true" });
|
|
451
592
|
}
|
|
452
593
|
if (operator === "is_false") {
|
|
453
|
-
return Filter.boolean({
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
});
|
|
594
|
+
return Filter.boolean({ value: false, operator: "is_false" });
|
|
595
|
+
}
|
|
596
|
+
if (operator === "is_null" || operator === "is_not_null") {
|
|
597
|
+
return Filter.boolean({ operator });
|
|
457
598
|
}
|
|
458
599
|
return undefined;
|
|
459
600
|
}
|
|
460
601
|
if (type === "select") {
|
|
461
|
-
if (
|
|
602
|
+
if (
|
|
603
|
+
draft.kind !== "options" ||
|
|
604
|
+
!draft.options ||
|
|
605
|
+
draft.options.length === 0
|
|
606
|
+
) {
|
|
462
607
|
return undefined;
|
|
463
608
|
}
|
|
464
609
|
return Filter.select({
|