From 6e0ee6931ba6ff69b64ba52750505926e524ddaa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 05:48:16 +0000 Subject: [PATCH 1/2] Initial plan From 35d526e00be6191e47dc555158fe733276632c5b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 05:54:09 +0000 Subject: [PATCH 2/2] feat(ui): add InterfaceNavItemSchema, disambiguate PageTypeSchema, update design doc decisions Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../design/airtable-interface-gap-analysis.md | 17 ++-- packages/spec/src/ui/app.test.ts | 81 +++++++++++++++++++ packages/spec/src/ui/app.zod.ts | 15 +++- packages/spec/src/ui/page.zod.ts | 31 ++++--- 4 files changed, 127 insertions(+), 17 deletions(-) diff --git a/docs/design/airtable-interface-gap-analysis.md b/docs/design/airtable-interface-gap-analysis.md index dbf0060d9..3dbc4d751 100644 --- a/docs/design/airtable-interface-gap-analysis.md +++ b/docs/design/airtable-interface-gap-analysis.md @@ -58,13 +58,13 @@ ties them together — specifically: | Area | Airtable | ObjectStack | |:---|:---|:---| -| **Interface as a first-class entity** | ✅ Multi-page app per base | 🟡 App + Page exist separately | +| **Interface as a first-class entity** | ✅ Multi-page app per base | ✅ `InterfaceSchema` + `InterfaceNavItemSchema` in App navigation | | **Drag-and-drop element canvas** | ✅ Free-form element placement | 🟡 Region-based composition | -| **Record Review workflow** | ✅ Built-in record-by-record review | ❌ Not modeled | -| **Element-level data binding** | ✅ Each element binds to any table/view | 🟡 Page-level object binding | -| **Shareable interface URLs** | ✅ Public/private share links | ❌ Not modeled | -| **Interface-level permissions** | ✅ Per-interface user access | 🟡 App-level permissions only | -| **Embeddable interfaces** | ✅ iframe embed codes | ❌ Not modeled | +| **Record Review workflow** | ✅ Built-in record-by-record review | ✅ `RecordReviewConfigSchema` in `PageSchema` | +| **Element-level data binding** | ✅ Each element binds to any table/view | ✅ `ElementDataSourceSchema` per component | +| **Shareable interface URLs** | ✅ Public/private share links | ❌ Not modeled (Phase C) | +| **Interface-level permissions** | ✅ Per-interface user access | ✅ `assignedRoles` on `InterfaceSchema` | +| **Embeddable interfaces** | ✅ iframe embed codes | ❌ Not modeled (Phase C) | This document proposes specific schema additions and a phased roadmap to close these gaps while preserving ObjectStack's superior extensibility and enterprise capabilities. @@ -576,6 +576,8 @@ export const EmbedConfigSchema = z.object({ - [x] Merge `InterfacePageSchema` into `PageSchema` — unified `PageTypeSchema` with 16 types - [x] Extract shared `SortItemSchema` to `shared/enums.zod.ts` - [x] Export `defineInterface()` from root index.ts +- [x] Add `InterfaceNavItemSchema` to `AppSchema` navigation for App↔Interface bridging +- [x] Disambiguate overlapping page types (`record`/`record_detail`, `home`/`overview`) in `PageTypeSchema` docs - [ ] Generate JSON Schema for new types **Estimated effort:** 2–3 weeks @@ -650,6 +652,9 @@ export const EmbedConfigSchema = z.object({ | 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 | | 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 | | 8 | `InterfaceBrandingSchema` extends `AppBrandingSchema` | 2 of 3 fields (`primaryColor`, `logo`) were identical. Using `.extend()` adds only `coverImage`, avoiding property divergence. | 2026-02-16 | +| 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 | +| 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 | +| 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 | --- diff --git a/packages/spec/src/ui/app.test.ts b/packages/spec/src/ui/app.test.ts index 06d561732..c612138d4 100644 --- a/packages/spec/src/ui/app.test.ts +++ b/packages/spec/src/ui/app.test.ts @@ -7,6 +7,7 @@ import { DashboardNavItemSchema, PageNavItemSchema, UrlNavItemSchema, + InterfaceNavItemSchema, GroupNavItemSchema, defineApp, type App, @@ -127,6 +128,53 @@ describe('UrlNavItemSchema', () => { }); }); +describe('InterfaceNavItemSchema', () => { + it('should accept interface nav item with just interfaceName', () => { + const navItem = { + id: 'nav_order_review', + label: 'Order Review', + type: 'interface' as const, + interfaceName: 'order_review', + }; + + const result = InterfaceNavItemSchema.parse(navItem); + expect(result.interfaceName).toBe('order_review'); + expect(result.pageName).toBeUndefined(); + }); + + it('should accept interface nav item with pageName', () => { + const navItem = { + id: 'nav_sales_dashboard', + label: 'Sales Dashboard', + icon: 'layout-dashboard', + type: 'interface' as const, + interfaceName: 'sales_portal', + pageName: 'page_dashboard', + }; + + const result = InterfaceNavItemSchema.parse(navItem); + expect(result.interfaceName).toBe('sales_portal'); + expect(result.pageName).toBe('page_dashboard'); + }); + + it('should work in NavigationItemSchema union', () => { + expect(() => NavigationItemSchema.parse({ + id: 'nav_interface', + label: 'Interface', + type: 'interface', + interfaceName: 'my_interface', + })).not.toThrow(); + }); + + it('should reject without interfaceName', () => { + expect(() => InterfaceNavItemSchema.parse({ + id: 'nav_missing', + label: 'Missing', + type: 'interface', + })).toThrow(); + }); +}); + describe('GroupNavItemSchema', () => { it('should accept group nav item', () => { const navItem = { @@ -459,6 +507,39 @@ describe('AppSchema', () => { expect(() => AppSchema.parse(hrApp)).not.toThrow(); }); + + it('should accept app with interface navigation items', () => { + const app: App = { + name: 'data_platform', + label: 'Data Platform', + navigation: [ + { + id: 'nav_home', + label: 'Home', + icon: 'home', + type: 'dashboard', + dashboardName: 'main_dashboard', + }, + { + id: 'nav_order_review', + label: 'Order Review', + icon: 'clipboard-check', + type: 'interface', + interfaceName: 'order_review', + }, + { + id: 'nav_sales_portal', + label: 'Sales Portal', + icon: 'layout-dashboard', + type: 'interface', + interfaceName: 'sales_portal', + pageName: 'page_dashboard', + }, + ], + }; + + expect(() => AppSchema.parse(app)).not.toThrow(); + }); }); }); diff --git a/packages/spec/src/ui/app.zod.ts b/packages/spec/src/ui/app.zod.ts index 55ba26f44..f1d2592b1 100644 --- a/packages/spec/src/ui/app.zod.ts +++ b/packages/spec/src/ui/app.zod.ts @@ -78,7 +78,18 @@ export const UrlNavItemSchema = BaseNavItemSchema.extend({ }); /** - * 5. Group Navigation Item + * 5. Interface Navigation Item + * Navigates to a specific Interface (self-contained multi-page surface). + * Bridges AppSchema (navigation container) with InterfaceSchema (content surface). + */ +export const InterfaceNavItemSchema = BaseNavItemSchema.extend({ + type: z.literal('interface'), + interfaceName: z.string().describe('Target interface name (snake_case)'), + pageName: z.string().optional().describe('Specific page within the interface to open'), +}); + +/** + * 6. Group Navigation Item * A container for child navigation items (Sub-menu). * Does not perform navigation itself. */ @@ -101,6 +112,7 @@ export const NavigationItemSchema: z.ZodType = z.lazy(() => DashboardNavItemSchema, PageNavItemSchema, UrlNavItemSchema, + InterfaceNavItemSchema, GroupNavItemSchema.extend({ children: z.array(NavigationItemSchema).describe('Child navigation items'), }) @@ -256,4 +268,5 @@ export type ObjectNavItem = z.infer; export type DashboardNavItem = z.infer; export type PageNavItem = z.infer; export type UrlNavItem = z.infer; +export type InterfaceNavItem = z.infer; export type GroupNavItem = z.infer & { children: NavigationItem[] }; diff --git a/packages/spec/src/ui/page.zod.ts b/packages/spec/src/ui/page.zod.ts index 8b35ed9ea..6930be0c2 100644 --- a/packages/spec/src/ui/page.zod.ts +++ b/packages/spec/src/ui/page.zod.ts @@ -100,15 +100,26 @@ export const PageVariableSchema = z.object({ /** * Page Type Schema - * Unified page type enum covering both platform pages (record, home, app, utility) - * and Airtable-inspired interface page types (dashboard, grid, kanban, etc.). + * Unified page type enum covering both platform pages (Salesforce FlexiPage style) + * and Airtable-inspired interface page types. + * + * **Disambiguation of similar types:** + * - `record` vs `record_detail`: `record` is a component-based layout page (FlexiPage style with regions), + * `record_detail` is a field-display page showing all fields of a single record (Airtable style). + * Use `record` for custom record pages with regions/components, `record_detail` for auto-generated detail views. + * - `home` vs `overview`: `home` is the platform-level landing page (tab landing), + * `overview` is an interface-level navigation hub with links/instructions. + * Use `home` for app-level landing, `overview` for in-interface navigation hubs. + * - `app` vs `utility` vs `blank`: `app` is an app-level page with navigation context, + * `utility` is a floating utility panel (e.g. notes, phone), `blank` is a free-form canvas + * for custom composition. They serve distinct layout purposes. */ export const PageTypeSchema = z.enum([ - // Platform page types - 'record', // Record detail page (Salesforce FlexiPage) - 'home', // Home/landing page - 'app', // App-level page - 'utility', // Utility panel + // Platform page types (Salesforce FlexiPage style) + 'record', // Component-based record layout page with regions + 'home', // Platform-level home/landing page + 'app', // App-level page with navigation context + 'utility', // Floating utility panel (e.g. notes, phone dialer) // Interface page types (Airtable Interface parity) 'dashboard', // KPI summary with charts/metrics 'grid', // Spreadsheet-like data table @@ -118,10 +129,10 @@ export const PageTypeSchema = z.enum([ 'calendar', // Date-based scheduling 'timeline', // Gantt-like project timeline 'form', // Data entry form - 'record_detail', // Single record deep-dive + 'record_detail', // Auto-generated single record field display 'record_review', // Sequential record review/approval - 'overview', // Landing/navigation hub - 'blank', // Free-form canvas + 'overview', // Interface-level navigation/landing hub + 'blank', // Free-form canvas for custom composition ]).describe('Page type — platform or interface page types'); /**