Skip to content

Commit 77235d6

Browse files
authored
Merge pull request #698 from objectstack-ai/copilot/refactor-navigation-architecture
2 parents 6c45ef7 + 5d508fa commit 77235d6

12 files changed

Lines changed: 544 additions & 15 deletions

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

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ ties them together — specifically:
5858

5959
| Area | Airtable | ObjectStack |
6060
|:---|:---|:---|
61-
| **Interface as a first-class entity** | ✅ Multi-page app per base |`InterfaceSchema` + `InterfaceNavItemSchema` in App navigation |
61+
| **Interface as a first-class entity** | ✅ Multi-page app per base |`InterfaceSchema` + `App.interfaces[]` drives sidebar |
6262
| **Drag-and-drop element canvas** | ✅ Free-form element placement | 🟡 Region-based composition |
6363
| **Record Review workflow** | ✅ Built-in record-by-record review |`RecordReviewConfigSchema` in `PageSchema` |
6464
| **Element-level data binding** | ✅ Each element binds to any table/view |`ElementDataSourceSchema` per component |
@@ -345,6 +345,50 @@ ObjectStack's UI Protocol already **exceeds Airtable** in several significant ar
345345

346346
## 6. Schema Improvement Proposals
347347

348+
This section details the schema changes to support Airtable Interface parity.
349+
350+
**Note:** Many of these proposals have been **IMPLEMENTED** in Phase A (see Section 7.1).
351+
The code samples below reflect the current state of the schemas.
352+
353+
### 6.0 App Schema Enhancements
354+
355+
To enable the new Interface-driven navigation model, `AppSchema` has been enhanced with:
356+
357+
```typescript
358+
// Implemented: src/ui/app.zod.ts
359+
360+
export const AppSchema = z.object({
361+
// ... existing fields ...
362+
363+
/**
364+
* Interface names registered in this App.
365+
* Sidebar renders as a two-level menu: Interface (collapsible group) → Pages (menu items).
366+
*/
367+
interfaces: z.array(z.string()).optional()
368+
.describe('Interface names available in this App. Sidebar renders as Interface→Pages two-level menu.'),
369+
370+
/** Default interface to activate on App launch */
371+
defaultInterface: z.string().optional()
372+
.describe('Default interface to show when the App opens'),
373+
374+
/**
375+
* Navigation Tree Structure (Global Utility Entries Only).
376+
* Now repurposed for global utility items (Settings, Help, external links)
377+
* rendered at the bottom of the sidebar.
378+
*/
379+
navigation: z.array(NavigationItemSchema).optional()
380+
.describe('Global utility navigation items (Settings, Help, external links) — rendered at bottom of sidebar'),
381+
382+
// ... remaining fields ...
383+
});
384+
```
385+
386+
**Key Changes:**
387+
- Added `interfaces[]` — declares which interfaces belong to the app
388+
- Added `defaultInterface` — specifies which interface to show on app launch
389+
- Repurposed `navigation[]` — now for global utility entries only (Settings, Help, etc.)
390+
- The runtime auto-generates the main sidebar from `interfaces[]` and their `pages[]`
391+
348392
### 6.1 Interface Schema (New)
349393

350394
A new `InterfaceSchema` to represent the Airtable "Interface" concept — a self-contained,
@@ -360,6 +404,10 @@ export const InterfaceSchema = z.object({
360404
.describe('Display name'),
361405
description: z.string().optional()
362406
.describe('Purpose description'),
407+
icon: z.string().optional()
408+
.describe('Icon name for sidebar display (Lucide icon)'),
409+
group: z.string().optional()
410+
.describe('Business group label for sidebar grouping (e.g. "Sales Cloud", "Service Cloud")'),
363411
object: z.string().optional()
364412
.describe('Primary object binding (snake_case)'),
365413
pages: z.array(InterfacePageSchema)
@@ -655,6 +703,7 @@ export const EmbedConfigSchema = z.object({
655703
| 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 |
656704
| 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 |
657705
| 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 |
706+
| 12 | App.interfaces[] drives sidebar as Interface→Pages two-level menu | App's `navigation` was a hand-written tree that conflicted with Interface's `pages[]`. New model: App declares `interfaces[]`, runtime renders Interface.label as collapsible group → Interface.pages[] as menu items. `navigation` retained for global utility entries only (Settings, Help). Eliminates dual-navigation confusion. Added `defaultInterface` to specify startup interface. Interface gets `icon` and `group` fields for sidebar rendering. | 2026-02-16 |
658707

659708
---
660709

examples/app-crm/objectstack.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import * as agents from './src/agents';
1313
import * as ragPipelines from './src/rag';
1414
import * as profiles from './src/profiles';
1515
import * as apps from './src/apps';
16+
import * as interfaces from './src/interfaces';
1617
import * as translations from './src/translations';
1718
import { CrmSeedData } from './src/data';
1819

@@ -46,6 +47,7 @@ export default defineStack({
4647
ragPipelines: Object.values(ragPipelines),
4748
profiles: Object.values(profiles),
4849
apps: Object.values(apps),
50+
interfaces: Object.values(interfaces),
4951

5052
// Seed Data (top-level, registered as metadata)
5153
data: CrmSeedData,
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { defineApp } from '@objectstack/spec/ui';
4+
5+
/**
6+
* Modern CRM App using the new Interface-driven navigation pattern
7+
*
8+
* Demonstrates:
9+
* - App.interfaces[] for main navigation (auto-generates Interface→Pages sidebar)
10+
* - App.defaultInterface for startup interface
11+
* - App.navigation[] repurposed for global utility entries only (Settings, Help)
12+
*/
13+
export const CrmAppModern = defineApp({
14+
name: 'crm_modern',
15+
label: 'CRM (Modern)',
16+
description: 'Enterprise CRM with Interface-driven navigation',
17+
icon: 'briefcase',
18+
19+
branding: {
20+
primaryColor: '#4169E1',
21+
logo: '/assets/crm-logo.png',
22+
favicon: '/assets/crm-favicon.ico',
23+
},
24+
25+
// NEW: Interface-driven navigation
26+
// The sidebar will auto-render a two-level menu:
27+
// - Sales Cloud (group)
28+
// - Sales Workspace (interface) → Pipeline, Accounts, Leads (pages)
29+
// - Lead Review (interface) → Review Queue, Qualified Leads (pages)
30+
// - Analytics (group)
31+
// - Sales Analytics (interface) → Overview, Pipeline Report (pages)
32+
interfaces: ['sales_workspace', 'lead_review', 'sales_analytics'],
33+
34+
// NEW: Default interface on app launch
35+
defaultInterface: 'sales_workspace',
36+
37+
// REPURPOSED: navigation[] is now for global utility entries only
38+
// These render at the bottom of the sidebar
39+
navigation: [
40+
{
41+
id: 'nav_settings',
42+
type: 'page',
43+
label: 'Settings',
44+
icon: 'settings',
45+
pageName: 'admin_settings',
46+
},
47+
{
48+
id: 'nav_help',
49+
type: 'url',
50+
label: 'Help Center',
51+
icon: 'help-circle',
52+
url: 'https://help.example.com',
53+
target: '_blank',
54+
},
55+
],
56+
57+
requiredPermissions: ['app.access.crm'],
58+
isDefault: true,
59+
});

examples/app-crm/src/apps/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@
44
* App Definitions Barrel
55
*/
66
export { CrmApp } from './crm.app';
7+
export { CrmAppModern } from './crm_modern.app';
8+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
export { SalesWorkspaceInterface } from './sales_workspace.interface';
4+
export { LeadReviewInterface } from './lead_review.interface';
5+
export { SalesAnalyticsInterface } from './sales_analytics.interface';
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { defineInterface } from '@objectstack/spec/ui';
4+
5+
/**
6+
* Lead Review Interface
7+
* Sequential lead qualification and assignment workflow
8+
*/
9+
export const LeadReviewInterface = defineInterface({
10+
name: 'lead_review',
11+
label: 'Lead Review',
12+
description: 'Review and qualify incoming leads',
13+
icon: 'clipboard-check',
14+
group: 'Sales Cloud',
15+
object: 'lead',
16+
17+
pages: [
18+
{
19+
name: 'page_review_queue',
20+
label: 'Review Queue',
21+
type: 'record_review',
22+
icon: 'check-square',
23+
object: 'lead',
24+
recordReview: {
25+
object: 'lead',
26+
filter: { status: 'new' },
27+
sort: [{ field: 'created_at', order: 'desc' }],
28+
displayFields: ['company', 'title', 'email', 'phone', 'source'],
29+
actions: [
30+
{
31+
label: 'Qualify',
32+
type: 'approve',
33+
field: 'status',
34+
value: 'qualified',
35+
nextRecord: true,
36+
},
37+
{
38+
label: 'Disqualify',
39+
type: 'reject',
40+
field: 'status',
41+
value: 'disqualified',
42+
nextRecord: true,
43+
},
44+
{
45+
label: 'Skip',
46+
type: 'skip',
47+
nextRecord: true,
48+
},
49+
],
50+
navigation: 'sequential',
51+
showProgress: true,
52+
},
53+
regions: [],
54+
},
55+
{
56+
name: 'page_qualified',
57+
label: 'Qualified Leads',
58+
type: 'grid',
59+
icon: 'check-circle',
60+
object: 'lead',
61+
regions: [],
62+
},
63+
],
64+
65+
homePageName: 'page_review_queue',
66+
assignedRoles: ['sales_manager', 'lead_qualifier'],
67+
});
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { defineInterface } from '@objectstack/spec/ui';
4+
5+
/**
6+
* Sales Analytics Interface
7+
* Dashboards and reports for sales metrics
8+
*/
9+
export const SalesAnalyticsInterface = defineInterface({
10+
name: 'sales_analytics',
11+
label: 'Sales Analytics',
12+
description: 'Sales performance dashboards and analytics',
13+
icon: 'chart-line',
14+
group: 'Analytics',
15+
object: 'opportunity',
16+
17+
pages: [
18+
{
19+
name: 'page_overview',
20+
label: 'Overview',
21+
type: 'dashboard',
22+
icon: 'gauge',
23+
regions: [
24+
{
25+
name: 'main',
26+
components: [
27+
{
28+
type: 'element:text',
29+
properties: {
30+
content: '# Sales Performance',
31+
variant: 'heading',
32+
},
33+
},
34+
{
35+
type: 'element:number',
36+
properties: {
37+
object: 'opportunity',
38+
aggregate: 'count',
39+
},
40+
dataSource: {
41+
object: 'opportunity',
42+
filter: { stage: 'closed_won' },
43+
},
44+
},
45+
{
46+
type: 'element:number',
47+
properties: {
48+
object: 'opportunity',
49+
field: 'amount',
50+
aggregate: 'sum',
51+
format: 'currency',
52+
prefix: '$',
53+
},
54+
dataSource: {
55+
object: 'opportunity',
56+
filter: { stage: 'closed_won' },
57+
},
58+
},
59+
],
60+
},
61+
],
62+
},
63+
{
64+
name: 'page_pipeline',
65+
label: 'Pipeline Report',
66+
type: 'dashboard',
67+
icon: 'chart-bar',
68+
regions: [],
69+
},
70+
],
71+
72+
homePageName: 'page_overview',
73+
assignedRoles: ['sales_manager', 'exec'],
74+
branding: {
75+
primaryColor: '#1A73E8',
76+
},
77+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { defineInterface } from '@objectstack/spec/ui';
4+
5+
/**
6+
* Sales Workspace Interface
7+
* Primary sales rep workspace with pipeline, accounts, and leads
8+
*/
9+
export const SalesWorkspaceInterface = defineInterface({
10+
name: 'sales_workspace',
11+
label: 'Sales Workspace',
12+
description: 'Primary workspace for sales representatives',
13+
icon: 'briefcase',
14+
group: 'Sales Cloud',
15+
object: 'opportunity',
16+
17+
pages: [
18+
{
19+
name: 'page_pipeline',
20+
label: 'Pipeline',
21+
type: 'kanban',
22+
icon: 'columns',
23+
object: 'opportunity',
24+
regions: [],
25+
},
26+
{
27+
name: 'page_accounts',
28+
label: 'Accounts',
29+
type: 'grid',
30+
icon: 'building',
31+
object: 'account',
32+
regions: [],
33+
},
34+
{
35+
name: 'page_leads',
36+
label: 'Leads',
37+
type: 'list',
38+
icon: 'user-plus',
39+
object: 'lead',
40+
regions: [],
41+
},
42+
],
43+
44+
homePageName: 'page_pipeline',
45+
assignedRoles: ['sales_rep', 'sales_manager'],
46+
});

0 commit comments

Comments
 (0)