Skip to content

Commit 334ca87

Browse files
authored
Merge pull request #697 from objectstack-ai/copilot/review-design-document-revisions
2 parents 0e35543 + 35d526e commit 334ca87

4 files changed

Lines changed: 127 additions & 17 deletions

File tree

docs/design/airtable-interface-gap-analysis.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,13 @@ ties them together — specifically:
5858

5959
| Area | Airtable | ObjectStack |
6060
|:---|:---|:---|
61-
| **Interface as a first-class entity** | ✅ Multi-page app per base | 🟡 App + Page exist separately |
61+
| **Interface as a first-class entity** | ✅ Multi-page app per base | `InterfaceSchema` + `InterfaceNavItemSchema` in App navigation |
6262
| **Drag-and-drop element canvas** | ✅ Free-form element placement | 🟡 Region-based composition |
63-
| **Record Review workflow** | ✅ Built-in record-by-record review | ❌ Not modeled |
64-
| **Element-level data binding** | ✅ Each element binds to any table/view | 🟡 Page-level object binding |
65-
| **Shareable interface URLs** | ✅ Public/private share links | ❌ Not modeled |
66-
| **Interface-level permissions** | ✅ Per-interface user access | 🟡 App-level permissions only |
67-
| **Embeddable interfaces** | ✅ iframe embed codes | ❌ Not modeled |
63+
| **Record Review workflow** | ✅ Built-in record-by-record review | `RecordReviewConfigSchema` in `PageSchema` |
64+
| **Element-level data binding** | ✅ Each element binds to any table/view | `ElementDataSourceSchema` per component |
65+
| **Shareable interface URLs** | ✅ Public/private share links | ❌ Not modeled (Phase C) |
66+
| **Interface-level permissions** | ✅ Per-interface user access | `assignedRoles` on `InterfaceSchema` |
67+
| **Embeddable interfaces** | ✅ iframe embed codes | ❌ Not modeled (Phase C) |
6868

6969
This document proposes specific schema additions and a phased roadmap to close these gaps while
7070
preserving ObjectStack's superior extensibility and enterprise capabilities.
@@ -576,6 +576,8 @@ export const EmbedConfigSchema = z.object({
576576
- [x] Merge `InterfacePageSchema` into `PageSchema` — unified `PageTypeSchema` with 16 types
577577
- [x] Extract shared `SortItemSchema` to `shared/enums.zod.ts`
578578
- [x] Export `defineInterface()` from root index.ts
579+
- [x] Add `InterfaceNavItemSchema` to `AppSchema` navigation for App↔Interface bridging
580+
- [x] Disambiguate overlapping page types (`record`/`record_detail`, `home`/`overview`) in `PageTypeSchema` docs
579581
- [ ] Generate JSON Schema for new types
580582

581583
**Estimated effort:** 2–3 weeks
@@ -650,6 +652,9 @@ export const EmbedConfigSchema = z.object({
650652
| 6 | Merge `InterfacePageSchema` into `PageSchema` | 7 of 9 properties were identical. Unified `PageTypeSchema` with 16 types (4 platform + 12 interface) eliminates duplication while preserving both use cases. `InterfaceSchema.pages` now references `PageSchema` directly. | 2026-02-16 |
651653
| 7 | Extract shared `SortItemSchema` to `shared/enums.zod.ts` | Sort item pattern `{ field, order }` was defined inline in 4+ schemas (ElementDataSource, RecordReview, ListView, RecordRelatedList). Shared schema ensures consistency and reduces duplication. | 2026-02-16 |
652654
| 8 | `InterfaceBrandingSchema` extends `AppBrandingSchema` | 2 of 3 fields (`primaryColor`, `logo`) were identical. Using `.extend()` adds only `coverImage`, avoiding property divergence. | 2026-02-16 |
655+
| 9 | Keep `InterfaceSchema` and `AppSchema` separate — do NOT merge | **App** = navigation container (menu tree, routing, mobile nav). **Interface** = content surface (ordered pages, data binding, role-specific views). Merging would conflate navigation topology with page composition. An App can embed multiple Interfaces via `InterfaceNavItemSchema`. This mirrors Salesforce App/FlexiPage and Airtable Base/Interface separation. | 2026-02-16 |
656+
| 10 | Add `InterfaceNavItemSchema` to bridge App↔Interface | `AppSchema.navigation` lacked a way to reference Interfaces. Added `type: 'interface'` nav item with `interfaceName` and optional `pageName` to enable App→Interface navigation without merging the schemas. | 2026-02-16 |
657+
| 11 | Keep all 16 page types — no merge, disambiguate in docs | Reviewed overlapping pairs: `record` vs `record_detail` (component-based layout vs auto-generated field display), `home` vs `overview` (platform landing vs interface navigation hub), `app`/`utility`/`blank` (distinct layout contexts). Each serves a different use case at a different abstraction level. Added disambiguation comments to `PageTypeSchema`. | 2026-02-16 |
653658

654659
---
655660

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

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
DashboardNavItemSchema,
88
PageNavItemSchema,
99
UrlNavItemSchema,
10+
InterfaceNavItemSchema,
1011
GroupNavItemSchema,
1112
defineApp,
1213
type App,
@@ -127,6 +128,53 @@ describe('UrlNavItemSchema', () => {
127128
});
128129
});
129130

131+
describe('InterfaceNavItemSchema', () => {
132+
it('should accept interface nav item with just interfaceName', () => {
133+
const navItem = {
134+
id: 'nav_order_review',
135+
label: 'Order Review',
136+
type: 'interface' as const,
137+
interfaceName: 'order_review',
138+
};
139+
140+
const result = InterfaceNavItemSchema.parse(navItem);
141+
expect(result.interfaceName).toBe('order_review');
142+
expect(result.pageName).toBeUndefined();
143+
});
144+
145+
it('should accept interface nav item with pageName', () => {
146+
const navItem = {
147+
id: 'nav_sales_dashboard',
148+
label: 'Sales Dashboard',
149+
icon: 'layout-dashboard',
150+
type: 'interface' as const,
151+
interfaceName: 'sales_portal',
152+
pageName: 'page_dashboard',
153+
};
154+
155+
const result = InterfaceNavItemSchema.parse(navItem);
156+
expect(result.interfaceName).toBe('sales_portal');
157+
expect(result.pageName).toBe('page_dashboard');
158+
});
159+
160+
it('should work in NavigationItemSchema union', () => {
161+
expect(() => NavigationItemSchema.parse({
162+
id: 'nav_interface',
163+
label: 'Interface',
164+
type: 'interface',
165+
interfaceName: 'my_interface',
166+
})).not.toThrow();
167+
});
168+
169+
it('should reject without interfaceName', () => {
170+
expect(() => InterfaceNavItemSchema.parse({
171+
id: 'nav_missing',
172+
label: 'Missing',
173+
type: 'interface',
174+
})).toThrow();
175+
});
176+
});
177+
130178
describe('GroupNavItemSchema', () => {
131179
it('should accept group nav item', () => {
132180
const navItem = {
@@ -459,6 +507,39 @@ describe('AppSchema', () => {
459507

460508
expect(() => AppSchema.parse(hrApp)).not.toThrow();
461509
});
510+
511+
it('should accept app with interface navigation items', () => {
512+
const app: App = {
513+
name: 'data_platform',
514+
label: 'Data Platform',
515+
navigation: [
516+
{
517+
id: 'nav_home',
518+
label: 'Home',
519+
icon: 'home',
520+
type: 'dashboard',
521+
dashboardName: 'main_dashboard',
522+
},
523+
{
524+
id: 'nav_order_review',
525+
label: 'Order Review',
526+
icon: 'clipboard-check',
527+
type: 'interface',
528+
interfaceName: 'order_review',
529+
},
530+
{
531+
id: 'nav_sales_portal',
532+
label: 'Sales Portal',
533+
icon: 'layout-dashboard',
534+
type: 'interface',
535+
interfaceName: 'sales_portal',
536+
pageName: 'page_dashboard',
537+
},
538+
],
539+
};
540+
541+
expect(() => AppSchema.parse(app)).not.toThrow();
542+
});
462543
});
463544
});
464545

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,18 @@ export const UrlNavItemSchema = BaseNavItemSchema.extend({
7878
});
7979

8080
/**
81-
* 5. Group Navigation Item
81+
* 5. Interface Navigation Item
82+
* Navigates to a specific Interface (self-contained multi-page surface).
83+
* Bridges AppSchema (navigation container) with InterfaceSchema (content surface).
84+
*/
85+
export const InterfaceNavItemSchema = BaseNavItemSchema.extend({
86+
type: z.literal('interface'),
87+
interfaceName: z.string().describe('Target interface name (snake_case)'),
88+
pageName: z.string().optional().describe('Specific page within the interface to open'),
89+
});
90+
91+
/**
92+
* 6. Group Navigation Item
8293
* A container for child navigation items (Sub-menu).
8394
* Does not perform navigation itself.
8495
*/
@@ -101,6 +112,7 @@ export const NavigationItemSchema: z.ZodType<any> = z.lazy(() =>
101112
DashboardNavItemSchema,
102113
PageNavItemSchema,
103114
UrlNavItemSchema,
115+
InterfaceNavItemSchema,
104116
GroupNavItemSchema.extend({
105117
children: z.array(NavigationItemSchema).describe('Child navigation items'),
106118
})
@@ -256,4 +268,5 @@ export type ObjectNavItem = z.infer<typeof ObjectNavItemSchema>;
256268
export type DashboardNavItem = z.infer<typeof DashboardNavItemSchema>;
257269
export type PageNavItem = z.infer<typeof PageNavItemSchema>;
258270
export type UrlNavItem = z.infer<typeof UrlNavItemSchema>;
271+
export type InterfaceNavItem = z.infer<typeof InterfaceNavItemSchema>;
259272
export type GroupNavItem = z.infer<typeof GroupNavItemSchema> & { children: NavigationItem[] };

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

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,26 @@ export const PageVariableSchema = z.object({
100100

101101
/**
102102
* Page Type Schema
103-
* Unified page type enum covering both platform pages (record, home, app, utility)
104-
* and Airtable-inspired interface page types (dashboard, grid, kanban, etc.).
103+
* Unified page type enum covering both platform pages (Salesforce FlexiPage style)
104+
* and Airtable-inspired interface page types.
105+
*
106+
* **Disambiguation of similar types:**
107+
* - `record` vs `record_detail`: `record` is a component-based layout page (FlexiPage style with regions),
108+
* `record_detail` is a field-display page showing all fields of a single record (Airtable style).
109+
* Use `record` for custom record pages with regions/components, `record_detail` for auto-generated detail views.
110+
* - `home` vs `overview`: `home` is the platform-level landing page (tab landing),
111+
* `overview` is an interface-level navigation hub with links/instructions.
112+
* Use `home` for app-level landing, `overview` for in-interface navigation hubs.
113+
* - `app` vs `utility` vs `blank`: `app` is an app-level page with navigation context,
114+
* `utility` is a floating utility panel (e.g. notes, phone), `blank` is a free-form canvas
115+
* for custom composition. They serve distinct layout purposes.
105116
*/
106117
export const PageTypeSchema = z.enum([
107-
// Platform page types
108-
'record', // Record detail page (Salesforce FlexiPage)
109-
'home', // Home/landing page
110-
'app', // App-level page
111-
'utility', // Utility panel
118+
// Platform page types (Salesforce FlexiPage style)
119+
'record', // Component-based record layout page with regions
120+
'home', // Platform-level home/landing page
121+
'app', // App-level page with navigation context
122+
'utility', // Floating utility panel (e.g. notes, phone dialer)
112123
// Interface page types (Airtable Interface parity)
113124
'dashboard', // KPI summary with charts/metrics
114125
'grid', // Spreadsheet-like data table
@@ -118,10 +129,10 @@ export const PageTypeSchema = z.enum([
118129
'calendar', // Date-based scheduling
119130
'timeline', // Gantt-like project timeline
120131
'form', // Data entry form
121-
'record_detail', // Single record deep-dive
132+
'record_detail', // Auto-generated single record field display
122133
'record_review', // Sequential record review/approval
123-
'overview', // Landing/navigation hub
124-
'blank', // Free-form canvas
134+
'overview', // Interface-level navigation/landing hub
135+
'blank', // Free-form canvas for custom composition
125136
]).describe('Page type — platform or interface page types');
126137

127138
/**

0 commit comments

Comments
 (0)