diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx index bee39e99b..6667e0731 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx @@ -584,6 +584,7 @@ function MetadataResourceEditPageImpl({ // `object`; other types fall back to their own `object`/`objectName`. const sourceObjectName: string | undefined = ((draft as any)?.interfaceConfig?.source as string | undefined) || + ((draft as any)?.data?.object as string | undefined) || ((draft as any)?.object as string | undefined) || ((draft as any)?.objectName as string | undefined); const [objectFields, setObjectFields] = React.useState>([]); diff --git a/packages/app-shell/src/views/metadata-admin/widgets.tsx b/packages/app-shell/src/views/metadata-admin/widgets.tsx index 560a851ec..7ba3cb469 100644 --- a/packages/app-shell/src/views/metadata-admin/widgets.tsx +++ b/packages/app-shell/src/views/metadata-admin/widgets.tsx @@ -1668,22 +1668,34 @@ function ActionMultiWidget({ id, value, onChange, readOnly, context }: WidgetPro /* -------------------------------------------------------------------------- */ /* filter-builder — the SAME runtime FilterBuilder used by the list toolbar, */ -/* reused in Studio for tab presets and the page base filter (unified UX). */ -/* Stored format stays spec ViewFilterRule[] ({field,operator,value}); the */ -/* builder's camelCase operators are mapped at the boundary so the runtime */ -/* (specOperatorToAst) keeps working unchanged. */ +/* reused in Studio for view/tab filters and the page base filter (unified UX).*/ +/* Stored format stays spec ViewFilterRule[] ({field,operator,value}). Writes */ +/* use the CANONICAL @objectstack/spec operator vocabulary (the enum enforced */ +/* by ViewFilterRuleSchema); reads also accept legacy shorthand/camelCase */ +/* spellings still present in already-stored view metadata (gt / eq / isNull). */ /* -------------------------------------------------------------------------- */ const FB_TO_SPEC: Record = { equals: 'equals', notEquals: 'not_equals', contains: 'contains', notContains: 'not_contains', - isEmpty: 'is_empty', isNotEmpty: 'is_not_empty', greaterThan: 'gt', lessThan: 'lt', - greaterOrEqual: 'gte', lessOrEqual: 'lte', before: 'lt', after: 'gt', between: 'between', + isEmpty: 'is_empty', isNotEmpty: 'is_not_empty', + greaterThan: 'greater_than', lessThan: 'less_than', + greaterOrEqual: 'greater_than_or_equal', lessOrEqual: 'less_than_or_equal', + before: 'before', after: 'after', between: 'between', in: 'in', notIn: 'not_in', }; +/** Spec operator → FilterBuilder camelCase. Keys cover both the canonical + * vocabulary and legacy spellings (shorthand + snake/camel) so stored view + * metadata written before canonicalization still seeds the builder. */ const SPEC_TO_FB: Record = { - equals: 'equals', eq: 'equals', not_equals: 'notEquals', ne: 'notEquals', neq: 'notEquals', - contains: 'contains', not_contains: 'notContains', is_empty: 'isEmpty', is_not_empty: 'isNotEmpty', - gt: 'greaterThan', greater_than: 'greaterThan', lt: 'lessThan', less_than: 'lessThan', - gte: 'greaterOrEqual', lte: 'lessOrEqual', in: 'in', not_in: 'notIn', nin: 'notIn', + equals: 'equals', eq: 'equals', + not_equals: 'notEquals', ne: 'notEquals', neq: 'notEquals', notEquals: 'notEquals', + contains: 'contains', not_contains: 'notContains', notContains: 'notContains', + is_empty: 'isEmpty', isEmpty: 'isEmpty', is_not_empty: 'isNotEmpty', isNotEmpty: 'isNotEmpty', + greater_than: 'greaterThan', gt: 'greaterThan', greaterThan: 'greaterThan', + less_than: 'lessThan', lt: 'lessThan', lessThan: 'lessThan', + greater_than_or_equal: 'greaterOrEqual', gte: 'greaterOrEqual', greaterOrEqual: 'greaterOrEqual', + less_than_or_equal: 'lessOrEqual', lte: 'lessOrEqual', lessOrEqual: 'lessOrEqual', + before: 'before', after: 'after', between: 'between', + in: 'in', not_in: 'notIn', nin: 'notIn', notIn: 'notIn', }; interface FilterRuleLite { field: string; operator: string; value?: unknown } @@ -1700,8 +1712,10 @@ function FilterBuilderField({ value, onChange, fields, readOnly }: { logic: 'and' as const, conditions: rules.map((r, i) => ({ id: `c${i}`, - field: r.field, + // Keep the raw operator verbatim if the builder has no camelCase + // equivalent, so it round-trips on save instead of being rewritten. operator: SPEC_TO_FB[r.operator] ?? r.operator ?? 'equals', + field: r.field, value: (r.value as any) ?? '', })), }; diff --git a/packages/plugin-list/src/ListView.tsx b/packages/plugin-list/src/ListView.tsx index b880d45d9..3b7e5bde0 100644 --- a/packages/plugin-list/src/ListView.tsx +++ b/packages/plugin-list/src/ListView.tsx @@ -64,11 +64,12 @@ function mapOperator(op: string) { case 'equals': case 'eq': return '='; case 'notEquals': case 'not_equals': case 'ne': case 'neq': return '!='; case 'contains': return 'contains'; - case 'notContains': case 'notcontains': return 'notcontains'; - case 'greaterThan': case 'gt': return '>'; - case 'greaterOrEqual': case 'gte': return '>='; - case 'lessThan': case 'lt': return '<'; - case 'lessOrEqual': case 'lte': return '<='; + case 'notContains': case 'not_contains': case 'notcontains': return 'notcontains'; + case 'startsWith': case 'starts_with': return 'startswith'; + case 'greaterThan': case 'greater_than': case 'gt': return '>'; + case 'greaterOrEqual': case 'greater_than_or_equal': case 'gte': return '>='; + case 'lessThan': case 'less_than': case 'lt': return '<'; + case 'lessOrEqual': case 'less_than_or_equal': case 'lte': return '<='; case 'in': return 'in'; case 'notIn': case 'not_in': case 'nin': return 'not in'; case 'before': return '<'; diff --git a/packages/plugin-list/src/UserFilters.tsx b/packages/plugin-list/src/UserFilters.tsx index a00113702..877110bf2 100644 --- a/packages/plugin-list/src/UserFilters.tsx +++ b/packages/plugin-list/src/UserFilters.tsx @@ -72,13 +72,22 @@ export interface UserFiltersProps { onSelectionsChange?: (selections: Record>) => void; } -/** Map @objectstack/spec ViewFilterRule operators to ObjectQL AST operators. */ +/** + * Map @objectstack/spec ViewFilterRule operators to ObjectQL AST operators. + * Accepts the canonical vocabulary (`greater_than`, `not_equals`, `before`, …) + * that the Studio filter builder now writes, plus legacy shorthand spellings + * (`gt`, `eq`, `nin`) still present in already-stored view metadata. + */ function specOperatorToAst(op: string | undefined): string { switch (op) { case undefined: case 'equals': case 'eq': return '='; case 'not_equals': case 'ne': case 'neq': return '!='; - case 'gte': return '>='; case 'lte': return '<='; - case 'gt': return '>'; case 'lt': return '<'; + case 'greater_than_or_equal': case 'gte': return '>='; + case 'less_than_or_equal': case 'lte': return '<='; + case 'greater_than': case 'gt': case 'after': return '>'; + case 'less_than': case 'lt': case 'before': return '<'; + case 'not_contains': return 'notcontains'; + case 'starts_with': return 'startswith'; case 'not_in': case 'nin': return 'not in'; default: return op; }