1- import { BrowserRouter , Routes , Route , Navigate , useNavigate , useParams , useLocation } from 'react-router-dom' ;
2- import { useState , useEffect , useMemo } from 'react' ;
1+ import { BrowserRouter , Routes , Route , Navigate , useNavigate , useParams , useLocation , Link } from 'react-router-dom' ;
2+ import { useState , useEffect } from 'react' ;
33import { ObjectStackClient } from '@objectstack/client' ;
4- import { AppShell , SidebarNav } from '@object-ui/layout' ;
4+ import { AppShell } from '@object-ui/layout' ;
5+ import { Sidebar , SidebarContent , SidebarGroup , SidebarGroupLabel , SidebarGroupContent , SidebarMenu , SidebarMenuItem , SidebarMenuButton } from '@object-ui/components' ;
56import { ObjectGrid } from '@object-ui/plugin-grid' ;
67import { ObjectForm } from '@object-ui/plugin-form' ;
78import { Dialog , DialogContent , DialogHeader , DialogTitle , DialogDescription , Button } from '@object-ui/components' ;
89import { ObjectStackDataSource } from './dataSource' ;
9- import { LayoutDashboard , Users , Plus , Database , Settings } from 'lucide-react' ;
10-
10+ import { LayoutDashboard , Users , Plus , Database , CheckSquare , Activity , Briefcase , FileText } from 'lucide-react' ;
1111import appConfig from '../objectstack.config' ;
1212
13- const APPS : any = {
14- // Filter objects based on source config (heuristic: contacts->crm, todo->todo)
15- crm : {
16- ...appConfig ,
17- name : 'crm' ,
18- label : 'CRM App' ,
19- objects : appConfig . objects ?. filter ( ( o : any ) => [ 'account' , 'contact' , 'opportunity' ] . includes ( o . name ) ) || [ ]
20- } ,
21- todo : {
22- ...appConfig ,
23- name : 'todo' ,
24- label : 'Todo App' ,
25- objects : appConfig . objects ?. filter ( ( o : any ) => [ 'todo_task' ] . includes ( o . name ) ) || [ ]
26- }
13+ // Icon Map for dynamic icons
14+ const ICONS : Record < string , any > = {
15+ 'dashboard' : LayoutDashboard ,
16+ 'users' : Users ,
17+ 'user' : Users ,
18+ 'check-square' : CheckSquare ,
19+ 'activity' : Activity ,
20+ 'briefcase' : Briefcase ,
21+ 'file-text' : FileText ,
22+ 'database' : Database ,
2723} ;
2824
29- function ObjectView ( { dataSource, config, onEdit } : any ) {
25+ function getIcon ( name ?: string ) {
26+ if ( ! name ) return Database ;
27+ return ICONS [ name ] || Database ;
28+ }
29+
30+ function ObjectView ( { dataSource, objects, onEdit } : any ) {
3031 const { objectName } = useParams ( ) ;
3132 const [ refreshKey , setRefreshKey ] = useState ( 0 ) ;
32- const objectDef = config . objects . find ( ( o : any ) => o . name === objectName ) ;
33+ const objectDef = objects . find ( ( o : any ) => o . name === objectName ) ;
3334
3435 if ( ! objectDef ) return < div > Object { objectName } not found</ div > ;
3536
36- // Generate columns from fields if not specified (simple auto-generation)
37- // Handle both array fields and object fields definitions
37+ // Generate columns from fields if not specified
3838 const normalizedFields = Array . isArray ( objectDef . fields )
3939 ? objectDef . fields
4040 : Object . entries ( objectDef . fields || { } ) . map ( ( [ key , value ] : [ string , any ] ) => ( { name : key , ...value } ) ) ;
@@ -43,7 +43,7 @@ function ObjectView({ dataSource, config, onEdit }: any) {
4343 field : f . name ,
4444 label : f . label || f . name ,
4545 width : 150
46- } ) ) . slice ( 0 , 8 ) ; // Limit to 8 columns for demo
46+ } ) ) . slice ( 0 , 8 ) ;
4747
4848 return (
4949 < div className = "h-full flex flex-col gap-4" >
@@ -81,10 +81,73 @@ function ObjectView({ dataSource, config, onEdit }: any) {
8181 ) ;
8282}
8383
84+ // Recursive Navigation Item Renderer
85+ function NavigationItemRenderer ( { item } : { item : any } ) {
86+ const Icon = getIcon ( item . icon ) ;
87+ const location = useLocation ( ) ;
88+
89+ if ( item . type === 'group' ) {
90+ return (
91+ < SidebarGroup >
92+ < SidebarGroupLabel > { item . label } </ SidebarGroupLabel >
93+ < SidebarGroupContent >
94+ < SidebarMenu >
95+ { item . children ?. map ( ( child : any ) => (
96+ < NavigationItemRenderer key = { child . id } item = { child } />
97+ ) ) }
98+ </ SidebarMenu >
99+ </ SidebarGroupContent >
100+ </ SidebarGroup >
101+ ) ;
102+ }
103+
104+ // Default object/page items
105+ const href = item . type === 'object' ? `/${ item . objectName } ` : ( item . path || '#' ) ;
106+ const isActive = location . pathname === href ;
107+
108+ return (
109+ < SidebarMenuItem >
110+ < SidebarMenuButton asChild isActive = { isActive } >
111+ < Link to = { href } >
112+ < Icon className = "mr-2 h-4 w-4" />
113+ < span > { item . label } </ span >
114+ </ Link >
115+ </ SidebarMenuButton >
116+ </ SidebarMenuItem >
117+ ) ;
118+ }
119+
120+ function NavigationTree ( { items } : { items : any [ ] } ) {
121+ // If top level items are mixed (groups and non-groups), wrap non-groups in a generic group or render directly
122+ const hasGroups = items . some ( i => i . type === 'group' ) ;
123+
124+ if ( hasGroups ) {
125+ return (
126+ < >
127+ { items . map ( item => < NavigationItemRenderer key = { item . id } item = { item } /> ) }
128+ </ >
129+ ) ;
130+ }
131+
132+ // Flat list (create a default group)
133+ return (
134+ < SidebarGroup >
135+ < SidebarGroupContent >
136+ < SidebarMenu >
137+ { items . map ( item => < NavigationItemRenderer key = { item . id } item = { item } /> ) }
138+ </ SidebarMenu >
139+ </ SidebarGroupContent >
140+ </ SidebarGroup >
141+ ) ;
142+ }
143+
84144function AppContent ( ) {
85145 const [ client , setClient ] = useState < ObjectStackClient | null > ( null ) ;
86146 const [ dataSource , setDataSource ] = useState < ObjectStackDataSource | null > ( null ) ;
87- const [ activeAppKey , setActiveAppKey ] = useState < string > ( 'crm' ) ;
147+
148+ // App Selection
149+ const apps = appConfig . apps || [ ] ;
150+ const [ activeAppName , setActiveAppName ] = useState < string > ( apps [ 0 ] ?. name || 'default' ) ;
88151
89152 const [ isDialogOpen , setIsDialogOpen ] = useState ( false ) ;
90153 const [ editingRecord , setEditingRecord ] = useState < any > ( null ) ;
@@ -107,50 +170,47 @@ function AppContent() {
107170 }
108171 }
109172
110- const activeConfig = APPS [ activeAppKey ] ;
111- const currentObjectDef = activeConfig . objects . find ( ( o : any ) => location . pathname === `/${ o . name } ` ) ;
112-
113- // Sidebar items from active app objects
114- const sidebarItems = useMemo ( ( ) => {
115- // Filter out objects that might not be top-level or are internal if needed
116- return [
117- ...activeConfig . objects . map ( ( obj : any ) => ( {
118- title : obj . label ,
119- href : `/${ obj . name } ` ,
120- icon : obj . name === 'contact' ? Users : Database
121- } ) )
122- ] ;
123- } , [ activeConfig ] ) ;
173+ const activeApp = apps . find ( ( a : any ) => a . name === activeAppName ) || apps [ 0 ] ;
174+ const allObjects = appConfig . objects || [ ] ;
175+
176+ // Find current object definition for Dialog
177+ const currentObjectDef = allObjects . find ( ( o : any ) => location . pathname === `/${ o . name } ` ) ;
124178
125179 const handleEdit = ( record : any ) => {
126180 setEditingRecord ( record ) ;
127181 setIsDialogOpen ( true ) ;
128182 } ;
129183
130184 if ( ! client || ! dataSource ) return < div className = "flex items-center justify-center h-screen" > Loading ObjectStack...</ div > ;
185+ if ( ! activeApp ) return < div className = "p-4" > No Apps configured.</ div > ;
131186
132187 return (
133188 < AppShell
134189 sidebar = {
135- < SidebarNav
136- title = { activeConfig . label }
137- items = { sidebarItems }
138- />
190+ < Sidebar collapsible = "icon" >
191+ < SidebarContent >
192+ < div className = "p-2 font-semibold text-xs text-slate-500 uppercase tracking-wider pl-4 mt-2" >
193+ { activeApp . label }
194+ </ div >
195+ < NavigationTree items = { activeApp . navigation || [ ] } />
196+ </ SidebarContent >
197+ </ Sidebar >
139198 }
140199 navbar = {
141200 < div className = "flex items-center justify-between w-full" >
142201 < div className = "flex items-center gap-4" >
143- < h2 className = "text-lg font-semibold" > Workspace</ h2 >
202+ < h2 className = "text-lg font-semibold" > ObjectUI Workspace</ h2 >
144203 < select
145204 className = "border rounded px-2 py-1 text-sm bg-white"
146- value = { activeAppKey }
205+ value = { activeAppName }
147206 onChange = { ( e ) => {
148- setActiveAppKey ( e . target . value ) ;
207+ setActiveAppName ( e . target . value ) ;
149208 navigate ( '/' ) ;
150209 } }
151210 >
152- < option value = "crm" > CRM App</ option >
153- < option value = "todo" > Todo App</ option >
211+ { apps . map ( ( app : any ) => (
212+ < option key = { app . name } value = { app . name } > { app . label } </ option >
213+ ) ) }
154214 </ select >
155215 </ div >
156216 < div className = "flex gap-2" >
@@ -160,9 +220,12 @@ function AppContent() {
160220 }
161221 >
162222 < Routes >
163- < Route path = "/" element = { < Navigate to = { `/${ activeConfig . objects [ 0 ] ?. name || '' } ` } replace /> } />
223+ < Route path = "/" element = {
224+ /* Redirect to first navigable object in the active app */
225+ < Navigate to = { findFirstRoute ( activeApp . navigation ) } replace />
226+ } />
164227 < Route path = "/:objectName" element = {
165- < ObjectView dataSource = { dataSource } config = { activeConfig } onEdit = { handleEdit } />
228+ < ObjectView dataSource = { dataSource } objects = { allObjects } onEdit = { handleEdit } />
166229 } />
167230 </ Routes >
168231
@@ -201,6 +264,20 @@ function AppContent() {
201264 ) ;
202265}
203266
267+ // Helper to find first valid route in navigation tree
268+ function findFirstRoute ( items : any [ ] ) : string {
269+ if ( ! items || items . length === 0 ) return '/' ;
270+ for ( const item of items ) {
271+ if ( item . type === 'object' ) return `/${ item . objectName } ` ;
272+ if ( item . type === 'page' ) return item . path ;
273+ if ( item . type === 'group' && item . children ) {
274+ const childRoute = findFirstRoute ( item . children ) ; // Recurse
275+ if ( childRoute !== '/' ) return childRoute ;
276+ }
277+ }
278+ return '/' ;
279+ }
280+
204281export function App ( ) {
205282 return (
206283 < BrowserRouter >
0 commit comments