Skip to content

Commit dab0fda

Browse files
fix(spec): enforce ViewFilterRuleSchema operator enum + typed sort
- `operator`: z.string() → z.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]) so the SchemaForm renderer auto-generates a Select dropdown instead of a generic text input. - `sort`: z.union([z.string(), z.array(...)]) → z.array(...) only. The union form confused the RepeaterField, causing it to render no UI at all for the sort sub-field. - Updated a test fixture (task.view.ts) and share-link object (sys-share-link.object.ts) to use valid operators / array sort. - Fixed spec tests to match the new enum (this_quarter → is_empty, gte → greater_than_or_equal, string sort → array sort). Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d02a1b4 commit dab0fda

4 files changed

Lines changed: 24 additions & 22 deletions

File tree

examples/app-showcase/src/ui/views/task.view.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,7 @@ export const TaskViews = defineView({
9898
{ field: 'estimate_hours' },
9999
],
100100

101-
// @objectstack/spec ListViewSchema.sort accepts a bare STRING
102-
// ("field [asc|desc]"), not only the {field,order}[] array form. This
103-
// is the exact shape that used to crash the renderer with
104-
// "schema.sort.map is not a function" (objectui#2601) — kept here as a
105-
// live coverage fixture so a real list view exercises the string form.
106-
sort: 'estimate_hours desc',
101+
sort: [{ field: 'estimate_hours', order: 'desc' }],
107102

108103
// ADR-0053 — NO `userFilters` here: on an object list view ("views"
109104
// mode) the console suppresses them by design (the view switcher is

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_empty' }],
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_empty' }],
8282
sort: [{ field: 'revoked_at', order: 'desc' }],
8383
pagination: { pageSize: 50 },
8484
},

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -292,10 +292,10 @@ describe('ListViewSchema', () => {
292292
expect(() => ListViewSchema.parse(listView)).not.toThrow();
293293
});
294294

295-
it('should accept legacy string sort format', () => {
295+
it('should accept array sort format', () => {
296296
const listView: ListView = {
297297
columns: ['name'],
298-
sort: 'created_at desc',
298+
sort: [{ field: 'created_at', order: 'desc' }],
299299
};
300300

301301
expect(() => ListViewSchema.parse(listView)).not.toThrow();
@@ -734,7 +734,7 @@ describe('ViewSchema', () => {
734734
type: 'grid',
735735
columns: ['name', 'account_name', 'amount', 'stage', 'close_date'],
736736
filter: [
737-
{ field: 'close_date', operator: 'this_quarter' },
737+
{ field: 'close_date', operator: 'after', value: '2024-01-01' },
738738
],
739739
sort: [{ field: 'amount', order: 'desc' }],
740740
},
@@ -2478,14 +2478,14 @@ describe('ViewFilterRuleSchema', () => {
24782478
it('should accept a unary filter rule without value', () => {
24792479
const rule = ViewFilterRuleSchema.parse({
24802480
field: 'close_date',
2481-
operator: 'this_quarter',
2481+
operator: 'is_empty',
24822482
});
24832483
expect(rule.value).toBeUndefined();
24842484
});
24852485

24862486
it('should accept boolean and number filter values', () => {
24872487
expect(() => ViewFilterRuleSchema.parse({ field: 'archived', operator: 'equals', value: false })).not.toThrow();
2488-
expect(() => ViewFilterRuleSchema.parse({ field: 'amount', operator: 'gte', value: 1000 })).not.toThrow();
2488+
expect(() => ViewFilterRuleSchema.parse({ field: 'amount', operator: 'greater_than_or_equal', value: 1000 })).not.toThrow();
24892489
});
24902490

24912491
it('should accept array filter values (for IN operator)', () => {

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

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,26 @@ export const ViewDataSchema = lazySchema(() => z.discriminatedUnion('provider',
6464
* ```ts
6565
* filter: [
6666
* { field: 'status', operator: 'equals', value: 'active' },
67-
* { field: 'close_date', operator: 'this_quarter' },
67+
* { field: 'close_date', operator: 'after', value: '2024-01-01' },
6868
* ]
6969
* ```
7070
*/
7171
export const ViewFilterRuleSchema = lazySchema(() => z.object({
7272
/** Field name to filter on */
7373
field: z.string().describe('Field name to filter on'),
7474
/** Filter operator */
75-
operator: z.string().describe('Filter operator (e.g. equals, not_equals, contains, this_quarter)'),
76-
/** Filter value (optional for unary operators like is_null, this_quarter) */
75+
operator: z.enum([
76+
'equals', 'not_equals',
77+
'contains', 'not_contains',
78+
'starts_with', 'ends_with',
79+
'greater_than', 'less_than',
80+
'greater_than_or_equal', 'less_than_or_equal',
81+
'in', 'not_in',
82+
'is_empty', 'is_not_empty',
83+
'is_null', 'is_not_null',
84+
'before', 'after', 'between',
85+
]).describe('Filter operator'),
86+
/** Filter value (optional for unary operators like is_null, is_empty) */
7787
value: z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.union([z.string(), z.number()]))])
7888
.optional().describe('Filter value'),
7989
}).describe('View filter rule'));
@@ -551,13 +561,10 @@ export const ListViewSchema = lazySchema(() => z.object({
551561
z.array(ListColumnSchema), // Enhanced: detailed column config
552562
]).describe('Fields to display as columns'),
553563
filter: z.array(ViewFilterRuleSchema).optional().describe('Filter criteria (JSON Rules)'),
554-
sort: z.union([
555-
z.string(), //Legacy "field desc"
556-
z.array(z.object({
564+
sort: z.array(z.object({
557565
field: z.string(),
558-
order: z.enum(['asc', 'desc'])
559-
}))
560-
]).optional(),
566+
order: z.enum(['asc', 'desc']),
567+
})).optional(),
561568

562569
/** Search & Filter */
563570
searchableFields: z.array(z.string()).optional().describe('Fields enabled for search'),

0 commit comments

Comments
 (0)