Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions examples/app-showcase/src/ui/views/task.view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
},
Expand All @@ -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 },
},
Expand Down
10 changes: 5 additions & 5 deletions packages/spec/src/ui/view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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' }],
},
Expand Down Expand Up @@ -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)', () => {
Expand Down
25 changes: 16 additions & 9 deletions packages/spec/src/ui/view.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,26 @@ 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' },
* ]
* ```
*/
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'));
Expand Down Expand Up @@ -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'),
Expand Down
Loading