@marimo-team/islands 0.23.2-dev12 → 0.23.2-dev14

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/main.js CHANGED
@@ -35,7 +35,7 @@ import { a as Plus, i as Trash, n as ErrorBoundary, o as Pencil, r as require_pr
35
35
  import { n as clsx_default } from "./clsx-CwTY0BxM.js";
36
36
  import { t as require_react_dom } from "./react-dom-D5FDLRUB.js";
37
37
  import { t as require_jsx_runtime } from "./jsx-runtime-9hcJiI23.js";
38
- import { $ as StyleNamespace, A as looseObject, B as union, C as any, F as optional, G as parse$3, I as record, J as prettifyError, K as parseAsync, N as number, P as object, Q as isInVscodeExtension, R as string, S as _null, T as boolean, U as ZodError, V as unknown, X as withFullScreenAsRoot, Y as MAX_HEIGHT_OFFSET, Z as withSmartCollisionBoundary, _ as ZodString, _t as Primitive$1, a as ZodIssueCode, b as _enum, c as ZodBoolean, d as ZodDiscriminatedUnion, dt as Presence, f as ZodEnum, ft as useControllableState, g as ZodOptional, gt as createContextScope, h as ZodObject, i as string$1, j as nan, k as literal, l as ZodDate, lt as useCallbackRef, m as ZodNumber, mt as composeEventHandlers$1, n as date, nt as __awaiter, o as ZodAny, p as ZodLiteral, pt as useLayoutEffect2, q as $ZodError, r as number$1, s as ZodArray, st as DismissableLayer, t as zod_default, u as ZodDefault, ut as useId$12, v as ZodType, vt as dispatchDiscreteCustomEvent, w as array, x as _instanceof, y as ZodUnion, z as tuple } from "./zod-W5ZEjzaE.js";
38
+ import { $ as StyleNamespace, A as looseObject, B as union, C as any, F as optional, G as parse$3, I as record, J as prettifyError, K as parseAsync, N as number, O as lazy$2, P as object, Q as isInVscodeExtension, R as string, S as _null, T as boolean, U as ZodError, V as unknown, X as withFullScreenAsRoot, Y as MAX_HEIGHT_OFFSET, Z as withSmartCollisionBoundary, _ as ZodString, _t as Primitive$1, a as ZodIssueCode, b as _enum, c as ZodBoolean, d as ZodDiscriminatedUnion, dt as Presence, f as ZodEnum, ft as useControllableState, g as ZodOptional, gt as createContextScope, h as ZodObject, i as string$1, j as nan, k as literal, l as ZodDate, lt as useCallbackRef, m as ZodNumber, mt as composeEventHandlers$1, n as date, nt as __awaiter, o as ZodAny, p as ZodLiteral, pt as useLayoutEffect2, q as $ZodError, r as number$1, s as ZodArray, st as DismissableLayer, t as zod_default, u as ZodDefault, ut as useId$12, v as ZodType, vt as dispatchDiscreteCustomEvent, w as array, x as _instanceof, y as ZodUnion, z as tuple } from "./zod-W5ZEjzaE.js";
39
39
  import { a as Content$2, c as Root$6, i as prettyError, n as ErrorBanner, o as Overlay, r as CellNotInitializedError, s as Portal$1, t as Banner } from "./error-banner-B_ioHva3.js";
40
40
  import { a as TooltipRoot, i as TooltipProvider, n as TooltipContent, o as TooltipTrigger, r as TooltipPortal, t as Tooltip } from "./tooltip-DwNnFsxZ.js";
41
41
  import { n as minimalSetup, r as _extends$5, t as esm_default, u as CopyClipboardIcon } from "./esm-CDHI9cuO.js";
@@ -23789,10 +23789,10 @@ Database schema: ${c}`), (_a4 = r2.aiFix) == null ? void 0 : _a4.setAiCompletion
23789
23789
  else if (c.some(_temp2$24)) _ = "An internal error occurred";
23790
23790
  else if (c.some(_temp3$16)) _ = "Ancestor prevented from running", y = "default", S = "text-secondary-foreground";
23791
23791
  else if (c.some(_temp4$9)) _ = "Ancestor stopped", y = "default", S = "text-secondary-foreground";
23792
- else if (c.some(_temp5$6)) _ = "SQL error";
23792
+ else if (c.some(_temp5$5)) _ = "SQL error";
23793
23793
  else {
23794
23794
  let e2;
23795
- r[0] === c ? e2 = r[1] : (e2 = c.find(_temp6$6), r[0] = c, r[1] = e2);
23795
+ r[0] === c ? e2 = r[1] : (e2 = c.find(_temp6$5), r[0] = c, r[1] = e2);
23796
23796
  let d2 = e2;
23797
23797
  d2 && "exception_type" in d2 && (_ = d2.exception_type);
23798
23798
  }
@@ -24164,10 +24164,10 @@ Database schema: ${c}`), (_a4 = r2.aiFix) == null ? void 0 : _a4.setAiCompletion
24164
24164
  function _temp4$9(e) {
24165
24165
  return e.type === "ancestor-stopped";
24166
24166
  }
24167
- function _temp5$6(e) {
24167
+ function _temp5$5(e) {
24168
24168
  return e.type === "sql-error";
24169
24169
  }
24170
- function _temp6$6(e) {
24170
+ function _temp6$5(e) {
24171
24171
  return e.type === "exception";
24172
24172
  }
24173
24173
  function _temp7$2(e) {
@@ -25715,40 +25715,56 @@ Database schema: ${c}`), (_a4 = r2.aiFix) == null ? void 0 : _a4.setAiCompletion
25715
25715
  function filterToFilterCondition(e, r) {
25716
25716
  if (!r) return [];
25717
25717
  let c = e;
25718
- if (r.operator === "is_null" || r.operator === "is_not_null") return {
25719
- column_id: c,
25720
- operator: r.operator,
25721
- value: void 0
25722
- };
25718
+ if (r.operator === "is_null" || r.operator === "is_not_null") return [
25719
+ {
25720
+ column_id: c,
25721
+ operator: r.operator,
25722
+ value: void 0,
25723
+ type: "condition",
25724
+ negate: false
25725
+ }
25726
+ ];
25723
25727
  switch (r.type) {
25724
25728
  case "number": {
25725
25729
  let e2 = [];
25726
25730
  return r.min !== void 0 && e2.push({
25727
25731
  column_id: c,
25728
25732
  operator: ">=",
25729
- value: r.min
25733
+ value: r.min,
25734
+ type: "condition",
25735
+ negate: false
25730
25736
  }), r.max !== void 0 && e2.push({
25731
25737
  column_id: c,
25732
25738
  operator: "<=",
25733
- value: r.max
25739
+ value: r.max,
25740
+ type: "condition",
25741
+ negate: false
25734
25742
  }), e2;
25735
25743
  }
25736
25744
  case "text":
25737
- return {
25738
- column_id: c,
25739
- operator: r.operator,
25740
- value: r.text
25741
- };
25745
+ return [
25746
+ {
25747
+ column_id: c,
25748
+ operator: r.operator,
25749
+ value: r.text,
25750
+ type: "condition",
25751
+ negate: false
25752
+ }
25753
+ ];
25742
25754
  case "datetime": {
25743
25755
  let e2 = [];
25744
25756
  return r.min !== void 0 && e2.push({
25745
25757
  column_id: c,
25746
25758
  operator: ">=",
25747
- value: r.min.toISOString()
25759
+ value: r.min.toISOString(),
25760
+ type: "condition",
25761
+ negate: false
25748
25762
  }), r.max !== void 0 && e2.push({
25749
25763
  column_id: c,
25750
25764
  operator: "<=",
25751
- value: r.max.toISOString()
25765
+ value: r.max.toISOString(),
25766
+ type: "condition",
25767
+ negate: false
25752
25768
  }), e2;
25753
25769
  }
25754
25770
  case "date": {
@@ -25756,11 +25772,15 @@ Database schema: ${c}`), (_a4 = r2.aiFix) == null ? void 0 : _a4.setAiCompletion
25756
25772
  return r.min !== void 0 && e2.push({
25757
25773
  column_id: c,
25758
25774
  operator: ">=",
25759
- value: r.min.toISOString()
25775
+ value: r.min.toISOString(),
25776
+ type: "condition",
25777
+ negate: false
25760
25778
  }), r.max !== void 0 && e2.push({
25761
25779
  column_id: c,
25762
25780
  operator: "<=",
25763
- value: r.max.toISOString()
25781
+ value: r.max.toISOString(),
25782
+ type: "condition",
25783
+ negate: false
25764
25784
  }), e2;
25765
25785
  }
25766
25786
  case "time": {
@@ -25768,37 +25788,61 @@ Database schema: ${c}`), (_a4 = r2.aiFix) == null ? void 0 : _a4.setAiCompletion
25768
25788
  return r.min !== void 0 && e2.push({
25769
25789
  column_id: c,
25770
25790
  operator: ">=",
25771
- value: r.min.toISOString()
25791
+ value: r.min.toISOString(),
25792
+ type: "condition",
25793
+ negate: false
25772
25794
  }), r.max !== void 0 && e2.push({
25773
25795
  column_id: c,
25774
25796
  operator: "<=",
25775
- value: r.max.toISOString()
25797
+ value: r.max.toISOString(),
25798
+ type: "condition",
25799
+ negate: false
25776
25800
  }), e2;
25777
25801
  }
25778
25802
  case "boolean":
25779
- return r.value ? {
25780
- column_id: c,
25781
- operator: "is_true",
25782
- value: void 0
25783
- } : r.value ? [] : {
25784
- column_id: c,
25785
- operator: "is_false",
25786
- value: void 0
25787
- };
25803
+ return r.value ? [
25804
+ {
25805
+ column_id: c,
25806
+ operator: "is_true",
25807
+ value: void 0,
25808
+ type: "condition",
25809
+ negate: false
25810
+ }
25811
+ ] : r.value ? [] : [
25812
+ {
25813
+ column_id: c,
25814
+ operator: "is_false",
25815
+ value: void 0,
25816
+ type: "condition",
25817
+ negate: false
25818
+ }
25819
+ ];
25788
25820
  case "select": {
25789
25821
  let e2 = r.operator;
25790
25822
  return r.operator !== "in" && r.operator !== "not_in" && (Logger.warn("Invalid operator for select filter", {
25791
25823
  operator: r.operator
25792
- }), e2 = "in"), {
25793
- column_id: c,
25794
- operator: e2,
25795
- value: r.options
25796
- };
25824
+ }), e2 = "in"), [
25825
+ {
25826
+ column_id: c,
25827
+ operator: e2,
25828
+ value: r.options,
25829
+ type: "condition",
25830
+ negate: false
25831
+ }
25832
+ ];
25797
25833
  }
25798
25834
  default:
25799
25835
  assertNever(r);
25800
25836
  }
25801
25837
  }
25838
+ function filtersToFilterGroup(e) {
25839
+ return {
25840
+ type: "group",
25841
+ operator: "and",
25842
+ children: e.flatMap((e2) => filterToFilterCondition(e2.id, e2.value)),
25843
+ negate: false
25844
+ };
25845
+ }
25802
25846
  function functionalUpdate(e, r) {
25803
25847
  return typeof e == "function" ? e(r) : e;
25804
25848
  }
@@ -43129,7 +43173,7 @@ ${c}
43129
43173
  r[0] !== c || r[1] !== f ? (h = () => {
43130
43174
  f.setValue(c, "");
43131
43175
  }, r[0] = c, r[1] = f, r[2] = h) : h = r[2];
43132
- let _ = h, v = _temp5$5, y;
43176
+ let _ = h, v = _temp5$4, y;
43133
43177
  r[3] !== _ || r[4] !== d ? (y = (e2) => {
43134
43178
  let { field: r2 } = e2;
43135
43179
  return (0, import_jsx_runtime.jsxs)(FormItem, {
@@ -43250,7 +43294,7 @@ ${c}
43250
43294
  render: S
43251
43295
  }), r[5] = c, r[6] = y.control, r[7] = S, r[8] = w) : w = r[8], w;
43252
43296
  }, AggregationSelect = (e) => {
43253
- let r = (0, import_compiler_runtime$80.c)(12), { fieldName: c, selectedDataType: d, binFieldName: f, defaultAggregation: h } = e, _ = useFormContext(), v = d === "string" ? STRING_AGGREGATION_FNS : AGGREGATION_FNS$1, { chartType: y } = useChartFormContext(), S = y !== ChartType.HEATMAP, w = _temp6$5, E = _temp7$1, O;
43297
+ let r = (0, import_compiler_runtime$80.c)(12), { fieldName: c, selectedDataType: d, binFieldName: f, defaultAggregation: h } = e, _ = useFormContext(), v = d === "string" ? STRING_AGGREGATION_FNS : AGGREGATION_FNS$1, { chartType: y } = useChartFormContext(), S = y !== ChartType.HEATMAP, w = _temp6$4, E = _temp7$1, O;
43254
43298
  r[0] !== f || r[1] !== _ ? (O = (e2) => {
43255
43299
  let { value: r2, previousValue: c2, onChange: d2 } = e2;
43256
43300
  r2 === "bin" ? _.setValue(f, true) : c2 === "bin" && _.setValue(f, false), d2(r2);
@@ -43404,7 +43448,7 @@ ${c}
43404
43448
  function _temp4$7(e) {
43405
43449
  return e;
43406
43450
  }
43407
- function _temp5$5(e) {
43451
+ function _temp5$4(e) {
43408
43452
  let [r, c] = TIME_UNIT_DESCRIPTIONS[e];
43409
43453
  return (0, import_jsx_runtime.jsx)(SelectItem, {
43410
43454
  value: e,
@@ -43416,7 +43460,7 @@ ${c}
43416
43460
  children: r
43417
43461
  }, e);
43418
43462
  }
43419
- function _temp6$5(e) {
43463
+ function _temp6$4(e) {
43420
43464
  return (0, import_jsx_runtime.jsx)("span", {
43421
43465
  className: "text-xs text-muted-foreground pr-10",
43422
43466
  children: e
@@ -45609,6 +45653,12 @@ ${c}
45609
45653
  "<=": [
45610
45654
  e
45611
45655
  ],
45656
+ between: [
45657
+ object({
45658
+ min: e,
45659
+ max: e
45660
+ })
45661
+ ],
45612
45662
  is_null: [],
45613
45663
  is_not_null: []
45614
45664
  });
@@ -45637,6 +45687,7 @@ ${c}
45637
45687
  not_in: [
45638
45688
  Schema.stringMultiColumnValues
45639
45689
  ],
45690
+ is_empty: [],
45640
45691
  is_null: [],
45641
45692
  is_not_null: []
45642
45693
  }, ALL_OPERATORS = {
@@ -45753,17 +45804,30 @@ ${c}
45753
45804
  special: "radio_group"
45754
45805
  })).default("last")
45755
45806
  });
45756
- const ConditionSchema = object({
45807
+ const FilterConditionSchema = object({
45757
45808
  column_id,
45758
45809
  operator: _enum(Object.keys(ALL_OPERATORS)).describe(FieldOptions.of({
45759
45810
  label: " "
45760
45811
  })),
45812
+ type: literal("condition").default("condition"),
45761
45813
  value: any().describe(FieldOptions.of({
45762
45814
  label: "Value"
45763
- }))
45815
+ })),
45816
+ negate: boolean().default(false)
45764
45817
  }).describe(FieldOptions.of({
45765
45818
  direction: "row",
45766
45819
  special: "column_filter"
45820
+ })), FilterGroupSchema = lazy$2(() => object({
45821
+ type: literal("group").default("group"),
45822
+ operator: _enum([
45823
+ "and",
45824
+ "or"
45825
+ ]).default("and"),
45826
+ children: array(union([
45827
+ FilterConditionSchema,
45828
+ FilterGroupSchema
45829
+ ])).default([]),
45830
+ negate: boolean().default(false)
45767
45831
  }));
45768
45832
  var FilterRowsTransformSchema = object({
45769
45833
  type: literal("filter_rows"),
@@ -45773,16 +45837,23 @@ ${c}
45773
45837
  ]).default("keep_rows").describe(FieldOptions.of({
45774
45838
  special: "radio_group"
45775
45839
  })),
45776
- where: array(ConditionSchema).min(1).describe(FieldOptions.of({
45840
+ where: array(FilterConditionSchema).min(1).describe(FieldOptions.of({
45777
45841
  label: "Value",
45778
45842
  minLength: 1
45779
- })).transform((e) => e.filter((e2) => isConditionValueValid(e2.operator, e2.value))).default(() => [
45843
+ })).default(() => [
45780
45844
  {
45781
45845
  column_id: "",
45782
45846
  operator: "==",
45783
- value: ""
45784
- }
45785
- ])
45847
+ value: "",
45848
+ type: "condition",
45849
+ negate: false
45850
+ }
45851
+ ]).transform((e) => ({
45852
+ type: "group",
45853
+ operator: "and",
45854
+ children: e.filter((e2) => isConditionValueValid(e2.operator, e2.value)),
45855
+ negate: false
45856
+ }))
45786
45857
  }), GroupByTransformSchema = object({
45787
45858
  type: literal("group_by"),
45788
45859
  column_ids: array(column_id.describe(FieldOptions.of({
@@ -46008,7 +46079,7 @@ ${c}
46008
46079
  descending: boolean()
46009
46080
  })).optional(),
46010
46081
  query: string().optional(),
46011
- filters: array(ConditionSchema).optional(),
46082
+ filters: FilterGroupSchema.optional(),
46012
46083
  page_number: number(),
46013
46084
  page_size: number(),
46014
46085
  max_columns: number().nullable().optional()
@@ -46166,9 +46237,9 @@ ${c}
46166
46237
  query: M,
46167
46238
  page_number: E.pageIndex,
46168
46239
  page_size: E.pageSize,
46169
- filters: Y7.flatMap(_temp2$15)
46240
+ filters: filtersToFilterGroup(Y7)
46170
46241
  });
46171
- if (S2) w2.catch(_temp3$10);
46242
+ if (S2) w2.catch(_temp2$15);
46172
46243
  else {
46173
46244
  let e2 = await w2;
46174
46245
  r2 = e2.data, c2 = e2.raw_data, d2 = e2.total_rows, h = e2.cell_styles || {}, _2 = e2.cell_hover_texts || {};
@@ -46202,15 +46273,15 @@ ${c}
46202
46273
  rows: await loadTableData((await f({
46203
46274
  page_number: e2,
46204
46275
  page_size: 1,
46205
- sort: y.length > 0 ? y.map(_temp4$6) : void 0,
46276
+ sort: y.length > 0 ? y.map(_temp3$10) : void 0,
46206
46277
  query: M,
46207
- filters: Y7.flatMap(_temp5$4),
46278
+ filters: filtersToFilterGroup(Y7),
46208
46279
  max_columns: null
46209
46280
  })).data)
46210
46281
  }), r[48] = Y7, r[49] = f, r[50] = M, r[51] = y, r[52] = l9) : l9 = r[52];
46211
46282
  let u9 = l9, d9;
46212
46283
  r[53] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel") ? (d9 = () => {
46213
- O(_temp6$4);
46284
+ O(_temp4$6);
46214
46285
  }, r[53] = d9) : d9 = r[53];
46215
46286
  let f9 = a9 == null ? void 0 : a9.totalRows, p9;
46216
46287
  r[54] === f9 ? p9 = r[55] : (p9 = [
@@ -46512,21 +46583,15 @@ ${c}
46512
46583
  };
46513
46584
  }
46514
46585
  function _temp2$15(e) {
46515
- return filterToFilterCondition(e.id, e.value);
46516
- }
46517
- function _temp3$10(e) {
46518
46586
  Logger.error(e);
46519
46587
  }
46520
- function _temp4$6(e) {
46588
+ function _temp3$10(e) {
46521
46589
  return {
46522
46590
  by: e.id,
46523
46591
  descending: e.desc
46524
46592
  };
46525
46593
  }
46526
- function _temp5$4(e) {
46527
- return filterToFilterCondition(e.id, e.value);
46528
- }
46529
- function _temp6$4(e) {
46594
+ function _temp4$6(e) {
46530
46595
  return e.pageIndex === 0 ? e : {
46531
46596
  ...e,
46532
46597
  pageIndex: 0
@@ -49109,7 +49174,7 @@ ${c}
49109
49174
  descending: boolean()
49110
49175
  })).optional(),
49111
49176
  query: string().optional(),
49112
- filters: array(ConditionSchema).optional(),
49177
+ filters: FilterGroupSchema.optional(),
49113
49178
  page_number: number(),
49114
49179
  page_size: number()
49115
49180
  })).output(object({
@@ -68770,7 +68835,7 @@ ${c}
68770
68835
  return Logger.warn("Failed to get version from mount config"), null;
68771
68836
  }
68772
68837
  }
68773
- const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.23.2-dev12"), showCodeInRunModeAtom = atom(true);
68838
+ const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.23.2-dev14"), showCodeInRunModeAtom = atom(true);
68774
68839
  atom(null);
68775
68840
  var VIRTUAL_FILE_REGEX = /\/@file\/([^\s"&'/]+)\.([\dA-Za-z]+)/g, VirtualFileTracker = class e {
68776
68841
  constructor() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.23.2-dev12",
3
+ "version": "0.23.2-dev14",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -0,0 +1,304 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { describe, expect, it } from "vitest";
3
+ import {
4
+ filterToFilterCondition,
5
+ filtersToFilterGroup,
6
+ Filter,
7
+ } from "../filters";
8
+ import {
9
+ FilterConditionSchema,
10
+ FilterGroupSchema,
11
+ } from "@/plugins/impl/data-frames/schema";
12
+
13
+ describe("filterToFilterCondition", () => {
14
+ it("returns empty array for undefined filter", () => {
15
+ expect(filterToFilterCondition("col", undefined)).toEqual([]);
16
+ });
17
+
18
+ it("handles is_null filter", () => {
19
+ const result = filterToFilterCondition(
20
+ "col",
21
+ Filter.number({ operator: "is_null" }),
22
+ );
23
+ expect(result).toEqual([
24
+ {
25
+ column_id: "col",
26
+ operator: "is_null",
27
+ value: undefined,
28
+ type: "condition",
29
+ negate: false,
30
+ },
31
+ ]);
32
+ });
33
+
34
+ it("handles is_not_null filter", () => {
35
+ const result = filterToFilterCondition(
36
+ "col",
37
+ Filter.number({ operator: "is_not_null" }),
38
+ );
39
+ expect(result).toEqual([
40
+ {
41
+ column_id: "col",
42
+ operator: "is_not_null",
43
+ value: undefined,
44
+ type: "condition",
45
+ negate: false,
46
+ },
47
+ ]);
48
+ });
49
+
50
+ it("handles number filter with min only", () => {
51
+ const result = filterToFilterCondition("age", Filter.number({ min: 18 }));
52
+ expect(result).toHaveLength(1);
53
+ expect(result[0]).toMatchObject({
54
+ column_id: "age",
55
+ operator: ">=",
56
+ value: 18,
57
+ type: "condition",
58
+ negate: false,
59
+ });
60
+ });
61
+
62
+ it("handles number filter with max only", () => {
63
+ const result = filterToFilterCondition("age", Filter.number({ max: 65 }));
64
+ expect(result).toHaveLength(1);
65
+ expect(result[0]).toMatchObject({
66
+ column_id: "age",
67
+ operator: "<=",
68
+ value: 65,
69
+ type: "condition",
70
+ negate: false,
71
+ });
72
+ });
73
+
74
+ it("handles number filter with min and max", () => {
75
+ const result = filterToFilterCondition(
76
+ "age",
77
+ Filter.number({ min: 18, max: 65 }),
78
+ );
79
+ expect(result).toHaveLength(2);
80
+ expect(result[0]).toMatchObject({ operator: ">=", value: 18 });
81
+ expect(result[1]).toMatchObject({ operator: "<=", value: 65 });
82
+ });
83
+
84
+ it("handles text filter", () => {
85
+ const result = filterToFilterCondition(
86
+ "name",
87
+ Filter.text({ text: "foo", operator: "contains" }),
88
+ );
89
+ expect(result).toEqual([
90
+ {
91
+ column_id: "name",
92
+ operator: "contains",
93
+ value: "foo",
94
+ type: "condition",
95
+ negate: false,
96
+ },
97
+ ]);
98
+ });
99
+
100
+ it("handles boolean true filter", () => {
101
+ const result = filterToFilterCondition(
102
+ "active",
103
+ Filter.boolean({ value: true }),
104
+ );
105
+ expect(result).toEqual([
106
+ {
107
+ column_id: "active",
108
+ operator: "is_true",
109
+ value: undefined,
110
+ type: "condition",
111
+ negate: false,
112
+ },
113
+ ]);
114
+ });
115
+
116
+ it("handles boolean false filter", () => {
117
+ const result = filterToFilterCondition(
118
+ "active",
119
+ Filter.boolean({ value: false }),
120
+ );
121
+ expect(result).toEqual([
122
+ {
123
+ column_id: "active",
124
+ operator: "is_false",
125
+ value: undefined,
126
+ type: "condition",
127
+ negate: false,
128
+ },
129
+ ]);
130
+ });
131
+
132
+ it("handles select in filter", () => {
133
+ const result = filterToFilterCondition(
134
+ "status",
135
+ Filter.select({ options: ["a", "b"], operator: "in" }),
136
+ );
137
+ expect(result).toEqual([
138
+ {
139
+ column_id: "status",
140
+ operator: "in",
141
+ value: ["a", "b"],
142
+ type: "condition",
143
+ negate: false,
144
+ },
145
+ ]);
146
+ });
147
+
148
+ it("handles date filter with min and max", () => {
149
+ const min = new Date("2024-01-01");
150
+ const max = new Date("2024-12-31");
151
+ const result = filterToFilterCondition(
152
+ "created",
153
+ Filter.date({ min, max }),
154
+ );
155
+ expect(result).toHaveLength(2);
156
+ expect(result[0]).toMatchObject({
157
+ operator: ">=",
158
+ value: min.toISOString(),
159
+ });
160
+ expect(result[1]).toMatchObject({
161
+ operator: "<=",
162
+ value: max.toISOString(),
163
+ });
164
+ });
165
+
166
+ it("every condition has type and negate fields", () => {
167
+ const result = filterToFilterCondition(
168
+ "col",
169
+ Filter.number({ min: 1, max: 10 }),
170
+ );
171
+ for (const condition of result) {
172
+ expect(condition).toHaveProperty("type", "condition");
173
+ expect(condition).toHaveProperty("negate", false);
174
+ }
175
+ });
176
+ });
177
+
178
+ describe("filtersToFilterGroup", () => {
179
+ it("returns empty AND group for no filters", () => {
180
+ const result = filtersToFilterGroup([]);
181
+ expect(result).toEqual({
182
+ type: "group",
183
+ operator: "and",
184
+ children: [],
185
+ negate: false,
186
+ });
187
+ });
188
+
189
+ it("wraps single filter in AND group", () => {
190
+ const result = filtersToFilterGroup([
191
+ { id: "age", value: Filter.number({ min: 18 }) },
192
+ ]);
193
+ expect(result.type).toBe("group");
194
+ expect(result.operator).toBe("and");
195
+ expect(result.negate).toBe(false);
196
+ expect(result.children).toHaveLength(1);
197
+ });
198
+
199
+ it("wraps multiple filters in AND group", () => {
200
+ const result = filtersToFilterGroup([
201
+ { id: "age", value: Filter.number({ min: 18 }) },
202
+ { id: "name", value: Filter.text({ text: "foo", operator: "contains" }) },
203
+ ]);
204
+ expect(result.children).toHaveLength(2);
205
+ expect(result.operator).toBe("and");
206
+ });
207
+
208
+ it("flattens multi-condition filters", () => {
209
+ const result = filtersToFilterGroup([
210
+ { id: "age", value: Filter.number({ min: 18, max: 65 }) },
211
+ ]);
212
+ // min + max = 2 conditions
213
+ expect(result.children).toHaveLength(2);
214
+ });
215
+ });
216
+
217
+ describe("schema validation", () => {
218
+ it("FilterConditionSchema accepts valid condition", () => {
219
+ const result = FilterConditionSchema.safeParse({
220
+ column_id: "age",
221
+ operator: ">=",
222
+ value: 18,
223
+ });
224
+ expect(result.success).toBe(true);
225
+ if (result.success) {
226
+ expect(result.data.type).toBe("condition");
227
+ expect(result.data.negate).toBe(false);
228
+ }
229
+ });
230
+
231
+ it("FilterConditionSchema defaults type and negate", () => {
232
+ const result = FilterConditionSchema.safeParse({
233
+ column_id: "age",
234
+ operator: "==",
235
+ value: 5,
236
+ });
237
+ expect(result.success).toBe(true);
238
+ if (result.success) {
239
+ expect(result.data.type).toBe("condition");
240
+ expect(result.data.negate).toBe(false);
241
+ }
242
+ });
243
+
244
+ it("FilterConditionSchema accepts negate=true", () => {
245
+ const result = FilterConditionSchema.safeParse({
246
+ column_id: "age",
247
+ operator: "==",
248
+ value: 5,
249
+ negate: true,
250
+ });
251
+ expect(result.success).toBe(true);
252
+ if (result.success) {
253
+ expect(result.data.negate).toBe(true);
254
+ }
255
+ });
256
+
257
+ it("FilterGroupSchema accepts valid group", () => {
258
+ const result = FilterGroupSchema.safeParse({
259
+ type: "group",
260
+ operator: "and",
261
+ children: [{ column_id: "age", operator: ">=", value: 18 }],
262
+ });
263
+ expect(result.success).toBe(true);
264
+ });
265
+
266
+ it("FilterGroupSchema accepts nested groups", () => {
267
+ const result = FilterGroupSchema.safeParse({
268
+ type: "group",
269
+ operator: "or",
270
+ children: [
271
+ {
272
+ type: "group",
273
+ operator: "and",
274
+ children: [
275
+ { column_id: "a", operator: "==", value: 1 },
276
+ { column_id: "b", operator: ">", value: 2 },
277
+ ],
278
+ },
279
+ { column_id: "c", operator: "==", value: 3 },
280
+ ],
281
+ });
282
+ expect(result.success).toBe(true);
283
+ });
284
+
285
+ it("FilterGroupSchema rejects invalid operator", () => {
286
+ const result = FilterGroupSchema.safeParse({
287
+ type: "group",
288
+ operator: "xor",
289
+ children: [],
290
+ });
291
+ expect(result.success).toBe(false);
292
+ });
293
+
294
+ it("FilterGroupSchema defaults fields", () => {
295
+ const result = FilterGroupSchema.safeParse({});
296
+ expect(result.success).toBe(true);
297
+ if (result.success) {
298
+ expect(result.data.type).toBe("group");
299
+ expect(result.data.operator).toBe("and");
300
+ expect(result.data.children).toEqual([]);
301
+ expect(result.data.negate).toBe(false);
302
+ }
303
+ });
304
+ });
@@ -1,9 +1,12 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  "use no memo";
3
3
 
4
- import type { RowData } from "@tanstack/react-table";
4
+ import type { ColumnFiltersState, RowData } from "@tanstack/react-table";
5
5
  import type { DataType } from "@/core/kernel/messages";
6
- import type { ConditionType } from "@/plugins/impl/data-frames/schema";
6
+ import type {
7
+ FilterConditionType,
8
+ FilterGroupType,
9
+ } from "@/plugins/impl/data-frames/schema";
7
10
  import type { ColumnId } from "@/plugins/impl/data-frames/types";
8
11
  import type { OperatorType } from "@/plugins/impl/data-frames/utils/operators";
9
12
  import { assertNever } from "@/utils/assertNever";
@@ -84,28 +87,34 @@ export type ColumnFilterForType<T extends FilterType> = T extends FilterType
84
87
  export function filterToFilterCondition(
85
88
  columnIdString: string,
86
89
  filter: ColumnFilterValue | undefined,
87
- ): ConditionType[] | ConditionType {
90
+ ): FilterConditionType[] {
88
91
  if (!filter) {
89
92
  return [];
90
93
  }
91
94
  const columnId = columnIdString as ColumnId;
92
95
 
93
96
  if (filter.operator === "is_null" || filter.operator === "is_not_null") {
94
- return {
95
- column_id: columnId,
96
- operator: filter.operator,
97
- value: undefined,
98
- };
97
+ return [
98
+ {
99
+ column_id: columnId,
100
+ operator: filter.operator,
101
+ value: undefined,
102
+ type: "condition",
103
+ negate: false,
104
+ },
105
+ ];
99
106
  }
100
107
 
101
108
  switch (filter.type) {
102
109
  case "number": {
103
- const conditions: ConditionType[] = [];
110
+ const conditions: FilterConditionType[] = [];
104
111
  if (filter.min !== undefined) {
105
112
  conditions.push({
106
113
  column_id: columnId,
107
114
  operator: ">=",
108
115
  value: filter.min,
116
+ type: "condition",
117
+ negate: false,
109
118
  });
110
119
  }
111
120
  if (filter.max !== undefined) {
@@ -113,23 +122,31 @@ export function filterToFilterCondition(
113
122
  column_id: columnId,
114
123
  operator: "<=",
115
124
  value: filter.max,
125
+ type: "condition",
126
+ negate: false,
116
127
  });
117
128
  }
118
129
  return conditions;
119
130
  }
120
131
  case "text":
121
- return {
122
- column_id: columnId,
123
- operator: filter.operator,
124
- value: filter.text,
125
- };
132
+ return [
133
+ {
134
+ column_id: columnId,
135
+ operator: filter.operator,
136
+ value: filter.text,
137
+ type: "condition",
138
+ negate: false,
139
+ },
140
+ ];
126
141
  case "datetime": {
127
- const conditions: ConditionType[] = [];
142
+ const conditions: FilterConditionType[] = [];
128
143
  if (filter.min !== undefined) {
129
144
  conditions.push({
130
145
  column_id: columnId,
131
146
  operator: ">=",
132
147
  value: filter.min.toISOString(),
148
+ type: "condition",
149
+ negate: false,
133
150
  });
134
151
  }
135
152
  if (filter.max !== undefined) {
@@ -137,17 +154,21 @@ export function filterToFilterCondition(
137
154
  column_id: columnId,
138
155
  operator: "<=",
139
156
  value: filter.max.toISOString(),
157
+ type: "condition",
158
+ negate: false,
140
159
  });
141
160
  }
142
161
  return conditions;
143
162
  }
144
163
  case "date": {
145
- const conditions: ConditionType[] = [];
164
+ const conditions: FilterConditionType[] = [];
146
165
  if (filter.min !== undefined) {
147
166
  conditions.push({
148
167
  column_id: columnId,
149
168
  operator: ">=",
150
169
  value: filter.min.toISOString(),
170
+ type: "condition",
171
+ negate: false,
151
172
  });
152
173
  }
153
174
  if (filter.max !== undefined) {
@@ -155,17 +176,21 @@ export function filterToFilterCondition(
155
176
  column_id: columnId,
156
177
  operator: "<=",
157
178
  value: filter.max.toISOString(),
179
+ type: "condition",
180
+ negate: false,
158
181
  });
159
182
  }
160
183
  return conditions;
161
184
  }
162
185
  case "time": {
163
- const conditions: ConditionType[] = [];
186
+ const conditions: FilterConditionType[] = [];
164
187
  if (filter.min !== undefined) {
165
188
  conditions.push({
166
189
  column_id: columnId,
167
190
  operator: ">=",
168
191
  value: filter.min.toISOString(),
192
+ type: "condition",
193
+ negate: false,
169
194
  });
170
195
  }
171
196
  if (filter.max !== undefined) {
@@ -173,24 +198,34 @@ export function filterToFilterCondition(
173
198
  column_id: columnId,
174
199
  operator: "<=",
175
200
  value: filter.max.toISOString(),
201
+ type: "condition",
202
+ negate: false,
176
203
  });
177
204
  }
178
205
  return conditions;
179
206
  }
180
207
  case "boolean":
181
208
  if (filter.value) {
182
- return {
183
- column_id: columnId,
184
- operator: "is_true",
185
- value: undefined,
186
- };
209
+ return [
210
+ {
211
+ column_id: columnId,
212
+ operator: "is_true",
213
+ value: undefined,
214
+ type: "condition",
215
+ negate: false,
216
+ },
217
+ ];
187
218
  }
188
219
  if (!filter.value) {
189
- return {
190
- column_id: columnId,
191
- operator: "is_false",
192
- value: undefined,
193
- };
220
+ return [
221
+ {
222
+ column_id: columnId,
223
+ operator: "is_false",
224
+ value: undefined,
225
+ type: "condition",
226
+ negate: false,
227
+ },
228
+ ];
194
229
  }
195
230
 
196
231
  return [];
@@ -200,16 +235,35 @@ export function filterToFilterCondition(
200
235
  Logger.warn("Invalid operator for select filter", {
201
236
  operator: filter.operator,
202
237
  });
203
- operator = "in"; // default to in operator
238
+ operator = "in";
204
239
  }
205
- return {
206
- column_id: columnId,
207
- operator,
208
- value: filter.options,
209
- };
240
+ return [
241
+ {
242
+ column_id: columnId,
243
+ operator,
244
+ value: filter.options,
245
+ type: "condition",
246
+ negate: false,
247
+ },
248
+ ];
210
249
  }
211
250
 
212
251
  default:
213
252
  assertNever(filter);
214
253
  }
215
254
  }
255
+
256
+ export function filtersToFilterGroup(
257
+ columnFilters: ColumnFiltersState,
258
+ ): FilterGroupType {
259
+ const conditions = columnFilters.flatMap((filter) =>
260
+ filterToFilterCondition(filter.id, filter.value as ColumnFilterValue),
261
+ );
262
+ // To maintain existing behavior "and" all the conditions
263
+ return {
264
+ type: "group",
265
+ operator: "and",
266
+ children: conditions,
267
+ negate: false,
268
+ };
269
+ }
@@ -9,9 +9,7 @@ exports[`snapshot all duplicate keymaps > default keymaps 2`] = `
9
9
  },
10
10
  {
11
11
  "key": "ArrowDown",
12
- "preventDefault": true,
13
12
  "run": "run",
14
- "stopPropagation": true,
15
13
  },
16
14
  {
17
15
  "key": "ArrowDown",
@@ -27,9 +25,7 @@ exports[`snapshot all duplicate keymaps > default keymaps 2`] = `
27
25
  },
28
26
  {
29
27
  "key": "ArrowUp",
30
- "preventDefault": true,
31
28
  "run": "run",
32
- "stopPropagation": true,
33
29
  },
34
30
  {
35
31
  "key": "ArrowUp",
@@ -118,15 +114,12 @@ exports[`snapshot all duplicate keymaps > vim keymaps 2`] = `
118
114
  },
119
115
  {
120
116
  "key": "ArrowDown",
121
- "preventDefault": true,
122
117
  "run": "run",
123
- "stopPropagation": true,
124
118
  },
125
119
  {
126
120
  "key": "ArrowDown",
127
- "preventDefault": true,
128
- "run": "cursorLineDown",
129
- "shift": "selectLineDown",
121
+ "run": "<no name>",
122
+ "shift": "<no name>",
130
123
  },
131
124
  ],
132
125
  "ArrowUp": [
@@ -136,15 +129,12 @@ exports[`snapshot all duplicate keymaps > vim keymaps 2`] = `
136
129
  },
137
130
  {
138
131
  "key": "ArrowUp",
139
- "preventDefault": true,
140
132
  "run": "run",
141
- "stopPropagation": true,
142
133
  },
143
134
  {
144
135
  "key": "ArrowUp",
145
- "preventDefault": true,
146
- "run": "cursorLineUp",
147
- "shift": "selectLineUp",
136
+ "run": "<no name>",
137
+ "shift": "<no name>",
148
138
  },
149
139
  ],
150
140
  "Backspace": [
@@ -169,8 +169,6 @@ function cellKeymaps({
169
169
  },
170
170
  {
171
171
  key: "ArrowUp",
172
- preventDefault: true,
173
- stopPropagation: true,
174
172
  run: (ev) => {
175
173
  // Skip if we are in the middle of an autocompletion
176
174
  const hasAutocomplete = completionStatus(ev.state);
@@ -188,8 +186,6 @@ function cellKeymaps({
188
186
  },
189
187
  {
190
188
  key: "ArrowDown",
191
- preventDefault: true,
192
- stopPropagation: true,
193
189
  run: (ev) => {
194
190
  // Skip if we are in the middle of an autocompletion
195
191
  const hasAutocomplete = completionStatus(ev.state);
@@ -1,8 +1,16 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import {
4
+ cursorCharLeft,
5
+ cursorCharRight,
6
+ cursorLineDown,
7
+ cursorLineUp,
4
8
  insertNewlineAndIndent,
5
9
  defaultKeymap as originalDefaultKeymap,
10
+ selectCharLeft,
11
+ selectCharRight,
12
+ selectLineDown,
13
+ selectLineUp,
6
14
  toggleBlockComment,
7
15
  toggleComment,
8
16
  } from "@codemirror/commands";
@@ -13,7 +21,7 @@ import {
13
21
  type KeyBinding,
14
22
  keymap,
15
23
  } from "@codemirror/view";
16
- import { getCM, vim } from "@replit/codemirror-vim";
24
+ import { type CodeMirror, getCM, vim } from "@replit/codemirror-vim";
17
25
  import type { KeymapConfig } from "@/core/config/config-schema";
18
26
  import type { HotkeyProvider } from "@/core/hotkeys/hotkeys";
19
27
  import { logNever } from "@/utils/assertNever";
@@ -62,6 +70,12 @@ export function keymapBundle(
62
70
  },
63
71
  ),
64
72
  ),
73
+ // Arrow keys: use CodeMirror's cursor movement except in vim visual
74
+ // mode, where vim must handle them to maintain selection.
75
+ // The original cursorLineUp/Down bindings from the default keymap are
76
+ // filtered out (see defaultVimKeymap) because their preventDefault
77
+ // flag blocks vim's handler even when their run function returns false.
78
+ keymap.of(vimVisualModeArrowKeyBindings()),
65
79
  // Base vim mode
66
80
  vim({ status: false }),
67
81
  // Custom vim keymaps for cell navigation
@@ -101,12 +115,22 @@ const overrideKeymap = (keymap: HotkeyProvider): readonly KeyBinding[] => {
101
115
  };
102
116
 
103
117
  const defaultVimKeymap = once(() => {
104
- const toRemove = new Set(["Enter", "Ctrl-v"]);
118
+ const toRemove = new Set([
119
+ "Enter",
120
+ "Ctrl-v",
121
+ "ArrowUp",
122
+ "ArrowDown",
123
+ "ArrowLeft",
124
+ "ArrowRight",
125
+ ]);
105
126
  // Remove conflicting keys from the keymap
106
127
  // Enter (<CR>) adds a new line
107
128
  // - it should just go to the next line
108
129
  // Ctrl-v goes to the bottom of the cell
109
130
  // - should enter blockwise visual mode
131
+ // ArrowUp/ArrowDown (cursorLineUp/Down) always handle the event and have
132
+ // preventDefault, which blocks vim's handler from processing arrow keys.
133
+ // Replaced with visual-mode-aware wrappers in keymapBundle.
110
134
  return defaultKeymap().filter(
111
135
  (k) => !toRemove.has(k.key || k.mac || k.linux || k.win || ""),
112
136
  );
@@ -155,6 +179,49 @@ function doubleCharacterListener(
155
179
  ]);
156
180
  }
157
181
 
182
+ function isInVimVisualMode(cm: CodeMirror | undefined | null): boolean {
183
+ return cm?.state.vim?.visualMode === true;
184
+ }
185
+
186
+ /**
187
+ * In vim visual mode, arrow keys must be handled by vim to maintain selection.
188
+ * Wrap each arrow key's run and shift so they defer to vim in visual mode,
189
+ * but use CodeMirror's cursor commands in all other modes.
190
+ */
191
+ function vimVisualModeArrowKeyBindings(): KeyBinding[] {
192
+ const wrap =
193
+ (cmd: Command): Command =>
194
+ (view) => {
195
+ if (isInVimVisualMode(getCM(view))) {
196
+ return false;
197
+ }
198
+ return cmd(view);
199
+ };
200
+
201
+ return [
202
+ {
203
+ key: "ArrowDown",
204
+ run: wrap(cursorLineDown),
205
+ shift: wrap(selectLineDown),
206
+ },
207
+ {
208
+ key: "ArrowUp",
209
+ run: wrap(cursorLineUp),
210
+ shift: wrap(selectLineUp),
211
+ },
212
+ {
213
+ key: "ArrowLeft",
214
+ run: wrap(cursorCharLeft),
215
+ shift: wrap(selectCharLeft),
216
+ },
217
+ {
218
+ key: "ArrowRight",
219
+ run: wrap(cursorCharRight),
220
+ shift: wrap(selectCharRight),
221
+ },
222
+ ];
223
+ }
224
+
158
225
  export const visibleForTesting = {
159
226
  defaultKeymap,
160
227
  defaultVimKeymap,
@@ -28,10 +28,7 @@ import { TablePanel } from "@/components/data-table/charts/charts";
28
28
  import { hasChart } from "@/components/data-table/charts/storage";
29
29
  import { ColumnChartSpecModel } from "@/components/data-table/column-summary/chart-spec-model";
30
30
  import { ColumnChartContext } from "@/components/data-table/column-summary/column-summary";
31
- import {
32
- type ColumnFilterValue,
33
- filterToFilterCondition,
34
- } from "@/components/data-table/filters";
31
+ import { filtersToFilterGroup } from "@/components/data-table/filters";
35
32
  import { usePanelOwnership } from "@/components/data-table/hooks/use-panel-ownership";
36
33
  import { LoadingTable } from "@/components/data-table/loading-table";
37
34
  import {
@@ -86,8 +83,8 @@ import { rpc } from "../core/rpc";
86
83
  import { Banner } from "./common/error-banner";
87
84
  import { Labeled } from "./common/labeled";
88
85
  import {
89
- ConditionSchema,
90
- type ConditionType,
86
+ FilterGroupSchema,
87
+ type FilterGroupType,
91
88
  columnToFieldTypesSchema,
92
89
  } from "./data-frames/schema";
93
90
 
@@ -213,7 +210,7 @@ type DataTableFunctions = {
213
210
  descending: boolean;
214
211
  }[];
215
212
  query?: string;
216
- filters?: ConditionType[];
213
+ filters?: FilterGroupType;
217
214
  page_number: number;
218
215
  page_size: number;
219
216
  max_columns?: number | null;
@@ -312,7 +309,7 @@ export const DataTablePlugin = createPlugin<S>("marimo-table")
312
309
  )
313
310
  .optional(),
314
311
  query: z.string().optional(),
315
- filters: z.array(ConditionSchema).optional(),
312
+ filters: FilterGroupSchema.optional(),
316
313
  page_number: z.number(),
317
314
  page_size: z.number(),
318
315
  max_columns: z.number().nullable().optional(),
@@ -578,12 +575,7 @@ export const LoadingDataTableComponent = memo(
578
575
  query: searchQuery,
579
576
  page_number: paginationState.pageIndex,
580
577
  page_size: paginationState.pageSize,
581
- filters: filters.flatMap((filter) => {
582
- return filterToFilterCondition(
583
- filter.id,
584
- filter.value as ColumnFilterValue,
585
- );
586
- }),
578
+ filters: filtersToFilterGroup(filters),
587
579
  });
588
580
 
589
581
  if (canShowInitialPage) {
@@ -641,12 +633,7 @@ export const LoadingDataTableComponent = memo(
641
633
  page_size: 1,
642
634
  sort: sortArgs,
643
635
  query: searchQuery,
644
- filters: filters.flatMap((filter) => {
645
- return filterToFilterCondition(
646
- filter.id,
647
- filter.value as ColumnFilterValue,
648
- );
649
- }),
636
+ filters: filtersToFilterGroup(filters),
650
637
  // Do not clamp number of columns since we are viewing a single row
651
638
  max_columns: null,
652
639
  });
@@ -29,8 +29,8 @@ import { LoadingDataTableComponent, TableProviders } from "../DataTablePlugin";
29
29
  import type { DataType } from "../vega/vega-loader";
30
30
  import { TransformPanel, type TransformPanelHandle } from "./panel";
31
31
  import {
32
- ConditionSchema,
33
- type ConditionType,
32
+ FilterGroupSchema,
33
+ type FilterGroupType,
34
34
  columnToFieldTypesSchema,
35
35
  type Transformations,
36
36
  } from "./schema";
@@ -75,7 +75,7 @@ type PluginFunctions = {
75
75
  descending: boolean;
76
76
  }[];
77
77
  query?: string;
78
- filters?: ConditionType[];
78
+ filters?: FilterGroupType;
79
79
  page_number: number;
80
80
  page_size: number;
81
81
  }) => Promise<{
@@ -138,7 +138,7 @@ export const DataFramePlugin = createPlugin<S>("marimo-dataframe")
138
138
  )
139
139
  .optional(),
140
140
  query: z.string().optional(),
141
- filters: z.array(ConditionSchema).optional(),
141
+ filters: FilterGroupSchema.optional(),
142
142
  page_number: z.number(),
143
143
  page_size: z.number(),
144
144
  }),
@@ -73,16 +73,36 @@ const SortColumnTransformSchema = z.object({
73
73
  .default("last"),
74
74
  });
75
75
 
76
- export const ConditionSchema = z
76
+ export const FilterConditionSchema = z
77
77
  .object({
78
78
  column_id: column_id,
79
79
  operator: z
80
80
  .enum(Object.keys(ALL_OPERATORS) as [OperatorType, ...OperatorType[]])
81
81
  .describe(FieldOptions.of({ label: " " })),
82
+ type: z.literal("condition").default("condition"),
82
83
  value: z.any().describe(FieldOptions.of({ label: "Value" })),
84
+ negate: z.boolean().default(false),
83
85
  })
84
86
  .describe(FieldOptions.of({ direction: "row", special: "column_filter" }));
85
- export type ConditionType = z.infer<typeof ConditionSchema>;
87
+ export type FilterConditionType = z.infer<typeof FilterConditionSchema>;
88
+
89
+ export interface FilterGroupType {
90
+ type: "group";
91
+ operator: "and" | "or";
92
+ children: (FilterConditionType | FilterGroupType)[];
93
+ negate: boolean;
94
+ }
95
+
96
+ export const FilterGroupSchema: z.ZodType<FilterGroupType> = z.lazy(() =>
97
+ z.object({
98
+ type: z.literal("group").default("group"),
99
+ operator: z.enum(["and", "or"]).default("and"),
100
+ children: z
101
+ .array(z.union([FilterConditionSchema, FilterGroupSchema]))
102
+ .default([]),
103
+ negate: z.boolean().default(false),
104
+ }),
105
+ );
86
106
 
87
107
  const FilterRowsTransformSchema = z.object({
88
108
  type: z.literal("filter_rows"),
@@ -91,17 +111,29 @@ const FilterRowsTransformSchema = z.object({
91
111
  .default("keep_rows")
92
112
  .describe(FieldOptions.of({ special: "radio_group" })),
93
113
  where: z
94
- .array(ConditionSchema)
114
+ .array(FilterConditionSchema)
95
115
  .min(1)
96
116
  .describe(FieldOptions.of({ label: "Value", minLength: 1 }))
97
- .transform((value) => {
98
- return value.filter((condition) => {
117
+ .default(() => [
118
+ {
119
+ column_id: "" as ColumnId,
120
+ operator: "==" as const,
121
+ value: "",
122
+ type: "condition" as const,
123
+ negate: false,
124
+ },
125
+ ])
126
+ .transform((value): FilterGroupType => {
127
+ const validConditions = value.filter((condition) => {
99
128
  return isConditionValueValid(condition.operator, condition.value);
100
129
  });
101
- })
102
- .default(() => [
103
- { column_id: "" as ColumnId, operator: "==" as const, value: "" },
104
- ]),
130
+ return {
131
+ type: "group",
132
+ operator: "and",
133
+ children: validConditions,
134
+ negate: false,
135
+ };
136
+ }),
105
137
  });
106
138
 
107
139
  const GroupByTransformSchema = z
@@ -41,6 +41,7 @@ const createComparisonOperators = (schema: z.ZodType) => ({
41
41
  ">=": [schema],
42
42
  "<": [schema],
43
43
  "<=": [schema],
44
+ between: [z.object({ min: schema, max: schema })],
44
45
  is_null: [],
45
46
  is_not_null: [],
46
47
  });
@@ -59,6 +60,7 @@ export const STRING_OPERATORS = {
59
60
  ends_with: [Schema.string],
60
61
  in: [Schema.stringMultiColumnValues],
61
62
  not_in: [Schema.stringMultiColumnValues],
63
+ is_empty: [],
62
64
  is_null: [],
63
65
  is_not_null: [],
64
66
  };