Skip to content

Commit 11b0157

Browse files
Copilothotlong
andcommitted
refactor(ui): merge InterfacePageSchema into PageSchema — unified page type system
- Expand PageSchema.type to include all 16 types (4 platform + 12 interface) - Move RecordReviewConfigSchema from interface.zod.ts to page.zod.ts - Add icon and recordReview properties to PageSchema - Export new PageTypeSchema for the unified enum - InterfaceSchema.pages now uses PageSchema directly - Remove InterfacePageSchema, InterfacePageTypeSchema (merged into PageSchema) - Update all tests for the merged structure - All 199 test files (5468 tests) pass Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent a0a3d1a commit 11b0157

4 files changed

Lines changed: 137 additions & 129 deletions

File tree

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

Lines changed: 55 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import { describe, it, expect } from 'vitest';
22
import {
33
InterfaceSchema,
4-
InterfacePageSchema,
5-
InterfacePageTypeSchema,
64
InterfaceBrandingSchema,
7-
RecordReviewConfigSchema,
85
defineInterface,
96
type Interface,
10-
type InterfacePage,
11-
type RecordReviewConfig,
127
} from './interface.zod';
138
import {
9+
PageSchema,
10+
PageTypeSchema,
1411
PageComponentSchema,
12+
RecordReviewConfigSchema,
1513
ElementDataSourceSchema,
14+
type Page,
1615
type ElementDataSource,
16+
type RecordReviewConfig,
1717
} from './page.zod';
1818
import {
1919
ElementTextPropsSchema,
@@ -23,22 +23,29 @@ import {
2323
} from './component.zod';
2424

2525
// ---------------------------------------------------------------------------
26-
// InterfacePageTypeSchema
26+
// PageTypeSchema — unified page types (platform + interface)
2727
// ---------------------------------------------------------------------------
28-
describe('InterfacePageTypeSchema', () => {
29-
it('should accept all valid page types', () => {
28+
describe('PageTypeSchema', () => {
29+
it('should accept all platform page types', () => {
30+
const types = ['record', 'home', 'app', 'utility'];
31+
types.forEach(type => {
32+
expect(() => PageTypeSchema.parse(type)).not.toThrow();
33+
});
34+
});
35+
36+
it('should accept all interface page types', () => {
3037
const types = [
3138
'dashboard', 'grid', 'list', 'gallery', 'kanban', 'calendar',
3239
'timeline', 'form', 'record_detail', 'record_review', 'overview', 'blank',
3340
];
3441

3542
types.forEach(type => {
36-
expect(() => InterfacePageTypeSchema.parse(type)).not.toThrow();
43+
expect(() => PageTypeSchema.parse(type)).not.toThrow();
3744
});
3845
});
3946

4047
it('should reject invalid page type', () => {
41-
expect(() => InterfacePageTypeSchema.parse('invalid')).toThrow();
48+
expect(() => PageTypeSchema.parse('invalid')).toThrow();
4249
});
4350
});
4451

@@ -141,24 +148,25 @@ describe('InterfaceBrandingSchema', () => {
141148
});
142149

143150
// ---------------------------------------------------------------------------
144-
// InterfacePageSchema
151+
// PageSchema — interface page types (merged from InterfacePageSchema)
145152
// ---------------------------------------------------------------------------
146-
describe('InterfacePageSchema', () => {
147-
it('should accept minimal page', () => {
148-
const page: InterfacePage = InterfacePageSchema.parse({
149-
id: 'page_overview',
153+
describe('PageSchema with interface page types', () => {
154+
it('should accept minimal interface-style page', () => {
155+
const page: Page = PageSchema.parse({
156+
name: 'page_overview',
150157
label: 'Overview',
158+
type: 'blank',
151159
regions: [],
152160
});
153161

154-
expect(page.id).toBe('page_overview');
162+
expect(page.name).toBe('page_overview');
155163
expect(page.type).toBe('blank');
156164
expect(page.template).toBe('default');
157165
});
158166

159167
it('should accept dashboard page', () => {
160-
const page = InterfacePageSchema.parse({
161-
id: 'page_dashboard',
168+
const page = PageSchema.parse({
169+
name: 'page_dashboard',
162170
label: 'Dashboard',
163171
type: 'dashboard',
164172
regions: [
@@ -176,8 +184,8 @@ describe('InterfacePageSchema', () => {
176184
});
177185

178186
it('should accept record_review page with config', () => {
179-
const page = InterfacePageSchema.parse({
180-
id: 'page_review',
187+
const page = PageSchema.parse({
188+
name: 'page_review',
181189
label: 'Review Queue',
182190
type: 'record_review',
183191
object: 'order',
@@ -196,8 +204,8 @@ describe('InterfacePageSchema', () => {
196204
});
197205

198206
it('should accept page with variables', () => {
199-
const page = InterfacePageSchema.parse({
200-
id: 'page_filtered',
207+
const page = PageSchema.parse({
208+
name: 'page_filtered',
201209
label: 'Filtered View',
202210
type: 'blank',
203211
variables: [
@@ -210,33 +218,45 @@ describe('InterfacePageSchema', () => {
210218
expect(page.variables).toHaveLength(2);
211219
});
212220

213-
it('should accept all page types', () => {
221+
it('should accept all interface page types', () => {
214222
const types = [
215223
'dashboard', 'grid', 'list', 'gallery', 'kanban', 'calendar',
216224
'timeline', 'form', 'record_detail', 'record_review', 'overview', 'blank',
217225
];
218226

219227
types.forEach(type => {
220-
expect(() => InterfacePageSchema.parse({
221-
id: 'test_page',
228+
expect(() => PageSchema.parse({
229+
name: 'test_page',
222230
label: 'Test',
223231
type,
224232
regions: [],
225233
})).not.toThrow();
226234
});
227235
});
228236

237+
it('should accept page with icon', () => {
238+
const page = PageSchema.parse({
239+
name: 'page_with_icon',
240+
label: 'Dashboard',
241+
type: 'dashboard',
242+
icon: 'bar-chart',
243+
regions: [],
244+
});
245+
246+
expect(page.icon).toBe('bar-chart');
247+
});
248+
229249
it('should accept page with i18n label', () => {
230-
expect(() => InterfacePageSchema.parse({
231-
id: 'i18n_page',
250+
expect(() => PageSchema.parse({
251+
name: 'i18n_page',
232252
label: { key: 'interface.pages.overview', defaultValue: 'Overview' },
233253
regions: [],
234254
})).not.toThrow();
235255
});
236256

237257
it('should accept page with ARIA attributes', () => {
238-
expect(() => InterfacePageSchema.parse({
239-
id: 'accessible_page',
258+
expect(() => PageSchema.parse({
259+
name: 'accessible_page',
240260
label: 'Accessible Page',
241261
regions: [],
242262
aria: { ariaLabel: 'Interface overview page', role: 'main' },
@@ -268,13 +288,13 @@ describe('InterfaceSchema', () => {
268288
object: 'opportunity',
269289
pages: [
270290
{
271-
id: 'page_dashboard',
291+
name: 'page_dashboard',
272292
label: 'Dashboard',
273293
type: 'dashboard',
274294
regions: [],
275295
},
276296
{
277-
id: 'page_pipeline',
297+
name: 'page_pipeline',
278298
label: 'Pipeline',
279299
type: 'kanban',
280300
object: 'opportunity',
@@ -362,7 +382,7 @@ describe('defineInterface', () => {
362382
label: 'HR Portal',
363383
pages: [
364384
{
365-
id: 'page_onboarding',
385+
name: 'page_onboarding',
366386
label: 'Onboarding',
367387
type: 'overview',
368388
regions: [],
@@ -653,7 +673,7 @@ describe('Interface end-to-end', () => {
653673
object: 'order',
654674
pages: [
655675
{
656-
id: 'page_overview',
676+
name: 'page_overview',
657677
label: 'Overview',
658678
type: 'dashboard',
659679
regions: [
@@ -687,7 +707,7 @@ describe('Interface end-to-end', () => {
687707
],
688708
},
689709
{
690-
id: 'page_review',
710+
name: 'page_review',
691711
label: 'Review Queue',
692712
type: 'record_review',
693713
object: 'order',
@@ -707,7 +727,7 @@ describe('Interface end-to-end', () => {
707727
regions: [],
708728
},
709729
{
710-
id: 'page_grid',
730+
name: 'page_grid',
711731
label: 'All Orders',
712732
type: 'grid',
713733
object: 'order',

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

Lines changed: 8 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -2,92 +2,10 @@
22

33
import { z } from 'zod';
44
import { SnakeCaseIdentifierSchema } from '../shared/identifiers.zod';
5-
import { SortItemSchema } from '../shared/enums.zod';
65
import { I18nLabelSchema, AriaPropsSchema } from './i18n.zod';
7-
import { PageRegionSchema, PageVariableSchema } from './page.zod';
6+
import { PageSchema } from './page.zod';
87
import { AppBrandingSchema } from './app.zod';
98

10-
/**
11-
* Interface Page Type Schema
12-
* Page types available within an Interface (Airtable Interface parity).
13-
*/
14-
export const InterfacePageTypeSchema = z.enum([
15-
'dashboard', // KPI summary with charts/metrics
16-
'grid', // Spreadsheet-like data table
17-
'list', // Record list with quick actions
18-
'gallery', // Card-based visual browsing
19-
'kanban', // Status-based board
20-
'calendar', // Date-based scheduling
21-
'timeline', // Gantt-like project timeline
22-
'form', // Data entry form
23-
'record_detail', // Single record deep-dive
24-
'record_review', // Sequential record review/approval
25-
'overview', // Landing/navigation hub
26-
'blank', // Free-form canvas
27-
]);
28-
29-
/**
30-
* Record Review Config Schema
31-
* Configuration for a sequential record review/approval page.
32-
* Users navigate through records one-by-one, taking actions (approve/reject/skip).
33-
*/
34-
export const RecordReviewConfigSchema = z.object({
35-
object: z.string().describe('Target object for review'),
36-
filter: z.any().optional().describe('Filter criteria for review queue'),
37-
sort: z.array(SortItemSchema).optional().describe('Sort order for review queue'),
38-
displayFields: z.array(z.string()).optional()
39-
.describe('Fields to display on the review page'),
40-
actions: z.array(z.object({
41-
label: z.string().describe('Action button label'),
42-
type: z.enum(['approve', 'reject', 'skip', 'custom'])
43-
.describe('Action type'),
44-
field: z.string().optional()
45-
.describe('Field to update on action'),
46-
value: z.any().optional()
47-
.describe('Value to set on action'),
48-
nextRecord: z.boolean().optional().default(true)
49-
.describe('Auto-advance to next record after action'),
50-
})).describe('Review actions'),
51-
navigation: z.enum(['sequential', 'random', 'filtered'])
52-
.optional().default('sequential')
53-
.describe('Record navigation mode'),
54-
showProgress: z.boolean().optional().default(true)
55-
.describe('Show review progress indicator'),
56-
});
57-
58-
/**
59-
* Interface Page Schema
60-
* A page within an Interface, with Airtable-inspired page types.
61-
*/
62-
export const InterfacePageSchema = z.object({
63-
id: z.string().describe('Unique page identifier within the interface'),
64-
label: I18nLabelSchema.describe('Page display label'),
65-
description: I18nLabelSchema.optional().describe('Page description'),
66-
icon: z.string().optional().describe('Page icon name'),
67-
68-
/** Page Type */
69-
type: InterfacePageTypeSchema.default('blank').describe('Page type'),
70-
71-
/** Object Context */
72-
object: z.string().optional().describe('Bound object (for data-driven page types)'),
73-
74-
/** Record Review Configuration (only for record_review pages) */
75-
recordReview: RecordReviewConfigSchema.optional()
76-
.describe('Record review configuration (required when type is "record_review")'),
77-
78-
/** Page State Definitions */
79-
variables: z.array(PageVariableSchema).optional().describe('Local page state variables'),
80-
81-
/** Layout Template */
82-
template: z.string().default('default').describe('Layout template name'),
83-
84-
/** Regions & Content */
85-
regions: z.array(PageRegionSchema).describe('Defined regions with components'),
86-
87-
/** ARIA accessibility attributes */
88-
aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes'),
89-
});
90-
919
/**
9210
* Interface Branding Schema
9311
* Visual branding overrides for an interface.
@@ -107,6 +25,9 @@ export const InterfaceBrandingSchema = AppBrandingSchema.extend({
10725
*
10826
* An App can contain multiple Interfaces.
10927
*
28+
* Pages within an Interface use the unified `PageSchema` with interface page types
29+
* (dashboard, grid, kanban, record_review, etc.).
30+
*
11031
* **NAMING CONVENTION:**
11132
* Interface names must be lowercase snake_case.
11233
*
@@ -118,7 +39,7 @@ export const InterfaceBrandingSchema = AppBrandingSchema.extend({
11839
* object: 'order',
11940
* pages: [
12041
* {
121-
* id: 'review_queue',
42+
* name: 'review_queue',
12243
* label: 'Review Queue',
12344
* type: 'record_review',
12445
* object: 'order',
@@ -143,11 +64,11 @@ export const InterfaceSchema = z.object({
14364
/** Primary object binding */
14465
object: z.string().optional().describe('Primary object binding (snake_case)'),
14566

146-
/** Pages */
147-
pages: z.array(InterfacePageSchema).describe('Ordered list of pages in this interface'),
67+
/** Pages — uses the unified PageSchema */
68+
pages: z.array(PageSchema).describe('Ordered list of pages in this interface'),
14869

14970
/** Default landing page */
150-
homePageId: z.string().optional().describe('Default landing page ID'),
71+
homePageId: z.string().optional().describe('Default landing page name'),
15172

15273
/** Visual branding */
15374
branding: InterfaceBrandingSchema.optional().describe('Visual branding overrides'),
@@ -172,7 +93,4 @@ export function defineInterface(config: z.input<typeof InterfaceSchema>): Interf
17293
// Type Exports
17394
export type Interface = z.infer<typeof InterfaceSchema>;
17495
export type InterfaceInput = z.input<typeof InterfaceSchema>;
175-
export type InterfacePage = z.infer<typeof InterfacePageSchema>;
176-
export type InterfacePageType = z.infer<typeof InterfacePageTypeSchema>;
17796
export type InterfaceBranding = z.infer<typeof InterfaceBrandingSchema>;
178-
export type RecordReviewConfig = z.infer<typeof RecordReviewConfigSchema>;

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,11 @@ describe('PageSchema', () => {
191191
});
192192

193193
it('should accept different page types', () => {
194-
const types: Array<Page['type']> = ['record', 'home', 'app', 'utility'];
194+
const types: Array<Page['type']> = [
195+
'record', 'home', 'app', 'utility',
196+
'dashboard', 'grid', 'list', 'gallery', 'kanban', 'calendar',
197+
'timeline', 'form', 'record_detail', 'record_review', 'overview', 'blank',
198+
];
195199

196200
types.forEach(type => {
197201
const page = PageSchema.parse({

0 commit comments

Comments
 (0)