@marimo-team/islands 0.23.7-dev9 → 0.23.7

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 (153) hide show
  1. package/dist/{ConnectedDataExplorerComponent-DnRhpPMJ.js → ConnectedDataExplorerComponent-2lBNiUv6.js} +13 -13
  2. package/dist/{ErrorBoundary-Da4UeYxT.js → ErrorBoundary-D3wrPNma.js} +1 -1
  3. package/dist/{any-language-editor-DDubl8YH.js → any-language-editor-VWs_7v27.js} +5 -5
  4. package/dist/assets/__vite-browser-external-CAdMKBac.js +1 -0
  5. package/dist/assets/worker-CpBbwbQo.js +73 -0
  6. package/dist/{button-CA5pI2YF.js → button-Dj4BTre0.js} +5 -0
  7. package/dist/{capabilities-6laDasij.js → capabilities-C9rrYCzf.js} +1 -1
  8. package/dist/{chat-ui-BmWZZ3mE.js → chat-ui-D3XBept8.js} +625 -233
  9. package/dist/{check-CFM2mVDr.js → check-BcUIXnUT.js} +1 -1
  10. package/dist/{code-visibility-CRHzv49w.js → code-visibility-sKGUbHmr.js} +11480 -1992
  11. package/dist/{copy-TGGAUEWp.js → copy-DLf4aN7I.js} +2 -2
  12. package/dist/{dist-ESg7xyoD.js → dist-D3ZI9nhS.js} +2 -2
  13. package/dist/{error-banner-DnBPzEWg.js → error-banner-CVkfBUT3.js} +2 -2
  14. package/dist/{esm-Dd1z1auZ.js → esm-CWp0KQeK.js} +1 -1
  15. package/dist/{extends-CzJgxo2J.js → extends-vAi97cpa.js} +4 -4
  16. package/dist/{formats-CgaK7Gmx.js → formats-Dsy9kkZu.js} +3 -3
  17. package/dist/{glide-data-editor-B-3A3G02.js → glide-data-editor-DucgdjRo.js} +9 -9
  18. package/dist/{html-to-image-BwZL1Pkk.js → html-to-image-CpggM7u1.js} +2667 -2408
  19. package/dist/{input-BAOe64zx.js → input-D4kjoQUB.js} +8 -6
  20. package/dist/{label-BCWi-Oqu.js → label-BLqV33b1.js} +2 -2
  21. package/dist/{loader-BvW0-YWZ.js → loader-Dr8Qem8p.js} +1 -1
  22. package/dist/main.js +1697 -10282
  23. package/dist/{mermaid-cXSZ1pfD.js → mermaid-DO-Daq7u.js} +5 -5
  24. package/dist/{process-output-lpVrk7d5.js → process-output-X8TR20AK.js} +3 -3
  25. package/dist/reveal-component-BBAxPTso.js +7447 -0
  26. package/dist/{spec-DSIuqd3f.js → spec-hVaaZsY5.js} +4 -4
  27. package/dist/{strings-B_FOH6eV.js → strings-BiIhGaI8.js} +4 -4
  28. package/dist/style.css +1 -1
  29. package/dist/{swiper-component-BHs0PWwp.js → swiper-component-DlD2GU2g.js} +2 -2
  30. package/dist/{toDate-CHtl9vts.js → toDate-CIpC_34u.js} +33 -20
  31. package/dist/{tooltip-B0mtKTXm.js → tooltip-DRaMBu06.js} +3 -3
  32. package/dist/{types-DBtDeUKD.js → types-Dzuoc3LN.js} +1 -1
  33. package/dist/{useAsyncData-B6hCGywC.js → useAsyncData-C56Khv_R.js} +1 -1
  34. package/dist/{useDateFormatter-B3mCQMP3.js → useDateFormatter-B_9k85Ex.js} +2 -2
  35. package/dist/{useDeepCompareMemoize-CmwDuYUH.js → useDeepCompareMemoize-Dt98v2ua.js} +1 -1
  36. package/dist/{useIframeCapabilities-DbdLoEDm.js → useIframeCapabilities-BkYHTrss.js} +1 -1
  37. package/dist/{useLifecycle-CjMjllqy.js → useLifecycle-BF6-z62y.js} +3 -3
  38. package/dist/{useTheme-CByZUW0p.js → useTheme-DykuNHR2.js} +2 -2
  39. package/dist/{vega-component-C2BYPkfd.js → vega-component-cSdqoAxe.js} +10 -10
  40. package/dist/{zod-BxdsqRPd.js → zod-BWkcDORu.js} +1 -1
  41. package/package.json +3 -3
  42. package/src/components/chat/chat-components.tsx +47 -0
  43. package/src/components/chat/chat-display.tsx +41 -7
  44. package/src/components/chat/chat-panel.tsx +37 -10
  45. package/src/components/chat/chat-utils.ts +42 -20
  46. package/src/components/chat/reasoning-accordion.tsx +14 -3
  47. package/src/components/chat/tool-call/shared.ts +13 -0
  48. package/src/components/chat/tool-call/tool-approval-card.tsx +62 -0
  49. package/src/components/chat/tool-call/tool-args.tsx +26 -0
  50. package/src/components/chat/tool-call/tool-call-view.tsx +99 -0
  51. package/src/components/chat/tool-call/tool-error-card.tsx +81 -0
  52. package/src/components/chat/tool-call/tool-history-row.tsx +153 -0
  53. package/src/components/chat/tool-call/tool-result.tsx +101 -0
  54. package/src/components/data-table/__tests__/column-header.test.ts +3 -1
  55. package/src/components/data-table/__tests__/column-header.test.tsx +308 -0
  56. package/src/components/data-table/__tests__/filter-by-values-picker.test.tsx +112 -0
  57. package/src/components/data-table/__tests__/filter-pill-editor.test.tsx +261 -0
  58. package/src/components/data-table/__tests__/filters.test.ts +196 -49
  59. package/src/components/data-table/charts/components/form-fields.tsx +1 -0
  60. package/src/components/data-table/column-header.tsx +349 -170
  61. package/src/components/data-table/date-filter-inputs.tsx +325 -0
  62. package/src/components/data-table/filter-by-values-picker.tsx +70 -9
  63. package/src/components/data-table/filter-pill-editor.tsx +410 -156
  64. package/src/components/data-table/filter-pills.tsx +69 -54
  65. package/src/components/data-table/filters.ts +218 -101
  66. package/src/components/data-table/header-items.tsx +8 -1
  67. package/src/components/data-table/operator-labels.ts +25 -0
  68. package/src/components/data-table/regex-input.tsx +61 -0
  69. package/src/components/dependency-graph/minimap-content.tsx +14 -3
  70. package/src/components/editor/actions/pair-with-agent-modal.tsx +140 -49
  71. package/src/components/editor/actions/useNotebookActions.tsx +3 -1
  72. package/src/components/editor/app-container.tsx +7 -1
  73. package/src/components/editor/chrome/panels/context-aware-panel/context-aware-panel.tsx +10 -2
  74. package/src/components/editor/chrome/wrapper/app-chrome.tsx +1 -0
  75. package/src/components/editor/chrome/wrapper/footer-items/backend-status.tsx +1 -1
  76. package/src/components/editor/chrome/wrapper/footer.tsx +4 -1
  77. package/src/components/editor/chrome/wrapper/panels.tsx +4 -1
  78. package/src/components/editor/chrome/wrapper/sidebar.tsx +4 -1
  79. package/src/components/editor/controls/Controls.tsx +11 -3
  80. package/src/components/editor/file-tree/file-explorer.tsx +12 -2
  81. package/src/components/editor/header/__tests__/status.test.tsx +108 -0
  82. package/src/components/editor/header/status.tsx +44 -10
  83. package/src/components/editor/navigation/__tests__/clipboard.test.ts +106 -0
  84. package/src/components/editor/navigation/__tests__/navigation.test.ts +70 -0
  85. package/src/components/editor/navigation/clipboard.ts +99 -25
  86. package/src/components/editor/navigation/navigation.ts +15 -1
  87. package/src/components/editor/notebook-cell.tsx +5 -0
  88. package/src/components/editor/output/console/ConsoleOutput.tsx +23 -5
  89. package/src/components/editor/output/console/__tests__/ConsoleOutput.test.tsx +114 -0
  90. package/src/components/editor/renderers/slides-layout/__tests__/compute-slide-cells.test.ts +5 -4
  91. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +55 -15
  92. package/src/components/editor/renderers/slides-layout/plugin.tsx +8 -25
  93. package/src/components/editor/renderers/slides-layout/slides-layout.tsx +19 -6
  94. package/src/components/editor/renderers/slides-layout/types.ts +40 -31
  95. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +1 -0
  96. package/src/components/home/components.tsx +6 -0
  97. package/src/components/pages/run-page.tsx +4 -1
  98. package/src/components/scratchpad/scratchpad.tsx +1 -0
  99. package/src/components/slides/__tests__/slide-notes.test.ts +131 -0
  100. package/src/components/slides/reveal-component.tsx +252 -147
  101. package/src/components/slides/slide-notes-editor.tsx +127 -0
  102. package/src/components/slides/slide-notes.ts +64 -0
  103. package/src/components/slides/slides.css +14 -0
  104. package/src/components/ui/combobox.tsx +24 -5
  105. package/src/components/ui/number-field.tsx +2 -0
  106. package/src/core/ai/tools/__tests__/registry.test.ts +10 -12
  107. package/src/core/ai/tools/registry.ts +9 -5
  108. package/src/core/cells/__tests__/cells.test.ts +187 -0
  109. package/src/core/cells/__tests__/pending-cut-service.test.tsx +123 -0
  110. package/src/core/cells/cells.ts +102 -17
  111. package/src/core/cells/document-changes.ts +6 -1
  112. package/src/core/cells/pending-cut-service.ts +55 -0
  113. package/src/core/cells/utils.ts +11 -0
  114. package/src/core/codemirror/cells/extensions.ts +10 -0
  115. package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +152 -0
  116. package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +99 -0
  117. package/src/core/codemirror/go-to-definition/commands.ts +382 -22
  118. package/src/core/codemirror/go-to-definition/utils.ts +23 -5
  119. package/src/core/edit-app.tsx +3 -2
  120. package/src/core/hotkeys/hotkeys.ts +5 -0
  121. package/src/core/islands/worker/worker.tsx +3 -2
  122. package/src/core/run-app.tsx +2 -1
  123. package/src/core/runtime/__tests__/runtime.test.ts +38 -17
  124. package/src/core/runtime/runtime.ts +57 -34
  125. package/src/core/wasm/__tests__/utils.test.ts +34 -0
  126. package/src/core/wasm/utils.ts +14 -0
  127. package/src/core/wasm/worker/bootstrap.ts +3 -2
  128. package/src/core/wasm/worker/worker.ts +3 -2
  129. package/src/core/websocket/__tests__/useMarimoKernelConnection.hook.test.tsx +156 -0
  130. package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +101 -0
  131. package/src/core/websocket/transports/__tests__/ws.test.ts +125 -0
  132. package/src/core/websocket/transports/basic.ts +1 -1
  133. package/src/core/websocket/transports/ws.ts +96 -0
  134. package/src/core/websocket/useMarimoKernelConnection.tsx +133 -54
  135. package/src/core/websocket/useWebSocket.tsx +3 -15
  136. package/src/css/app/Cell.css +10 -0
  137. package/src/plugins/core/__test__/sanitize.test.ts +30 -0
  138. package/src/plugins/impl/DropdownPlugin.tsx +12 -1
  139. package/src/plugins/impl/MultiselectPlugin.tsx +4 -0
  140. package/src/plugins/impl/SearchableSelect.tsx +11 -1
  141. package/src/plugins/impl/TabsPlugin.tsx +35 -7
  142. package/src/plugins/impl/__tests__/DropdownPlugin.test.tsx +56 -0
  143. package/src/plugins/impl/__tests__/TabsPlugin.test.tsx +154 -0
  144. package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +48 -36
  145. package/src/plugins/impl/data-frames/schema.ts +4 -1
  146. package/src/plugins/layout/DownloadPlugin.tsx +9 -7
  147. package/src/utils/__tests__/id-tree.test.ts +71 -0
  148. package/src/utils/download.ts +4 -2
  149. package/src/utils/id-tree.tsx +89 -0
  150. package/dist/assets/__vite-browser-external-rrUYDKRl.js +0 -1
  151. package/dist/assets/worker-Bfy15ViQ.js +0 -73
  152. package/dist/reveal-component-C97Ceb7e.js +0 -4863
  153. package/src/components/chat/tool-call-accordion.tsx +0 -247
@@ -9,7 +9,7 @@ import {
9
9
  TextIcon,
10
10
  XIcon,
11
11
  } from "lucide-react";
12
- import { useRef, useState } from "react";
12
+ import { useState } from "react";
13
13
  import { useLocale } from "react-aria";
14
14
  import {
15
15
  DropdownMenu,
@@ -26,26 +26,37 @@ import type { CalculateTopKRows } from "@/plugins/impl/DataTablePlugin";
26
26
  import type { OperatorType } from "@/plugins/impl/data-frames/utils/operators";
27
27
  import { logNever } from "@/utils/assertNever";
28
28
  import { cn } from "@/utils/cn";
29
- import { capitalize } from "@/utils/strings";
30
29
  import { Button } from "../ui/button";
31
30
  import { DraggablePopover } from "../ui/draggable-popover";
32
31
  import { Input } from "../ui/input";
32
+ import { RegexInput } from "./regex-input";
33
33
  import { NumberField } from "../ui/number-field";
34
34
  import { PopoverClose } from "../ui/popover";
35
35
  import {
36
36
  Select,
37
37
  SelectContent,
38
38
  SelectItem,
39
- SelectSeparator,
40
39
  SelectTrigger,
41
40
  SelectValue,
42
41
  } from "../ui/select";
43
42
  import { FilterByValuesList } from "./filter-by-values-picker";
43
+ import { OPERATOR_LABELS } from "./operator-labels";
44
44
  import {
45
45
  type ColumnFilterForType,
46
46
  type ColumnFilterValue,
47
+ DATETIME_OPS,
47
48
  Filter,
49
+ isDatetimeComparisonOp,
50
+ isNumberComparisonOp,
51
+ isTextScalarOp,
52
+ NUMBER_OPS,
53
+ TEXT_OPS,
48
54
  } from "./filters";
55
+ import {
56
+ type DateLikeFilterType,
57
+ DateLikeInput,
58
+ DateLikeRangeInput,
59
+ } from "./date-filter-inputs";
49
60
  import {
50
61
  ClearFilterMenuItem,
51
62
  FilterButtons,
@@ -148,7 +159,7 @@ export const DataTableColumnHeader = <TData, TValue>({
148
159
  {renderColumnWrapping(column)}
149
160
  {renderFormatOptions(column, locale)}
150
161
  <DropdownMenuSeparator />
151
- {renderMenuItemFilter(column)}
162
+ {renderMenuItemFilter(column, calculateTopKRows)}
152
163
  {renderFilterByValues(column, setIsFilterValueOpen)}
153
164
  {hasFilter && <ClearFilterMenuItem column={column} />}
154
165
  </DropdownMenuContent>
@@ -211,6 +222,7 @@ const SortButton = <TData, TValue>({
211
222
 
212
223
  export function renderMenuItemFilter<TData, TValue>(
213
224
  column: Column<TData, TValue>,
225
+ calculateTopKRows?: CalculateTopKRows,
214
226
  ) {
215
227
  const canFilter = column.getCanFilter();
216
228
  if (!canFilter) {
@@ -248,7 +260,10 @@ export function renderMenuItemFilter<TData, TValue>(
248
260
  {filterMenuItem}
249
261
  <DropdownMenuPortal>
250
262
  <DropdownMenuSubContent>
251
- <TextFilter column={column} />
263
+ <TextFilterMenu
264
+ column={column}
265
+ calculateTopKRows={calculateTopKRows}
266
+ />
252
267
  </DropdownMenuSubContent>
253
268
  </DropdownMenuPortal>
254
269
  </DropdownMenuSub>
@@ -261,7 +276,7 @@ export function renderMenuItemFilter<TData, TValue>(
261
276
  {filterMenuItem}
262
277
  <DropdownMenuPortal>
263
278
  <DropdownMenuSubContent>
264
- <NumberRangeFilter column={column} />
279
+ <NumberFilterMenu column={column} />
265
280
  </DropdownMenuSubContent>
266
281
  </DropdownMenuPortal>
267
282
  </DropdownMenuSub>
@@ -273,77 +288,49 @@ export function renderMenuItemFilter<TData, TValue>(
273
288
  return null;
274
289
  }
275
290
 
276
- if (filterType === "time") {
277
- // Not implemented
278
- return null;
279
- }
280
-
281
- if (filterType === "datetime") {
282
- // Not implemented
283
- return null;
284
- }
285
-
286
- if (filterType === "date") {
287
- // Not implemented
288
- return null;
291
+ if (
292
+ filterType === "date" ||
293
+ filterType === "datetime" ||
294
+ filterType === "time"
295
+ ) {
296
+ return (
297
+ <DropdownMenuSub>
298
+ {filterMenuItem}
299
+ <DropdownMenuPortal>
300
+ <DropdownMenuSubContent>
301
+ <DateFilterMenu column={column} filterType={filterType} />
302
+ </DropdownMenuSubContent>
303
+ </DropdownMenuPortal>
304
+ </DropdownMenuSub>
305
+ );
289
306
  }
290
307
 
291
308
  logNever(filterType);
292
309
  return null;
293
310
  }
294
311
 
295
- // Type-safe constants for null filter operators
296
- const NULL_FILTER_OPERATORS = {
297
- is_null: "is_null",
298
- is_not_null: "is_not_null",
299
- } satisfies Record<string, OperatorType>;
300
-
301
- const NullFilter = <TData, TValue>({
302
- column,
303
- defaultItem,
312
+ const OperatorSelect = ({
304
313
  operator,
305
- setOperator,
314
+ options,
315
+ onChange,
306
316
  }: {
307
- column: Column<TData, TValue>;
308
- defaultItem?: OperatorType | "between";
309
- operator: OperatorType | "between";
310
- setOperator: (operator: OperatorType) => void;
311
- }) => {
312
- const handleValueChange = (value: OperatorType) => {
313
- setOperator(value);
314
- if (value === "is_null" || value === "is_not_null") {
315
- column.setFilterValue(Filter.text({ operator: value }));
316
- }
317
- };
318
-
319
- const isNullOrNotNull = operator === "is_null" || operator === "is_not_null";
320
-
321
- return (
322
- <Select
323
- value={operator}
324
- onValueChange={(value) => handleValueChange(value as OperatorType)}
325
- >
326
- <SelectTrigger
327
- className={cn(
328
- "border-border shadow-none! ring-0! w-full mb-0.5",
329
- isNullOrNotNull && "mb-2",
330
- )}
331
- >
332
- <SelectValue defaultValue={operator} />
333
- </SelectTrigger>
334
- <SelectContent>
335
- {defaultItem && (
336
- <SelectItem value={defaultItem}>{capitalize(defaultItem)}</SelectItem>
337
- )}
338
- <SelectSeparator />
339
- <SelectItem value={NULL_FILTER_OPERATORS.is_null}>Is null</SelectItem>
340
- <SelectItem value={NULL_FILTER_OPERATORS.is_not_null}>
341
- Is not null
317
+ operator: OperatorType;
318
+ options: readonly OperatorType[];
319
+ onChange: (next: OperatorType) => void;
320
+ }) => (
321
+ <Select value={operator} onValueChange={(v) => onChange(v as OperatorType)}>
322
+ <SelectTrigger className="border-border shadow-none! ring-0! w-full mb-0.5">
323
+ <SelectValue />
324
+ </SelectTrigger>
325
+ <SelectContent>
326
+ {options.map((op) => (
327
+ <SelectItem key={op} value={op}>
328
+ {OPERATOR_LABELS[op]}
342
329
  </SelectItem>
343
- </SelectContent>
344
- </Select>
345
- );
346
- };
330
+ ))}
331
+ </SelectContent>
332
+ </Select>
333
+ );
347
334
 
348
335
  const BooleanFilter = <TData, TValue>({
349
336
  column,
@@ -389,7 +376,15 @@ const BooleanFilter = <TData, TValue>({
389
376
  );
390
377
  };
391
378
 
392
- const NumberRangeFilter = <TData, TValue>({
379
+ type NumberComparisonFilter = Extract<
380
+ ColumnFilterForType<"number">,
381
+ { value: number }
382
+ >;
383
+ const isNumberComparisonFilter = (
384
+ filter: ColumnFilterForType<"number">,
385
+ ): filter is NumberComparisonFilter => isNumberComparisonOp(filter.operator);
386
+
387
+ export const NumberFilterMenu = <TData, TValue>({
393
388
  column,
394
389
  }: {
395
390
  column: Column<TData, TValue>;
@@ -399,149 +394,333 @@ const NumberRangeFilter = <TData, TValue>({
399
394
  | undefined;
400
395
  const hasFilter = currentFilter !== undefined;
401
396
 
402
- const [operator, setOperator] = useState<OperatorType | "between">(
397
+ const [operator, setOperator] = useState<OperatorType>(
403
398
  currentFilter?.operator ?? "between",
404
399
  );
405
- const [min, setMin] = useState<number | undefined>(currentFilter?.min);
406
- const [max, setMax] = useState<number | undefined>(currentFilter?.max);
407
- const minRef = useRef<HTMLInputElement>(null);
408
- const maxRef = useRef<HTMLInputElement>(null);
400
+ const [min, setMin] = useState<number | undefined>(
401
+ currentFilter?.operator === "between" ? currentFilter.min : undefined,
402
+ );
403
+ const [max, setMax] = useState<number | undefined>(
404
+ currentFilter?.operator === "between" ? currentFilter.max : undefined,
405
+ );
406
+ const [value, setValue] = useState<number | undefined>(
407
+ currentFilter !== undefined && isNumberComparisonFilter(currentFilter)
408
+ ? currentFilter.value
409
+ : undefined,
410
+ );
409
411
 
410
- const handleApply = (opts: { min?: number; max?: number } = {}) => {
411
- column.setFilterValue(
412
- Filter.number({
413
- min: opts.min ?? min,
414
- max: opts.max ?? max,
415
- operator: operator === "between" ? undefined : operator,
416
- }),
417
- );
412
+ const isComparison = isNumberComparisonOp(operator);
413
+ const isNullish = operator === "is_null" || operator === "is_not_null";
414
+
415
+ const applyDisabled =
416
+ (operator === "between" && (min === undefined || max === undefined)) ||
417
+ (isComparison && value === undefined);
418
+
419
+ const handleApply = () => {
420
+ if (isNullish) {
421
+ column.setFilterValue(Filter.number({ operator }));
422
+ return;
423
+ }
424
+ if (operator === "between" && min !== undefined && max !== undefined) {
425
+ column.setFilterValue(Filter.number({ operator: "between", min, max }));
426
+ return;
427
+ }
428
+ if (isComparison && value !== undefined) {
429
+ column.setFilterValue(Filter.number({ operator, value }));
430
+ }
431
+ };
432
+
433
+ const handleClear = () => {
434
+ setMin(undefined);
435
+ setMax(undefined);
436
+ setValue(undefined);
437
+ column.setFilterValue(undefined);
438
+ };
439
+
440
+ const handleOperatorChange = (next: OperatorType) => {
441
+ setOperator(next);
418
442
  };
419
443
 
420
444
  return (
421
445
  <div className="flex flex-col gap-1 pt-3 px-2">
422
- <NullFilter
423
- column={column}
424
- defaultItem="between"
446
+ <OperatorSelect
425
447
  operator={operator}
426
- setOperator={setOperator}
448
+ options={NUMBER_OPS}
449
+ onChange={handleOperatorChange}
427
450
  />
428
451
  {operator === "between" && (
429
- <>
430
- <div className="flex gap-1 items-center">
431
- <NumberField
432
- ref={minRef}
433
- value={min}
434
- onChange={(value) => setMin(value)}
435
- aria-label="min"
436
- placeholder="min"
437
- onKeyDown={(e) => {
438
- if (e.key === "Enter") {
439
- handleApply({
440
- min: Number.parseFloat(e.currentTarget.value),
441
- });
442
- }
443
- if (e.key === "Tab") {
444
- maxRef.current?.focus();
445
- }
446
- }}
447
- className="shadow-none! border-border hover:shadow-none!"
448
- />
449
- <MinusIcon className="h-5 w-5 text-muted-foreground" />
450
- <NumberField
451
- ref={maxRef}
452
- value={max}
453
- onChange={(value) => setMax(value)}
454
- aria-label="max"
455
- onKeyDown={(e) => {
456
- if (e.key === "Enter") {
457
- handleApply({
458
- max: Number.parseFloat(e.currentTarget.value),
459
- });
460
- }
461
- if (e.key === "Tab") {
462
- minRef.current?.focus();
463
- }
464
- }}
465
- placeholder="max"
466
- className="shadow-none! border-border hover:shadow-none!"
467
- />
468
- </div>
469
- <FilterButtons
470
- onApply={handleApply}
471
- onClear={() => {
472
- setMin(undefined);
473
- setMax(undefined);
474
- column.setFilterValue(undefined);
475
- }}
476
- clearButtonDisabled={!hasFilter}
452
+ <div className="flex gap-1 items-center">
453
+ <NumberField
454
+ value={min}
455
+ onChange={setMin}
456
+ aria-label="min"
457
+ placeholder="min"
458
+ className="shadow-none! border-border hover:shadow-none!"
477
459
  />
478
- </>
460
+ <MinusIcon className="h-5 w-5 text-muted-foreground" />
461
+ <NumberField
462
+ value={max}
463
+ onChange={setMax}
464
+ aria-label="max"
465
+ placeholder="max"
466
+ className="shadow-none! border-border hover:shadow-none!"
467
+ />
468
+ </div>
469
+ )}
470
+ {isComparison && (
471
+ <NumberField
472
+ value={value}
473
+ onChange={setValue}
474
+ aria-label="value"
475
+ placeholder="value"
476
+ className="shadow-none! border-border hover:shadow-none!"
477
+ />
479
478
  )}
479
+ <FilterButtons
480
+ onApply={handleApply}
481
+ onClear={handleClear}
482
+ clearButtonDisabled={!hasFilter}
483
+ applyButtonDisabled={applyDisabled}
484
+ />
480
485
  </div>
481
486
  );
482
487
  };
483
488
 
484
- const TextFilter = <TData, TValue>({
489
+ type DateComparisonFilter = Extract<
490
+ ColumnFilterForType<DateLikeFilterType>,
491
+ { value: Date }
492
+ >;
493
+ const isDateComparisonFilter = (
494
+ filter: ColumnFilterForType<DateLikeFilterType>,
495
+ ): filter is DateComparisonFilter => isDatetimeComparisonOp(filter.operator);
496
+
497
+ export const DateFilterMenu = <TData, TValue>({
485
498
  column,
499
+ filterType,
486
500
  }: {
487
501
  column: Column<TData, TValue>;
502
+ filterType: DateLikeFilterType;
503
+ }) => {
504
+ const currentFilter = column.getFilterValue() as
505
+ | ColumnFilterForType<DateLikeFilterType>
506
+ | undefined;
507
+ const hasFilter = currentFilter !== undefined;
508
+
509
+ const [operator, setOperator] = useState<OperatorType>(
510
+ currentFilter?.operator ?? "between",
511
+ );
512
+ const [min, setMin] = useState<Date | undefined>(
513
+ currentFilter?.operator === "between" ? currentFilter.min : undefined,
514
+ );
515
+ const [max, setMax] = useState<Date | undefined>(
516
+ currentFilter?.operator === "between" ? currentFilter.max : undefined,
517
+ );
518
+ const [value, setValue] = useState<Date | undefined>(
519
+ currentFilter !== undefined && isDateComparisonFilter(currentFilter)
520
+ ? currentFilter.value
521
+ : undefined,
522
+ );
523
+
524
+ const isComparison = isDatetimeComparisonOp(operator);
525
+ const isNullish = operator === "is_null" || operator === "is_not_null";
526
+
527
+ const applyDisabled =
528
+ (operator === "between" && (min === undefined || max === undefined)) ||
529
+ (isComparison && value === undefined);
530
+
531
+ const buildFilter = (
532
+ opts: Parameters<typeof Filter.date>[0],
533
+ ): ColumnFilterForType<DateLikeFilterType> => {
534
+ switch (filterType) {
535
+ case "date":
536
+ return Filter.date(opts);
537
+ case "datetime":
538
+ return Filter.datetime(opts);
539
+ case "time":
540
+ return Filter.time(opts);
541
+ }
542
+ };
543
+
544
+ const handleApply = () => {
545
+ if (isNullish) {
546
+ column.setFilterValue(buildFilter({ operator }));
547
+ return;
548
+ }
549
+ if (operator === "between" && min !== undefined && max !== undefined) {
550
+ column.setFilterValue(buildFilter({ operator: "between", min, max }));
551
+ return;
552
+ }
553
+ if (isComparison && value !== undefined) {
554
+ column.setFilterValue(buildFilter({ operator, value }));
555
+ }
556
+ };
557
+
558
+ const [resetKey, setResetKey] = useState(0);
559
+ const handleClear = () => {
560
+ setMin(undefined);
561
+ setMax(undefined);
562
+ setValue(undefined);
563
+ setResetKey((k) => k + 1);
564
+ column.setFilterValue(undefined);
565
+ };
566
+
567
+ const handleOperatorChange = (next: OperatorType) => {
568
+ setOperator(next);
569
+ };
570
+
571
+ return (
572
+ <div
573
+ className="flex flex-col gap-1 pt-3 px-2"
574
+ onKeyDownCapture={(e) => {
575
+ if (e.key === "Tab") {
576
+ e.stopPropagation();
577
+ }
578
+ }}
579
+ >
580
+ <OperatorSelect
581
+ operator={operator}
582
+ options={DATETIME_OPS}
583
+ onChange={handleOperatorChange}
584
+ />
585
+ {operator === "between" && (
586
+ <DateLikeRangeInput
587
+ key={`${filterType}-${resetKey}`}
588
+ filterType={filterType}
589
+ min={min}
590
+ max={max}
591
+ onRangeChange={(nextMin, nextMax) => {
592
+ setMin(nextMin);
593
+ setMax(nextMax);
594
+ }}
595
+ className="shadow-none! border-border hover:shadow-none!"
596
+ />
597
+ )}
598
+ {isComparison && (
599
+ <DateLikeInput
600
+ key={`${filterType}-${resetKey}`}
601
+ filterType={filterType}
602
+ value={value}
603
+ onChange={setValue}
604
+ aria-label="value"
605
+ className="shadow-none! border-border hover:shadow-none!"
606
+ />
607
+ )}
608
+ <FilterButtons
609
+ onApply={handleApply}
610
+ onClear={handleClear}
611
+ clearButtonDisabled={!hasFilter}
612
+ applyButtonDisabled={applyDisabled}
613
+ />
614
+ </div>
615
+ );
616
+ };
617
+
618
+ export const TextFilterMenu = <TData, TValue>({
619
+ column,
620
+ calculateTopKRows,
621
+ }: {
622
+ column: Column<TData, TValue>;
623
+ calculateTopKRows?: CalculateTopKRows;
488
624
  }) => {
489
625
  const currentFilter = column.getFilterValue() as
490
626
  | ColumnFilterForType<"text">
491
627
  | undefined;
492
628
  const hasFilter = currentFilter !== undefined;
493
- const [value, setValue] = useState<string>(currentFilter?.text ?? "");
629
+
494
630
  const [operator, setOperator] = useState<OperatorType>(
495
631
  currentFilter?.operator ?? "contains",
496
632
  );
633
+ const [text, setText] = useState<string>(
634
+ currentFilter && "text" in currentFilter ? currentFilter.text : "",
635
+ );
636
+ const [values, setValues] = useState<string[]>(
637
+ currentFilter && "values" in currentFilter ? [...currentFilter.values] : [],
638
+ );
639
+
640
+ const isScalar = isTextScalarOp(operator);
641
+ const isMulti = operator === "in" || operator === "not_in";
642
+ const isNullish =
643
+ operator === "is_null" ||
644
+ operator === "is_not_null" ||
645
+ operator === "is_empty";
646
+
647
+ const applyDisabled =
648
+ (isScalar && text === "") || (isMulti && values.length === 0);
497
649
 
498
650
  const handleApply = () => {
499
- if (operator !== "contains") {
651
+ if (isNullish) {
500
652
  column.setFilterValue(Filter.text({ operator }));
501
653
  return;
502
654
  }
503
-
504
- if (value === "") {
505
- column.setFilterValue(undefined);
655
+ if (isScalar && text !== "") {
656
+ column.setFilterValue(Filter.text({ operator, text }));
506
657
  return;
507
658
  }
659
+ if (isMulti && values.length > 0) {
660
+ column.setFilterValue(Filter.text({ operator, values }));
661
+ }
662
+ };
663
+
664
+ const handleClear = () => {
665
+ setText("");
666
+ setValues([]);
667
+ column.setFilterValue(undefined);
668
+ };
508
669
 
509
- column.setFilterValue(Filter.text({ text: value, operator }));
670
+ const handleOperatorChange = (next: OperatorType) => {
671
+ setOperator(next);
510
672
  };
511
673
 
512
674
  return (
513
675
  <div className="flex flex-col gap-1 pt-3 px-2">
514
- <NullFilter
515
- column={column}
516
- defaultItem="contains"
676
+ <OperatorSelect
517
677
  operator={operator}
518
- setOperator={setOperator}
678
+ options={TEXT_OPS}
679
+ onChange={handleOperatorChange}
519
680
  />
520
- {operator === "contains" && (
521
- <>
522
- <Input
523
- type="text"
524
- icon={<TextIcon className="h-3 w-3 text-muted-foreground mb-1" />}
525
- value={value ?? ""}
526
- onChange={(e) => setValue(e.target.value)}
527
- placeholder="Text..."
528
- onKeyDown={(e) => {
529
- if (e.key === "Enter") {
530
- handleApply();
531
- }
532
- }}
533
- className="shadow-none! border-border hover:shadow-none!"
534
- />
535
- <FilterButtons
536
- onApply={handleApply}
537
- onClear={() => {
538
- setValue("");
539
- column.setFilterValue(undefined);
540
- }}
541
- clearButtonDisabled={!hasFilter}
542
- />
543
- </>
681
+ {isScalar && operator === "regex" && (
682
+ <RegexInput
683
+ value={text}
684
+ onChange={setText}
685
+ onKeyDown={(e) => {
686
+ e.stopPropagation();
687
+ if (e.key === "Enter") {
688
+ handleApply();
689
+ }
690
+ }}
691
+ />
544
692
  )}
693
+ {isScalar && operator !== "regex" && (
694
+ <Input
695
+ type="text"
696
+ icon={<TextIcon className="h-3 w-3 text-muted-foreground mb-1" />}
697
+ value={text}
698
+ onChange={(e) => setText(e.target.value)}
699
+ placeholder="Text..."
700
+ onKeyDown={(e) => {
701
+ e.stopPropagation();
702
+ if (e.key === "Enter") {
703
+ handleApply();
704
+ }
705
+ }}
706
+ className="shadow-none! border-border hover:shadow-none!"
707
+ />
708
+ )}
709
+ {isMulti && (
710
+ <FilterByValuesList
711
+ column={column}
712
+ calculateTopKRows={calculateTopKRows}
713
+ chosenValues={new Set(values)}
714
+ onChange={(next) => setValues(next.map(String))}
715
+ creatable={true}
716
+ />
717
+ )}
718
+ <FilterButtons
719
+ onApply={handleApply}
720
+ onClear={handleClear}
721
+ clearButtonDisabled={!hasFilter}
722
+ applyButtonDisabled={applyDisabled}
723
+ />
545
724
  </div>
546
725
  );
547
726
  };