Skip to content

Commit f0272e6

Browse files
authored
Merge branch 'main' into copilot/enhance-list-form-protocols
2 parents c9bd09e + df0d477 commit f0272e6

2 files changed

Lines changed: 241 additions & 0 deletions

File tree

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

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,112 @@ import {
1818
type FormField,
1919
} from './view.zod';
2020

21+
describe('HttpMethodSchema', () => {
22+
it('should accept valid HTTP methods', () => {
23+
const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] as const;
24+
25+
methods.forEach(method => {
26+
expect(() => HttpMethodSchema.parse(method)).not.toThrow();
27+
});
28+
});
29+
30+
it('should reject invalid HTTP methods', () => {
31+
expect(() => HttpMethodSchema.parse('INVALID')).toThrow();
32+
});
33+
});
34+
35+
describe('HttpRequestSchema', () => {
36+
it('should accept minimal HTTP request config', () => {
37+
const request: HttpRequest = {
38+
url: '/api/data',
39+
};
40+
41+
const result = HttpRequestSchema.parse(request);
42+
expect(result.method).toBe('GET');
43+
});
44+
45+
it('should accept full HTTP request config', () => {
46+
const request: HttpRequest = {
47+
url: '/api/data',
48+
method: 'POST',
49+
headers: { 'Content-Type': 'application/json' },
50+
params: { filter: 'active' },
51+
body: { name: 'test' },
52+
};
53+
54+
expect(() => HttpRequestSchema.parse(request)).not.toThrow();
55+
});
56+
});
57+
58+
describe('ViewDataSchema', () => {
59+
it('should accept object provider with object name', () => {
60+
const data: ViewData = {
61+
provider: 'object',
62+
object: 'account',
63+
};
64+
65+
expect(() => ViewDataSchema.parse(data)).not.toThrow();
66+
});
67+
68+
it('should require object name for object provider', () => {
69+
const data = {
70+
provider: 'object',
71+
};
72+
73+
expect(() => ViewDataSchema.parse(data)).toThrow();
74+
});
75+
76+
it('should accept api provider with read configuration', () => {
77+
const data: ViewData = {
78+
provider: 'api',
79+
read: {
80+
url: '/api/accounts',
81+
method: 'GET',
82+
params: { status: 'active' },
83+
},
84+
};
85+
86+
expect(() => ViewDataSchema.parse(data)).not.toThrow();
87+
});
88+
89+
it('should accept api provider with read and write configurations', () => {
90+
const data: ViewData = {
91+
provider: 'api',
92+
read: {
93+
url: '/api/accounts',
94+
method: 'GET',
95+
},
96+
write: {
97+
url: '/api/accounts',
98+
method: 'POST',
99+
headers: { 'Content-Type': 'application/json' },
100+
},
101+
};
102+
103+
expect(() => ViewDataSchema.parse(data)).not.toThrow();
104+
});
105+
106+
it('should accept value provider with static items', () => {
107+
const data: ViewData = {
108+
provider: 'value',
109+
items: [
110+
{ id: 1, name: 'Item 1' },
111+
{ id: 2, name: 'Item 2' },
112+
],
113+
};
114+
115+
expect(() => ViewDataSchema.parse(data)).not.toThrow();
116+
});
117+
118+
it('should require items for value provider', () => {
119+
const data = {
120+
provider: 'value',
121+
};
122+
123+
expect(() => ViewDataSchema.parse(data)).toThrow();
124+
});
125+
});
126+
21127
describe('KanbanConfigSchema', () => {
22128
it('should accept minimal kanban config', () => {
23129
const config = {
@@ -198,6 +304,72 @@ describe('ListViewSchema', () => {
198304

199305
expect(() => ListViewSchema.parse(namedView)).not.toThrow();
200306
});
307+
308+
it('should accept list view with object provider', () => {
309+
const listView: ListView = {
310+
type: 'grid',
311+
columns: ['name', 'email'],
312+
data: {
313+
provider: 'object',
314+
object: 'contact',
315+
},
316+
};
317+
318+
expect(() => ListViewSchema.parse(listView)).not.toThrow();
319+
});
320+
321+
it('should accept list view with api provider', () => {
322+
const listView: ListView = {
323+
type: 'grid',
324+
columns: ['name', 'email', 'phone'],
325+
data: {
326+
provider: 'api',
327+
read: {
328+
url: '/api/contacts',
329+
method: 'GET',
330+
},
331+
},
332+
};
333+
334+
expect(() => ListViewSchema.parse(listView)).not.toThrow();
335+
});
336+
337+
it('should accept list view with value provider', () => {
338+
const listView: ListView = {
339+
type: 'grid',
340+
columns: ['name', 'status'],
341+
data: {
342+
provider: 'value',
343+
items: [
344+
{ name: 'Task 1', status: 'Open' },
345+
{ name: 'Task 2', status: 'Closed' },
346+
],
347+
},
348+
};
349+
350+
expect(() => ListViewSchema.parse(listView)).not.toThrow();
351+
});
352+
353+
it('should accept kanban view with custom api data source', () => {
354+
const kanbanView: ListView = {
355+
type: 'kanban',
356+
columns: ['name', 'owner', 'amount'],
357+
data: {
358+
provider: 'api',
359+
read: {
360+
url: '/api/opportunities',
361+
params: { view: 'pipeline' },
362+
},
363+
},
364+
kanban: {
365+
groupByField: 'stage',
366+
summarizeField: 'amount',
367+
columns: ['name', 'owner', 'close_date'],
368+
},
369+
};
370+
371+
expect(() => ListViewSchema.parse(kanbanView)).not.toThrow();
372+
});
201373
});
202374

203375
describe('FormSectionSchema', () => {
@@ -328,6 +500,65 @@ describe('FormViewSchema', () => {
328500

329501
expect(() => FormViewSchema.parse(wizardView)).not.toThrow();
330502
});
503+
504+
it('should accept form view with object provider', () => {
505+
const formView: FormView = {
506+
type: 'simple',
507+
data: {
508+
provider: 'object',
509+
object: 'account',
510+
},
511+
sections: [
512+
{
513+
label: 'Account Information',
514+
fields: ['name', 'industry', 'revenue'],
515+
},
516+
],
517+
};
518+
519+
expect(() => FormViewSchema.parse(formView)).not.toThrow();
520+
});
521+
522+
it('should accept form view with api provider', () => {
523+
const formView: FormView = {
524+
type: 'simple',
525+
data: {
526+
provider: 'api',
527+
read: {
528+
url: '/api/accounts/:id',
529+
method: 'GET',
530+
},
531+
write: {
532+
url: '/api/accounts/:id',
533+
method: 'PUT',
534+
},
535+
},
536+
sections: [
537+
{
538+
fields: ['name', 'email', 'phone'],
539+
},
540+
],
541+
};
542+
543+
expect(() => FormViewSchema.parse(formView)).not.toThrow();
544+
});
545+
546+
it('should accept form view with value provider', () => {
547+
const formView: FormView = {
548+
type: 'simple',
549+
data: {
550+
provider: 'value',
551+
items: [{ name: 'Default Account', type: 'Customer' }],
552+
},
553+
sections: [
554+
{
555+
fields: ['name', 'type'],
556+
},
557+
],
558+
};
559+
560+
expect(() => FormViewSchema.parse(formView)).not.toThrow();
561+
});
331562
});
332563

333564
describe('ViewSchema', () => {

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ export const ListViewSchema = z.object({
6969
label: z.string().optional(), // Display label override
7070
type: z.enum(['grid', 'kanban', 'calendar', 'gantt', 'map']).default('grid'),
7171

72+
/** Data Source Configuration */
73+
data: ViewDataSchema.optional().describe('Data source configuration (defaults to "object" provider)'),
74+
7275
/** Shared Query Config */
7376
columns: z.union([
7477
z.array(z.string()), // Legacy: simple field names
@@ -140,6 +143,10 @@ export const FormSectionSchema = z.object({
140143
*/
141144
export const FormViewSchema = z.object({
142145
type: z.enum(['simple', 'tabbed', 'wizard']).default('simple'),
146+
147+
/** Data Source Configuration */
148+
data: ViewDataSchema.optional().describe('Data source configuration (defaults to "object" provider)'),
149+
143150
sections: z.array(FormSectionSchema).optional(), // For simple layout
144151
groups: z.array(FormSectionSchema).optional(), // Legacy support -> alias to sections
145152
});
@@ -163,3 +170,6 @@ export type ListColumn = z.infer<typeof ListColumnSchema>;
163170
export type FormField = z.infer<typeof FormFieldSchema>;
164171
export type SelectionConfig = z.infer<typeof SelectionConfigSchema>;
165172
export type PaginationConfig = z.infer<typeof PaginationConfigSchema>;
173+
export type ViewData = z.infer<typeof ViewDataSchema>;
174+
export type HttpRequest = z.infer<typeof HttpRequestSchema>;
175+
export type HttpMethod = z.infer<typeof HttpMethodSchema>;

0 commit comments

Comments
 (0)