Skip to content

Commit 93a85b2

Browse files
os-zhuangclaude
andauthored
feat(SchemaForm): field-type-aware operators + values for view filter (#2766)
Align the Studio view-config filter editor with the canonical @objectstack/spec ViewFilterRule operator vocabulary and reuse the runtime FilterBuilder (which already provides per-field-type operator lists, value inputs, and i18n) instead of raw repeater rows. - ResourceEditPage: resolve the source object from `data.object` so view filter rows get the object field catalog that drives operator/value widgets. - widgets.tsx FilterBuilderWidget: write the canonical operator vocabulary (greater_than, not_equals, before/after, …); reads still accept legacy shorthand/camelCase spellings from already-stored metadata and round-trip unknown operators instead of rewriting them. - Runtime operator->AST maps (ListView.mapOperator, UserFilters.specOperatorToAst) now accept the canonical long-form operators the builder writes, alongside the existing shorthand spellings. Depends on the paired @objectstack/spec change (ViewFilterRule operator enum + `widget: 'filter-builder'` on the view filter field) for the operator dropdown. Claude-Session: https://claude.ai/code/session_01PwbhoMeqh33xFWq5M77QTJ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 190cc40 commit 93a85b2

4 files changed

Lines changed: 44 additions & 19 deletions

File tree

packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,7 @@ function MetadataResourceEditPageImpl({
584584
// `object`; other types fall back to their own `object`/`objectName`.
585585
const sourceObjectName: string | undefined =
586586
((draft as any)?.interfaceConfig?.source as string | undefined) ||
587+
((draft as any)?.data?.object as string | undefined) ||
587588
((draft as any)?.object as string | undefined) ||
588589
((draft as any)?.objectName as string | undefined);
589590
const [objectFields, setObjectFields] = React.useState<Array<{ name: string; label?: string; type?: string }>>([]);

packages/app-shell/src/views/metadata-admin/widgets.tsx

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1668,22 +1668,34 @@ function ActionMultiWidget({ id, value, onChange, readOnly, context }: WidgetPro
16681668

16691669
/* -------------------------------------------------------------------------- */
16701670
/* filter-builder — the SAME runtime FilterBuilder used by the list toolbar, */
1671-
/* reused in Studio for tab presets and the page base filter (unified UX). */
1672-
/* Stored format stays spec ViewFilterRule[] ({field,operator,value}); the */
1673-
/* builder's camelCase operators are mapped at the boundary so the runtime */
1674-
/* (specOperatorToAst) keeps working unchanged. */
1671+
/* reused in Studio for view/tab filters and the page base filter (unified UX).*/
1672+
/* Stored format stays spec ViewFilterRule[] ({field,operator,value}). Writes */
1673+
/* use the CANONICAL @objectstack/spec operator vocabulary (the enum enforced */
1674+
/* by ViewFilterRuleSchema); reads also accept legacy shorthand/camelCase */
1675+
/* spellings still present in already-stored view metadata (gt / eq / isNull). */
16751676
/* -------------------------------------------------------------------------- */
16761677
const FB_TO_SPEC: Record<string, string> = {
16771678
equals: 'equals', notEquals: 'not_equals', contains: 'contains', notContains: 'not_contains',
1678-
isEmpty: 'is_empty', isNotEmpty: 'is_not_empty', greaterThan: 'gt', lessThan: 'lt',
1679-
greaterOrEqual: 'gte', lessOrEqual: 'lte', before: 'lt', after: 'gt', between: 'between',
1679+
isEmpty: 'is_empty', isNotEmpty: 'is_not_empty',
1680+
greaterThan: 'greater_than', lessThan: 'less_than',
1681+
greaterOrEqual: 'greater_than_or_equal', lessOrEqual: 'less_than_or_equal',
1682+
before: 'before', after: 'after', between: 'between',
16801683
in: 'in', notIn: 'not_in',
16811684
};
1685+
/** Spec operator → FilterBuilder camelCase. Keys cover both the canonical
1686+
* vocabulary and legacy spellings (shorthand + snake/camel) so stored view
1687+
* metadata written before canonicalization still seeds the builder. */
16821688
const SPEC_TO_FB: Record<string, string> = {
1683-
equals: 'equals', eq: 'equals', not_equals: 'notEquals', ne: 'notEquals', neq: 'notEquals',
1684-
contains: 'contains', not_contains: 'notContains', is_empty: 'isEmpty', is_not_empty: 'isNotEmpty',
1685-
gt: 'greaterThan', greater_than: 'greaterThan', lt: 'lessThan', less_than: 'lessThan',
1686-
gte: 'greaterOrEqual', lte: 'lessOrEqual', in: 'in', not_in: 'notIn', nin: 'notIn',
1689+
equals: 'equals', eq: 'equals',
1690+
not_equals: 'notEquals', ne: 'notEquals', neq: 'notEquals', notEquals: 'notEquals',
1691+
contains: 'contains', not_contains: 'notContains', notContains: 'notContains',
1692+
is_empty: 'isEmpty', isEmpty: 'isEmpty', is_not_empty: 'isNotEmpty', isNotEmpty: 'isNotEmpty',
1693+
greater_than: 'greaterThan', gt: 'greaterThan', greaterThan: 'greaterThan',
1694+
less_than: 'lessThan', lt: 'lessThan', lessThan: 'lessThan',
1695+
greater_than_or_equal: 'greaterOrEqual', gte: 'greaterOrEqual', greaterOrEqual: 'greaterOrEqual',
1696+
less_than_or_equal: 'lessOrEqual', lte: 'lessOrEqual', lessOrEqual: 'lessOrEqual',
1697+
before: 'before', after: 'after', between: 'between',
1698+
in: 'in', not_in: 'notIn', nin: 'notIn', notIn: 'notIn',
16871699
};
16881700

16891701
interface FilterRuleLite { field: string; operator: string; value?: unknown }
@@ -1700,8 +1712,10 @@ function FilterBuilderField({ value, onChange, fields, readOnly }: {
17001712
logic: 'and' as const,
17011713
conditions: rules.map((r, i) => ({
17021714
id: `c${i}`,
1703-
field: r.field,
1715+
// Keep the raw operator verbatim if the builder has no camelCase
1716+
// equivalent, so it round-trips on save instead of being rewritten.
17041717
operator: SPEC_TO_FB[r.operator] ?? r.operator ?? 'equals',
1718+
field: r.field,
17051719
value: (r.value as any) ?? '',
17061720
})),
17071721
};

packages/plugin-list/src/ListView.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,12 @@ function mapOperator(op: string) {
6464
case 'equals': case 'eq': return '=';
6565
case 'notEquals': case 'not_equals': case 'ne': case 'neq': return '!=';
6666
case 'contains': return 'contains';
67-
case 'notContains': case 'notcontains': return 'notcontains';
68-
case 'greaterThan': case 'gt': return '>';
69-
case 'greaterOrEqual': case 'gte': return '>=';
70-
case 'lessThan': case 'lt': return '<';
71-
case 'lessOrEqual': case 'lte': return '<=';
67+
case 'notContains': case 'not_contains': case 'notcontains': return 'notcontains';
68+
case 'startsWith': case 'starts_with': return 'startswith';
69+
case 'greaterThan': case 'greater_than': case 'gt': return '>';
70+
case 'greaterOrEqual': case 'greater_than_or_equal': case 'gte': return '>=';
71+
case 'lessThan': case 'less_than': case 'lt': return '<';
72+
case 'lessOrEqual': case 'less_than_or_equal': case 'lte': return '<=';
7273
case 'in': return 'in';
7374
case 'notIn': case 'not_in': case 'nin': return 'not in';
7475
case 'before': return '<';

packages/plugin-list/src/UserFilters.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,22 @@ export interface UserFiltersProps {
7272
onSelectionsChange?: (selections: Record<string, Array<string | number | boolean>>) => void;
7373
}
7474

75-
/** Map @objectstack/spec ViewFilterRule operators to ObjectQL AST operators. */
75+
/**
76+
* Map @objectstack/spec ViewFilterRule operators to ObjectQL AST operators.
77+
* Accepts the canonical vocabulary (`greater_than`, `not_equals`, `before`, …)
78+
* that the Studio filter builder now writes, plus legacy shorthand spellings
79+
* (`gt`, `eq`, `nin`) still present in already-stored view metadata.
80+
*/
7681
function specOperatorToAst(op: string | undefined): string {
7782
switch (op) {
7883
case undefined: case 'equals': case 'eq': return '=';
7984
case 'not_equals': case 'ne': case 'neq': return '!=';
80-
case 'gte': return '>='; case 'lte': return '<=';
81-
case 'gt': return '>'; case 'lt': return '<';
85+
case 'greater_than_or_equal': case 'gte': return '>=';
86+
case 'less_than_or_equal': case 'lte': return '<=';
87+
case 'greater_than': case 'gt': case 'after': return '>';
88+
case 'less_than': case 'lt': case 'before': return '<';
89+
case 'not_contains': return 'notcontains';
90+
case 'starts_with': return 'startswith';
8291
case 'not_in': case 'nin': return 'not in';
8392
default: return op;
8493
}

0 commit comments

Comments
 (0)