From dab0fdaeb94823b84d35d594c122680feea3ad98 Mon Sep 17 00:00:00 2001 From: akarma-synetal Date: Mon, 20 Jul 2026 19:24:59 +0530 Subject: [PATCH] fix(spec): enforce ViewFilterRuleSchema operator enum + typed sort MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `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 --- .../app-showcase/src/ui/views/task.view.ts | 7 +----- .../src/objects/sys-share-link.object.ts | 4 +-- packages/spec/src/ui/view.test.ts | 10 ++++---- packages/spec/src/ui/view.zod.ts | 25 ++++++++++++------- 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/examples/app-showcase/src/ui/views/task.view.ts b/examples/app-showcase/src/ui/views/task.view.ts index 703d758a0a..0e9c413d44 100644 --- a/examples/app-showcase/src/ui/views/task.view.ts +++ b/examples/app-showcase/src/ui/views/task.view.ts @@ -98,12 +98,7 @@ export const TaskViews = defineView({ { field: 'estimate_hours' }, ], - // @objectstack/spec ListViewSchema.sort accepts a bare STRING - // ("field [asc|desc]"), not only the {field,order}[] array form. This - // is the exact shape that used to crash the renderer with - // "schema.sort.map is not a function" (objectui#2601) — kept here as a - // live coverage fixture so a real list view exercises the string form. - sort: 'estimate_hours desc', + sort: [{ field: 'estimate_hours', order: 'desc' }], // ADR-0053 — NO `userFilters` here: on an object list view ("views" // mode) the console suppresses them by design (the view switcher is diff --git a/packages/plugins/plugin-sharing/src/objects/sys-share-link.object.ts b/packages/plugins/plugin-sharing/src/objects/sys-share-link.object.ts index 816535e9ba..d3c53ba773 100644 --- a/packages/plugins/plugin-sharing/src/objects/sys-share-link.object.ts +++ b/packages/plugins/plugin-sharing/src/objects/sys-share-link.object.ts @@ -58,7 +58,7 @@ export const SysShareLink = ObjectSchema.create({ label: 'Active', data: { provider: 'object', object: 'sys_share_link' }, columns: ['object_name', 'record_id', 'permission', 'audience', 'expires_at', 'use_count', 'last_used_at'], - filter: [{ field: 'revoked_at', operator: 'isNull' }], + filter: [{ field: 'revoked_at', operator: 'is_empty' }], sort: [{ field: 'created_at', order: 'desc' }], pagination: { pageSize: 100 }, }, @@ -78,7 +78,7 @@ export const SysShareLink = ObjectSchema.create({ label: 'Revoked', data: { provider: 'object', object: 'sys_share_link' }, columns: ['object_name', 'record_id', 'revoked_at', 'created_by'], - filter: [{ field: 'revoked_at', operator: 'isNotNull' }], + filter: [{ field: 'revoked_at', operator: 'is_not_empty' }], sort: [{ field: 'revoked_at', order: 'desc' }], pagination: { pageSize: 50 }, }, diff --git a/packages/spec/src/ui/view.test.ts b/packages/spec/src/ui/view.test.ts index 68226aa6d1..1af76e8291 100644 --- a/packages/spec/src/ui/view.test.ts +++ b/packages/spec/src/ui/view.test.ts @@ -292,10 +292,10 @@ describe('ListViewSchema', () => { expect(() => ListViewSchema.parse(listView)).not.toThrow(); }); - it('should accept legacy string sort format', () => { + it('should accept array sort format', () => { const listView: ListView = { columns: ['name'], - sort: 'created_at desc', + sort: [{ field: 'created_at', order: 'desc' }], }; expect(() => ListViewSchema.parse(listView)).not.toThrow(); @@ -734,7 +734,7 @@ describe('ViewSchema', () => { type: 'grid', columns: ['name', 'account_name', 'amount', 'stage', 'close_date'], filter: [ - { field: 'close_date', operator: 'this_quarter' }, + { field: 'close_date', operator: 'after', value: '2024-01-01' }, ], sort: [{ field: 'amount', order: 'desc' }], }, @@ -2478,14 +2478,14 @@ describe('ViewFilterRuleSchema', () => { it('should accept a unary filter rule without value', () => { const rule = ViewFilterRuleSchema.parse({ field: 'close_date', - operator: 'this_quarter', + operator: 'is_empty', }); expect(rule.value).toBeUndefined(); }); it('should accept boolean and number filter values', () => { expect(() => ViewFilterRuleSchema.parse({ field: 'archived', operator: 'equals', value: false })).not.toThrow(); - expect(() => ViewFilterRuleSchema.parse({ field: 'amount', operator: 'gte', value: 1000 })).not.toThrow(); + expect(() => ViewFilterRuleSchema.parse({ field: 'amount', operator: 'greater_than_or_equal', value: 1000 })).not.toThrow(); }); it('should accept array filter values (for IN operator)', () => { diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index 8bbed71fc1..99afc0bd73 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -64,7 +64,7 @@ export const ViewDataSchema = lazySchema(() => z.discriminatedUnion('provider', * ```ts * filter: [ * { field: 'status', operator: 'equals', value: 'active' }, - * { field: 'close_date', operator: 'this_quarter' }, + * { field: 'close_date', operator: 'after', value: '2024-01-01' }, * ] * ``` */ @@ -72,8 +72,18 @@ export const ViewFilterRuleSchema = lazySchema(() => z.object({ /** Field name to filter on */ field: z.string().describe('Field name to filter on'), /** Filter operator */ - operator: z.string().describe('Filter operator (e.g. equals, not_equals, contains, this_quarter)'), - /** Filter value (optional for unary operators like is_null, this_quarter) */ + operator: 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', + ]).describe('Filter operator'), + /** Filter value (optional for unary operators like is_null, is_empty) */ value: z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.union([z.string(), z.number()]))]) .optional().describe('Filter value'), }).describe('View filter rule')); @@ -551,13 +561,10 @@ export const ListViewSchema = lazySchema(() => z.object({ z.array(ListColumnSchema), // Enhanced: detailed column config ]).describe('Fields to display as columns'), filter: z.array(ViewFilterRuleSchema).optional().describe('Filter criteria (JSON Rules)'), - sort: z.union([ - z.string(), //Legacy "field desc" - z.array(z.object({ + sort: z.array(z.object({ field: z.string(), - order: z.enum(['asc', 'desc']) - })) - ]).optional(), + order: z.enum(['asc', 'desc']), + })).optional(), /** Search & Filter */ searchableFields: z.array(z.string()).optional().describe('Fields enabled for search'),