@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.
Files changed (27) hide show
  1. package/dist/{chat-ui-CufH8sfF.js → chat-ui-D8ZxPNTR.js} +3 -3
  2. package/dist/{code-visibility-C4KEMmUK.js → code-visibility-An0P9cL_.js} +1265 -935
  3. package/dist/{glide-data-editor-BK9s_dqy.js → glide-data-editor-DucgdjRo.js} +1 -1
  4. package/dist/{html-to-image-DxWM1HVj.js → html-to-image-DaPPaVDP.js} +1 -1
  5. package/dist/{input-Cc1Vvw9A.js → input-D4kjoQUB.js} +2 -0
  6. package/dist/main.js +8 -8
  7. package/dist/{process-output-DBYxXdrN.js → process-output-n0RJTxcC.js} +1 -1
  8. package/dist/{reveal-component-Dx7r_prC.js → reveal-component-B23qYh6r.js} +3 -3
  9. package/dist/style.css +1 -1
  10. package/package.json +1 -1
  11. package/src/components/data-table/__tests__/column-header.test.ts +3 -1
  12. package/src/components/data-table/__tests__/column-header.test.tsx +203 -0
  13. package/src/components/data-table/__tests__/filter-by-values-picker.test.tsx +112 -0
  14. package/src/components/data-table/__tests__/filter-pill-editor.test.tsx +175 -0
  15. package/src/components/data-table/__tests__/filters.test.ts +112 -36
  16. package/src/components/data-table/column-header.tsx +210 -157
  17. package/src/components/data-table/filter-by-values-picker.tsx +70 -9
  18. package/src/components/data-table/filter-pill-editor.tsx +289 -144
  19. package/src/components/data-table/filter-pills.tsx +49 -8
  20. package/src/components/data-table/filters.ts +131 -36
  21. package/src/components/data-table/header-items.tsx +8 -1
  22. package/src/components/data-table/operator-labels.ts +25 -0
  23. package/src/components/data-table/regex-input.tsx +61 -0
  24. package/src/components/ui/combobox.tsx +3 -2
  25. package/src/components/ui/number-field.tsx +2 -0
  26. package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +24 -24
  27. 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 { type ColumnFilterValue, Filter } from "./filters";
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
- // UI-level operator for the operator dropdown. Today the committed filter
28
- // value does not carry this operator for number ranges — ranges are
29
- // converted to `>=` / `<=` condition pairs at the RPC boundary
30
- // (`filterToFilterCondition`). The follow-up PR splits UI operators into
31
- // distinct `<`, `>`, `between` variants and routes them through as-is.
32
- type UiOperator =
33
- | "between"
34
- | "contains"
35
- | "is_true"
36
- | "is_false"
37
- | "is_null"
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, UiOperator> = {
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 OPERATOR_LABELS: Record<UiOperator, string> = {
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
- interface DraftValue {
76
- min?: number;
77
- max?: number;
78
- text?: string;
79
- options?: unknown[];
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, // current state of filter pre-edit
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 = getUiOperator(snapshot.value);
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<UiOperator>(snapshotOperator);
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
- type: EditableFilterType;
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: UiOperator) => {
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
- const value = buildFilterValue({
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
- <div className="flex flex-row gap-4 items-end p-3">
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 UiOperator)}
246
+ onValueChange={(v) => handleOperatorChange(v as OperatorType)}
223
247
  >
224
- <SelectTrigger id={operatorId} className="h-6 mb-1 bg-transparent">
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
- {OPERATORS_BY_TYPE[draftType].map((op) => (
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="Apply filter">
253
- <Button
254
- type="button"
255
- size="icon"
256
- variant="ghost"
257
- className="rounded-full text-primary hover:text-primary hover:bg-primary/10"
258
- onClick={handleApply}
259
- aria-label="Apply filter"
260
- >
261
- <CheckIcon className="h-3.5 w-3.5" aria-hidden={true} />
262
- </Button>
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
- </div>
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-48">
346
+ <div className="flex gap-1 items-center w-44">
313
347
  <NumberField
314
348
  id={id}
315
- value={value.min}
316
- onChange={(v) => onChange({ ...value, min: v })}
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={value.max}
324
- onChange={(v) => onChange({ ...value, max: v })}
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 === "text") {
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={value.text ?? ""}
338
- onChange={(e) => onChange({ ...value, text: e.target.value })}
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={value.options ?? []}
351
- onChange={(values) => onChange({ ...value, options: values })}
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 getUiOperator(value: ColumnFilterValue): UiOperator {
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
- return "between";
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
- return "contains";
388
- }
389
- if (value.type === "boolean") {
390
- return value.value ? "is_true" : "is_false";
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 value.operator === "not_in" ? "not_in" : "in";
487
+ return { kind: "options", options: [...value.options] };
394
488
  }
395
- return "contains";
489
+ return { kind: "none" };
396
490
  }
397
491
 
398
- function toDraftValue(value: ColumnFilterValue): DraftValue {
399
- if (value.type === "number") {
400
- return { min: value.min, max: value.max };
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 (value.type === "text") {
403
- return { text: value.text };
499
+ if (type === "number") {
500
+ return operator === "between"
501
+ ? { kind: "between" }
502
+ : { kind: "single-number" };
404
503
  }
405
- if (value.type === "select") {
406
- return { options: [...value.options] };
504
+ if (type === "text") {
505
+ return operator === "in" || operator === "not_in"
506
+ ? { kind: "multi-text", values: [] }
507
+ : { kind: "single-text" };
407
508
  }
408
- return {};
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: UiOperator;
534
+ operator: OperatorType;
418
535
  draft: DraftValue;
419
536
  }): ColumnFilterValue | undefined {
420
- if (operator === "is_null" || operator === "is_not_null") {
421
- const op = operator;
422
- if (type === "number") {
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 (type === "boolean") {
426
- return Filter.boolean({ operator: op });
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
- return Filter.text({ operator: op });
429
- }
430
- if (type === "number") {
431
- if (draft.min === undefined && draft.max === undefined) {
555
+ if (!isNumberComparisonOp(operator)) {
432
556
  return undefined;
433
557
  }
434
- return Filter.number({ min: draft.min, max: draft.max });
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 (!draft.text) {
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
- return Filter.text({
441
- text: draft.text,
442
- operator: "contains",
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
- value: false,
455
- operator: "is_false",
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 (!draft.options || draft.options.length === 0) {
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({