1- import { BrowserRouter , Routes , Route , Navigate , useNavigate , useParams , useLocation , Link } from 'react-router-dom' ;
1+ import { BrowserRouter , Routes , Route , Navigate , useNavigate , useParams , useLocation } from 'react-router-dom' ;
22import { useState , useEffect } from 'react' ;
33import { ObjectStackClient } from '@objectstack/client' ;
44import { AppShell } from '@object-ui/layout' ;
5- import { Sidebar , SidebarContent , SidebarGroup , SidebarGroupLabel , SidebarGroupContent , SidebarMenu , SidebarMenuItem , SidebarMenuButton } from '@object-ui/components' ;
6- import { ObjectGrid } from '@object-ui/plugin-grid' ;
75import { ObjectForm } from '@object-ui/plugin-form' ;
8- import { Dialog , DialogContent , DialogHeader , DialogTitle , DialogDescription , Button } from '@object-ui/components' ;
6+ import { Dialog , DialogContent , DialogHeader , DialogTitle , DialogDescription } from '@object-ui/components' ;
97import { ObjectStackDataSource } from './dataSource' ;
10- import { LayoutDashboard , Users , Plus , Database , CheckSquare , Activity , Briefcase , FileText } from 'lucide-react' ;
118import appConfig from '../objectstack.config' ;
129
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 ,
23- } ;
24-
25- function getIcon ( name ?: string ) {
26- if ( ! name ) return Database ;
27- return ICONS [ name ] || Database ;
28- }
29-
30- function ObjectView ( { dataSource, objects, onEdit } : any ) {
31- const { objectName } = useParams ( ) ;
32- const [ refreshKey , setRefreshKey ] = useState ( 0 ) ;
33- const objectDef = objects . find ( ( o : any ) => o . name === objectName ) ;
34-
35- if ( ! objectDef ) return < div > Object { objectName } not found</ div > ;
36-
37- // Generate columns from fields if not specified
38- const normalizedFields = Array . isArray ( objectDef . fields )
39- ? objectDef . fields
40- : Object . entries ( objectDef . fields || { } ) . map ( ( [ key , value ] : [ string , any ] ) => ( { name : key , ...value } ) ) ;
41-
42- const columns = normalizedFields . map ( ( f : any ) => ( {
43- field : f . name ,
44- label : f . label || f . name ,
45- width : 150
46- } ) ) . slice ( 0 , 8 ) ;
47-
48- return (
49- < div className = "h-full flex flex-col gap-4" >
50- < div className = "flex justify-between items-center bg-white p-4 rounded-lg border border-slate-200 shadow-sm" >
51- < div >
52- < h1 className = "text-xl font-bold text-slate-900" > { objectDef . label } </ h1 >
53- < p className = "text-slate-500 text-sm" > { objectDef . description || 'Manage your records' } </ p >
54- </ div >
55- < Button onClick = { ( ) => onEdit ( null ) } className = "bg-blue-600 hover:bg-blue-700" >
56- < Plus className = "mr-2 h-4 w-4" /> New { objectDef . label }
57- </ Button >
58- </ div >
59-
60- < div className = "flex-1 bg-white rounded-lg border border-slate-200 shadow-sm overflow-hidden p-4" >
61- < ObjectGrid
62- key = { `${ objectName } -${ refreshKey } ` }
63- schema = { {
64- type : 'object-grid' ,
65- objectName : objectDef . name ,
66- filterable : true ,
67- columns : columns ,
68- } }
69- dataSource = { dataSource }
70- onEdit = { onEdit }
71- onDelete = { async ( record : any ) => {
72- if ( confirm ( `Delete record?` ) ) {
73- await dataSource . delete ( objectName , record . id ) ;
74- setRefreshKey ( k => k + 1 ) ;
75- }
76- } }
77- className = "h-full"
78- />
79- </ div >
80- </ div >
81- ) ;
82- }
83-
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- }
10+ // New Components
11+ import { AppSidebar } from './components/AppSidebar' ;
12+ import { ObjectView } from './components/ObjectView' ;
13+ import { AppHeader } from './components/AppHeader' ;
14314
14415function AppContent ( ) {
14516 const [ client , setClient ] = useState < ObjectStackClient | null > ( null ) ;
14617 const [ dataSource , setDataSource ] = useState < ObjectStackDataSource | null > ( null ) ;
14718
14819 // App Selection
20+ const navigate = useNavigate ( ) ;
21+ const location = useLocation ( ) ;
14922 const apps = appConfig . apps || [ ] ;
150- const [ activeAppName , setActiveAppName ] = useState < string > ( apps [ 0 ] ?. name || 'default' ) ;
15123
24+ // Determine active app based on URL or default
25+ // Ideally, valid routes should drive this state, but for now we keep local state
26+ // synced or just use local state.
27+ const [ activeAppName , setActiveAppName ] = useState < string > ( apps [ 0 ] ?. name || 'default' ) ;
28+
15229 const [ isDialogOpen , setIsDialogOpen ] = useState ( false ) ;
15330 const [ editingRecord , setEditingRecord ] = useState < any > ( null ) ;
154- const navigate = useNavigate ( ) ;
155- const location = useLocation ( ) ;
15631
15732 useEffect ( ( ) => {
15833 initializeClient ( ) ;
@@ -173,67 +48,63 @@ function AppContent() {
17348 const activeApp = apps . find ( ( a : any ) => a . name === activeAppName ) || apps [ 0 ] ;
17449 const allObjects = appConfig . objects || [ ] ;
17550
176- // Find current object definition for Dialog
177- const currentObjectDef = allObjects . find ( ( o : any ) => location . pathname === `/${ o . name } ` ) ;
51+ // Find current object definition for Dialog (Create/Edit)
52+ const pathParts = location . pathname . split ( '/' ) ;
53+ const objectNameFromPath = pathParts [ 1 ] ; // /contact -> contact
54+ const currentObjectDef = allObjects . find ( ( o : any ) => o . name === objectNameFromPath ) ;
17855
17956 const handleEdit = ( record : any ) => {
18057 setEditingRecord ( record ) ;
18158 setIsDialogOpen ( true ) ;
18259 } ;
18360
184- if ( ! client || ! dataSource ) return < div className = "flex items-center justify-center h-screen" > Loading ObjectStack...</ div > ;
61+ const handleAppChange = ( appName : string ) => {
62+ setActiveAppName ( appName ) ;
63+ const app = apps . find ( ( a : any ) => a . name === appName ) ;
64+ if ( app ) {
65+ // simplified nav logic
66+ const firstNav = app . navigation ?. [ 0 ] ;
67+ if ( firstNav ) {
68+ if ( firstNav . type === 'object' ) navigate ( `/${ firstNav . objectName } ` ) ;
69+ else if ( firstNav . type === 'group' && firstNav . children ?. [ 0 ] ?. objectName ) navigate ( `/${ firstNav . children [ 0 ] . objectName } ` ) ;
70+ else navigate ( '/' ) ;
71+ } else {
72+ navigate ( '/' ) ;
73+ }
74+ }
75+ } ;
76+
77+ if ( ! client || ! dataSource ) return < div className = "flex items-center justify-center h-screen text-muted-foreground animate-pulse" > Initializing ObjectStack Console...</ div > ;
18578 if ( ! activeApp ) return < div className = "p-4" > No Apps configured.</ div > ;
18679
18780 return (
18881 < AppShell
18982 sidebar = {
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 >
83+ < AppSidebar
84+ activeAppName = { activeAppName }
85+ onAppChange = { handleAppChange }
86+ />
19887 }
19988 navbar = {
200- < div className = "flex items-center justify-between w-full" >
201- < div className = "flex items-center gap-4" >
202- < h2 className = "text-lg font-semibold" > ObjectUI Workspace</ h2 >
203- < select
204- className = "border rounded px-2 py-1 text-sm bg-white"
205- value = { activeAppName }
206- onChange = { ( e ) => {
207- setActiveAppName ( e . target . value ) ;
208- navigate ( '/' ) ;
209- } }
210- >
211- { apps . map ( ( app : any ) => (
212- < option key = { app . name } value = { app . name } > { app . label } </ option >
213- ) ) }
214- </ select >
215- </ div >
216- < div className = "flex gap-2" >
217- < Button variant = "outline" size = "sm" > Help</ Button >
218- </ div >
219- </ div >
89+ < AppHeader appName = { activeApp . label } objects = { allObjects } />
22090 }
22191 >
22292 < Routes >
22393 < Route path = "/" element = {
224- /* Redirect to first navigable object in the active app */
225- < Navigate to = { findFirstRoute ( activeApp . navigation ) } replace />
94+ < Navigate to = { findFirstRoute ( activeApp . navigation ) } replace />
22695 } />
22796 < Route path = "/:objectName" element = {
22897 < ObjectView dataSource = { dataSource } objects = { allObjects } onEdit = { handleEdit } />
22998 } />
23099 </ Routes >
231100
232101 < Dialog open = { isDialogOpen } onOpenChange = { setIsDialogOpen } >
233- < DialogContent className = "sm:max-w-3xl max-h-[90vh] flex flex-col p -0 gap-0 " >
234- < DialogHeader className = "p-6 pb-2 border-b border-slate-100 " >
102+ < DialogContent className = "sm:max-w-xl max-h-[90vh] flex flex-col gap -0 p-0 overflow-hidden " >
103+ < DialogHeader className = "p-6 pb-4 border-b" >
235104 < DialogTitle > { editingRecord ? 'Edit' : 'Create' } { currentObjectDef ?. label } </ DialogTitle >
236- < DialogDescription > Fill out the details below.</ DialogDescription >
105+ < DialogDescription >
106+ { editingRecord ? `Update details for ${ currentObjectDef ?. label } ` : `Add a new ${ currentObjectDef ?. label } to your database.` }
107+ </ DialogDescription >
237108 </ DialogHeader >
238109 < div className = "flex-1 overflow-y-auto p-6" >
239110 { currentObjectDef && (
@@ -253,6 +124,8 @@ function AppContent() {
253124 onCancel : ( ) => setIsDialogOpen ( false ) ,
254125 showSubmit : true ,
255126 showCancel : true ,
127+ submitText : 'Save Record' ,
128+ cancelText : 'Cancel'
256129 } }
257130 dataSource = { dataSource }
258131 />
0 commit comments