Skip to content

Commit d419826

Browse files
os-zhuangclaude
andauthored
fix(spec): enforce ViewFilterRule operator enum with legacy-alias normalization (#3373)
* fix(spec): enforce ViewFilterRule operator enum with legacy-alias normalization Replace the free-form `operator: z.string()` on ViewFilterRuleSchema with a canonical `z.enum` (VIEW_FILTER_OPERATORS) wrapped in `z.preprocess` that folds legacy shorthand/camelCase spellings (eq, gt, isNull, notEquals, …) to canonical on parse. This surfaces a clean operator enum to JSON-Schema consumers (ObjectUI SchemaForm renders a dropdown) and rejects genuinely unknown operators, while already-stored metadata keeps validating and upgrades to one vocabulary. - Export VIEW_FILTER_OPERATORS, ViewFilterOperator, VIEW_FILTER_OPERATOR_ALIASES and a normalizeFilterOperator() helper as the single source of truth. - Pin `widget: 'filter-builder'` on the list view `filter` field in view.form.ts, matching dataset/page forms so the visual builder renders instead of a repeater. - Canonicalize plugin-sharing sys_share_link filters (isNull -> is_null, isNotNull -> is_not_null), preserving null semantics. - Keep the legacy string `sort` form (deprecation note only); retain the objectui#2601 regression fixture. - Relative-date operators (this_quarter, …) are not filter-rule operators; tests updated to canonical operators and alias-normalization / unknown-rejection coverage added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PwbhoMeqh33xFWq5M77QTJ * docs(spec): regenerate view/component/page references for operator enum Auto-generated reference docs reflect the ViewFilterRule operator enum (replacing the free-form `string`). Regenerated via gen:schema && gen:docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PwbhoMeqh33xFWq5M77QTJ * chore(spec): update api-surface snapshot for new ViewFilterRule exports Additive-only: VIEW_FILTER_OPERATORS, VIEW_FILTER_OPERATOR_ALIASES, ViewFilterOperator, normalizeFilterOperator (0 breaking, 4 added). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PwbhoMeqh33xFWq5M77QTJ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9833822 commit d419826

8 files changed

Lines changed: 122 additions & 18 deletions

File tree

content/docs/references/ui/component.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ Type: `string`
366366
| **columns** | `string[]` | optional | Fields to display in the related list. Optional: when omitted, columns derive from the related object's highlightFields / default list columns (a related list is just another surface that lists that object). Override chain: child highlightFields → field-level relatedListColumns → this inline list. |
367367
| **sort** | `string \| { field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Sort order for related records |
368368
| **limit** | `integer` || Number of records to display initially |
369-
| **filter** | `{ field: string; operator: string; value?: string \| number \| boolean \| null \| string \| number[] }[]` | optional | Additional filter criteria for related records |
369+
| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'starts_with' \| 'ends_with' \| 'greater_than' \| 'less_than' \| 'greater_than_or_equal' \| 'less_than_or_equal' \| 'in' \| 'not_in' \| 'is_empty' \| 'is_not_empty' \| 'is_null' \| 'is_not_null' \| 'before' \| 'after' \| 'between'>; value?: string \| number \| boolean \| null \| string \| number[] }[]` | optional | Additional filter criteria for related records |
370370
| **title** | `string` | optional | Custom title for the related list |
371371
| **showViewAll** | `boolean` || Show "View All" link to see all related records |
372372
| **actions** | `string[]` | optional | Action IDs available for related records |

content/docs/references/ui/page.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ Interface-level page configuration (Airtable parity)
5151
| **source** | `string` | optional | Source object name for the page |
5252
| **columns** | `string[] \| { field: string; label?: string; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | optional | Columns shown by the page. Blank = all object fields. Defined directly on the page (no view inheritance). |
5353
| **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Default sort order for the page, defined directly on the page. |
54-
| **filterBy** | `{ field: string; operator: string; value?: string \| number \| boolean \| null \| string \| number[] }[]` | optional | Always-on page filter (base filter). |
54+
| **filterBy** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'starts_with' \| 'ends_with' \| 'greater_than' \| 'less_than' \| 'greater_than_or_equal' \| 'less_than_or_equal' \| 'in' \| 'not_in' \| 'is_empty' \| 'is_not_empty' \| 'is_null' \| 'is_not_null' \| 'before' \| 'after' \| 'between'>; value?: string \| number \| boolean \| null \| string \| number[] }[]` | optional | Always-on page filter (base filter). |
5555
| **levels** | `integer` | optional | Number of hierarchy levels to display |
5656
| **sourceView** | `string` | optional | @deprecated Legacy named-view inheritance. Define columns/sort/filterBy on the page instead. |
5757
| **appearance** | `{ showDescription: boolean; allowedVisualizations?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[] }` | optional | Appearance and visualization configuration |
@@ -83,7 +83,7 @@ Interface-level page configuration (Airtable parity)
8383
| **regions** | `{ name: string; width?: Enum<'small' \| 'medium' \| 'large' \| 'full'>; components: { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| 'page:card' \| 'page:section' \| 'record:details' \| 'record:highlights' \| 'record:related_list' \| 'record:activity' \| 'record:chatter' \| 'record:path' \| 'record:alert' \| 'record:quick_actions' \| 'record:reference_rail' \| 'record:history' \| 'app:launcher' \| 'nav:menu' \| 'nav:breadcrumb' \| 'global:search' \| 'global:notifications' \| 'user:profile' \| 'ai:chat_window' \| 'ai:suggestion' \| 'element:text' \| 'element:number' \| 'element:image' \| 'element:divider' \| 'element:button' \| 'element:filter' \| 'element:form' \| 'element:record_picker' \| 'element:text_input'> \| string; id?: string; label?: string; properties?: Record<string, any>; … }[] }[]` | optional | Layout regions (header, main, sidebar, footer) with their components. Optional — list pages use interfaceConfig, slotted pages use slots, and an empty full page falls back to the synthesized default layout. |
8484
| **isDefault** | `boolean` | optional | |
8585
| **assignedProfiles** | `string[]` | optional | |
86-
| **interfaceConfig** | `{ source?: string; columns?: string[] \| { field: string; label?: string; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]; sort?: { field: string; order: Enum<'asc' \| 'desc'> }[]; filterBy?: { field: string; operator: string; value?: string \| number \| boolean \| null \| string \| number[] }[]; … }` | optional | Interface-level page configuration (for Airtable-style interface pages) |
86+
| **interfaceConfig** | `{ source?: string; columns?: string[] \| { field: string; label?: string; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]; sort?: { field: string; order: Enum<'asc' \| 'desc'> }[]; filterBy?: { field: string; operator?: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'starts_with' \| 'ends_with' \| 'greater_than' \| 'less_than' \| 'greater_than_or_equal' \| 'less_than_or_equal' \| 'in' \| 'not_in' \| 'is_empty' \| 'is_not_empty' \| 'is_null' \| 'is_not_null' \| 'before' \| 'after' \| 'between'>; value?: string \| number \| boolean \| null \| string \| number[] }[]; … }` | optional | Interface-level page configuration (for Airtable-style interface pages) |
8787
| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes |
8888
| **kind** | `Enum<'full' \| 'slotted' \| 'html' \| 'react' \| 'jsx'>` | optional | Page override mode. full \| slotted = structured authoring; html = author-written constrained JSX/HTML+Tailwind compiled (parsed, never executed) to the tree (ADR-0080; the legacy value 'jsx' is a deprecated alias); react = real-React source executed at render by the runtime (ADR-0081); it runs author JS, so it is gated by a host capability that defaults ON and is disabled server-side via the OS_PAGE_REACT=off env toggle. |
8989
| **slots** | `{ header?: { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| 'page:card' \| 'page:section' \| 'record:details' \| 'record:highlights' \| 'record:related_list' \| 'record:activity' \| 'record:chatter' \| 'record:path' \| 'record:alert' \| 'record:quick_actions' \| 'record:reference_rail' \| 'record:history' \| 'app:launcher' \| 'nav:menu' \| 'nav:breadcrumb' \| 'global:search' \| 'global:notifications' \| 'user:profile' \| 'ai:chat_window' \| 'ai:suggestion' \| 'element:text' \| 'element:number' \| 'element:image' \| 'element:divider' \| 'element:button' \| 'element:filter' \| 'element:form' \| 'element:record_picker' \| 'element:text_input'> \| string; id?: string; label?: string; properties?: Record<string, any>; … } \| { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| 'page:card' \| 'page:section' \| 'record:details' \| 'record:highlights' \| 'record:related_list' \| 'record:activity' \| 'record:chatter' \| 'record:path' \| 'record:alert' \| 'record:quick_actions' \| 'record:reference_rail' \| 'record:history' \| 'app:launcher' \| 'nav:menu' \| 'nav:breadcrumb' \| 'global:search' \| 'global:notifications' \| 'user:profile' \| 'ai:chat_window' \| 'ai:suggestion' \| 'element:text' \| 'element:number' \| 'element:image' \| 'element:divider' \| 'element:button' \| 'element:filter' \| 'element:form' \| 'element:record_picker' \| 'element:text_input'> \| string; id?: string; label?: string; properties?: Record<string, any>; … }[]; actions?: { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| 'page:card' \| 'page:section' \| 'record:details' \| 'record:highlights' \| 'record:related_list' \| 'record:activity' \| 'record:chatter' \| 'record:path' \| 'record:alert' \| 'record:quick_actions' \| 'record:reference_rail' \| 'record:history' \| 'app:launcher' \| 'nav:menu' \| 'nav:breadcrumb' \| 'global:search' \| 'global:notifications' \| 'user:profile' \| 'ai:chat_window' \| 'ai:suggestion' \| 'element:text' \| 'element:number' \| 'element:image' \| 'element:divider' \| 'element:button' \| 'element:filter' \| 'element:form' \| 'element:record_picker' \| 'element:text_input'> \| string; id?: string; label?: string; properties?: Record<string, any>; … } \| { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| 'page:card' \| 'page:section' \| 'record:details' \| 'record:highlights' \| 'record:related_list' \| 'record:activity' \| 'record:chatter' \| 'record:path' \| 'record:alert' \| 'record:quick_actions' \| 'record:reference_rail' \| 'record:history' \| 'app:launcher' \| 'nav:menu' \| 'nav:breadcrumb' \| 'global:search' \| 'global:notifications' \| 'user:profile' \| 'ai:chat_window' \| 'ai:suggestion' \| 'element:text' \| 'element:number' \| 'element:image' \| 'element:divider' \| 'element:button' \| 'element:filter' \| 'element:form' \| 'element:record_picker' \| 'element:text_input'> \| string; id?: string; label?: string; properties?: Record<string, any>; … }[]; alerts?: { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| 'page:card' \| 'page:section' \| 'record:details' \| 'record:highlights' \| 'record:related_list' \| 'record:activity' \| 'record:chatter' \| 'record:path' \| 'record:alert' \| 'record:quick_actions' \| 'record:reference_rail' \| 'record:history' \| 'app:launcher' \| 'nav:menu' \| 'nav:breadcrumb' \| 'global:search' \| 'global:notifications' \| 'user:profile' \| 'ai:chat_window' \| 'ai:suggestion' \| 'element:text' \| 'element:number' \| 'element:image' \| 'element:divider' \| 'element:button' \| 'element:filter' \| 'element:form' \| 'element:record_picker' \| 'element:text_input'> \| string; id?: string; label?: string; properties?: Record<string, any>; … } \| { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| 'page:card' \| 'page:section' \| 'record:details' \| 'record:highlights' \| 'record:related_list' \| 'record:activity' \| 'record:chatter' \| 'record:path' \| 'record:alert' \| 'record:quick_actions' \| 'record:reference_rail' \| 'record:history' \| 'app:launcher' \| 'nav:menu' \| 'nav:breadcrumb' \| 'global:search' \| 'global:notifications' \| 'user:profile' \| 'ai:chat_window' \| 'ai:suggestion' \| 'element:text' \| 'element:number' \| 'element:image' \| 'element:divider' \| 'element:button' \| 'element:filter' \| 'element:form' \| 'element:record_picker' \| 'element:text_input'> \| string; id?: string; label?: string; properties?: Record<string, any>; … }[]; highlights?: { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| 'page:card' \| 'page:section' \| 'record:details' \| 'record:highlights' \| 'record:related_list' \| 'record:activity' \| 'record:chatter' \| 'record:path' \| 'record:alert' \| 'record:quick_actions' \| 'record:reference_rail' \| 'record:history' \| 'app:launcher' \| 'nav:menu' \| 'nav:breadcrumb' \| 'global:search' \| 'global:notifications' \| 'user:profile' \| 'ai:chat_window' \| 'ai:suggestion' \| 'element:text' \| 'element:number' \| 'element:image' \| 'element:divider' \| 'element:button' \| 'element:filter' \| 'element:form' \| 'element:record_picker' \| 'element:text_input'> \| string; id?: string; label?: string; properties?: Record<string, any>; … } \| { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| 'page:card' \| 'page:section' \| 'record:details' \| 'record:highlights' \| 'record:related_list' \| 'record:activity' \| 'record:chatter' \| 'record:path' \| 'record:alert' \| 'record:quick_actions' \| 'record:reference_rail' \| 'record:history' \| 'app:launcher' \| 'nav:menu' \| 'nav:breadcrumb' \| 'global:search' \| 'global:notifications' \| 'user:profile' \| 'ai:chat_window' \| 'ai:suggestion' \| 'element:text' \| 'element:number' \| 'element:image' \| 'element:divider' \| 'element:button' \| 'element:filter' \| 'element:form' \| 'element:record_picker' \| 'element:text_input'> \| string; id?: string; label?: string; properties?: Record<string, any>; … }[]; … }` | optional | Slot override map for slotted pages |

content/docs/references/ui/view.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ List chart view configuration
341341
| **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>` | optional | |
342342
| **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record<string, any> }` | optional | Data source configuration (defaults to "object" provider) |
343343
| **columns** | `string[] \| { field: string; label?: string; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` || Fields to display as columns |
344-
| **filter** | `{ field: string; operator: string; value?: string \| number \| boolean \| null \| string \| number[] }[]` | optional | Filter criteria (JSON Rules) |
344+
| **filter** | `{ field: string; operator?: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'starts_with' \| 'ends_with' \| 'greater_than' \| 'less_than' \| 'greater_than_or_equal' \| 'less_than_or_equal' \| 'in' \| 'not_in' \| 'is_empty' \| 'is_not_empty' \| 'is_null' \| 'is_not_null' \| 'before' \| 'after' \| 'between'>; value?: string \| number \| boolean \| null \| string \| number[] }[]` | optional | Filter criteria (JSON Rules) |
345345
| **sort** | `string \| { field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | |
346346
| **searchableFields** | `string[]` | optional | Fields enabled for search |
347347
| **filterableFields** | `string[]` | optional | Legacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters |
@@ -430,7 +430,7 @@ List chart view configuration
430430
| **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>` | optional | |
431431
| **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record<string, any> }` | optional | Data source configuration (defaults to "object" provider) |
432432
| **columns** | `string[] \| { field: string; label?: string; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` || Fields to display as columns |
433-
| **filter** | `{ field: string; operator: string; value?: string \| number \| boolean \| null \| string \| number[] }[]` | optional | Filter criteria (JSON Rules) |
433+
| **filter** | `{ field: string; operator?: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'starts_with' \| 'ends_with' \| 'greater_than' \| 'less_than' \| 'greater_than_or_equal' \| 'less_than_or_equal' \| 'in' \| 'not_in' \| 'is_empty' \| 'is_not_empty' \| 'is_null' \| 'is_not_null' \| 'before' \| 'after' \| 'between'>; value?: string \| number \| boolean \| null \| string \| number[] }[]` | optional | Filter criteria (JSON Rules) |
434434
| **sort** | `string \| { field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | |
435435
| **searchableFields** | `string[]` | optional | Fields enabled for search |
436436
| **filterableFields** | `string[]` | optional | Legacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters |
@@ -713,7 +713,7 @@ View filter rule
713713
| Property | Type | Required | Description |
714714
| :--- | :--- | :--- | :--- |
715715
| **field** | `string` || Field name to filter on |
716-
| **operator** | `string` || Filter operator (e.g. equals, not_equals, contains, this_quarter) |
716+
| **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'starts_with' \| 'ends_with' \| 'greater_than' \| 'less_than' \| 'greater_than_or_equal' \| 'less_than_or_equal' \| 'in' \| 'not_in' \| 'is_empty' \| 'is_not_empty' \| 'is_null' \| 'is_not_null' \| 'before' \| 'after' \| 'between'>` || Filter operator |
717717
| **value** | `string \| number \| boolean \| null \| string \| number[]` | optional | Filter value |
718718

719719

@@ -836,7 +836,7 @@ Tab configuration for multi-tab view interface
836836
| **label** | `string` | optional | Display label |
837837
| **icon** | `string` | optional | Tab icon name |
838838
| **view** | `string` | optional | Referenced list view name from listViews |
839-
| **filter** | `{ field: string; operator: string; value?: string \| number \| boolean \| null \| string \| number[] }[]` | optional | Tab-specific filter criteria |
839+
| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'starts_with' \| 'ends_with' \| 'greater_than' \| 'less_than' \| 'greater_than_or_equal' \| 'less_than_or_equal' \| 'in' \| 'not_in' \| 'is_empty' \| 'is_not_empty' \| 'is_null' \| 'is_not_null' \| 'before' \| 'after' \| 'between'>; value?: string \| number \| boolean \| null \| string \| number[] }[]` | optional | Tab-specific filter criteria |
840840
| **order** | `integer` | optional | Tab display order |
841841
| **pinned** | `boolean` || Pin tab (cannot be removed by users) |
842842
| **isDefault** | `boolean` || Set as the default active tab |

packages/plugins/plugin-sharing/src/objects/sys-share-link.object.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export const SysShareLink = ObjectSchema.create({
5858
label: 'Active',
5959
data: { provider: 'object', object: 'sys_share_link' },
6060
columns: ['object_name', 'record_id', 'permission', 'audience', 'expires_at', 'use_count', 'last_used_at'],
61-
filter: [{ field: 'revoked_at', operator: 'isNull' }],
61+
filter: [{ field: 'revoked_at', operator: 'is_null' }],
6262
sort: [{ field: 'created_at', order: 'desc' }],
6363
pagination: { pageSize: 100 },
6464
},
@@ -78,7 +78,7 @@ export const SysShareLink = ObjectSchema.create({
7878
label: 'Revoked',
7979
data: { provider: 'object', object: 'sys_share_link' },
8080
columns: ['object_name', 'record_id', 'revoked_at', 'created_by'],
81-
filter: [{ field: 'revoked_at', operator: 'isNotNull' }],
81+
filter: [{ field: 'revoked_at', operator: 'is_not_null' }],
8282
sort: [{ field: 'revoked_at', order: 'desc' }],
8383
pagination: { pageSize: 50 },
8484
},

packages/spec/api-surface.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3388,9 +3388,12 @@
33883388
"UserFilterFieldSchema (const)",
33893389
"UserFilters (type)",
33903390
"UserFiltersSchema (const)",
3391+
"VIEW_FILTER_OPERATORS (const)",
3392+
"VIEW_FILTER_OPERATOR_ALIASES (const)",
33913393
"View (type)",
33923394
"ViewData (type)",
33933395
"ViewDataSchema (const)",
3396+
"ViewFilterOperator (type)",
33943397
"ViewFilterRule (type)",
33953398
"ViewFilterRuleSchema (const)",
33963399
"ViewItem (type)",
@@ -3445,6 +3448,7 @@
34453448
"expandViewContainer (function)",
34463449
"expandViewContainerWithDiagnostics (function)",
34473450
"isAggregatedViewContainer (function)",
3451+
"normalizeFilterOperator (function)",
34483452
"pageForm (const)",
34493453
"reportForm (const)",
34503454
"viewForm (const)"

packages/spec/src/ui/view.form.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const viewForm = defineForm({
3737
description: 'What rows show and how users filter them.',
3838
fields: [
3939
{ field: 'columns', type: 'repeater', required: true, helpText: 'Columns to display (field names from selected object)' },
40-
{ field: 'filter', type: 'repeater', helpText: 'Filter conditions' },
40+
{ field: 'filter', widget: 'filter-builder', dependsOn: 'data.object', helpText: 'Filter conditions — same visual builder as the list toolbar, with field-type-aware operators and value inputs' },
4141
{ field: 'sort', type: 'repeater', helpText: 'Default sort order' },
4242
{ field: 'searchableFields', widget: 'string-tags', helpText: 'Field names available for quick search' },
4343
{ field: 'filterableFields', widget: 'string-tags', helpText: 'Field names available for filtering' },

0 commit comments

Comments
 (0)