1- import { BrowserRouter , Routes , Route , Navigate , useNavigate , useLocation } from 'react-router-dom' ;
1+ import { BrowserRouter , Routes , Route , Navigate , useNavigate , useLocation , useSearchParams } from 'react-router-dom' ;
22import { useState , useEffect } from 'react' ;
33import { ObjectStackClient } from '@objectstack/client' ;
44import { ObjectForm } from '@object-ui/plugin-form' ;
5- import { Dialog , DialogContent , DialogHeader , DialogTitle , DialogDescription , Empty , EmptyTitle } from '@object-ui/components' ;
5+ import { Sheet , SheetContent , SheetHeader , SheetTitle , SheetDescription , Dialog , DialogContent , DialogHeader , DialogTitle , DialogDescription , Empty , EmptyTitle } from '@object-ui/components' ;
66import { SchemaRendererProvider } from '@object-ui/react' ;
77import { ObjectStackDataSource } from './dataSource' ;
88import appConfig from '../objectstack.config' ;
@@ -72,14 +72,13 @@ export function AppContent() {
7272 // App Selection
7373 const navigate = useNavigate ( ) ;
7474 const location = useLocation ( ) ;
75+ const [ searchParams , setSearchParams ] = useSearchParams ( ) ;
76+ const { appName } = useParams ( ) ;
7577 const apps = appConfig . apps || [ ] ;
7678
77- // Determine active app based on URL or default
79+ // Determine active app based on URL
7880 const activeApps = apps . filter ( ( a : any ) => a . active !== false ) ;
79- const defaultApp = activeApps . find ( ( a : any ) => a . isDefault === true ) || activeApps [ 0 ] ;
80- const [ activeAppName , setActiveAppName ] = useState < string > ( defaultApp ?. name || 'default' ) ;
81-
82- const activeApp = apps . find ( ( a : any ) => a . name === activeAppName ) || apps [ 0 ] ;
81+ const activeApp = apps . find ( ( a : any ) => a . name === appName ) || activeApps . find ( ( a : any ) => a . isDefault === true ) || activeApps [ 0 ] ;
8382
8483 const [ isDialogOpen , setIsDialogOpen ] = useState ( false ) ;
8584 const [ editingRecord , setEditingRecord ] = useState < any > ( null ) ;
@@ -89,7 +88,7 @@ export function AppContent() {
8988 initializeClient ( ) ;
9089 } , [ ] ) ;
9190
92- // Apply favicon from app branding
91+ // Sync title
9392 useEffect ( ( ) => {
9493 const favicon = activeApp ?. branding ?. favicon ;
9594 if ( favicon ) {
@@ -98,7 +97,6 @@ export function AppContent() {
9897 link . href = favicon ;
9998 }
10099 }
101- // Update document title with app label
102100 if ( activeApp ?. label ) {
103101 document . title = `${ activeApp . label } - ObjectStack Console` ;
104102 }
@@ -118,9 +116,18 @@ export function AppContent() {
118116
119117 const allObjects = appConfig . objects || [ ] ;
120118
121- // Find current object definition for Dialog (Create/Edit)
119+ // Find current object for Dialog
120+ // Path is now relative to /apps/:appName/
121+ // e.g. /apps/crm/contact -> contact is at index 3 (0=, 1=apps, 2=crm, 3=contact)
122122 const pathParts = location . pathname . split ( '/' ) ;
123- const objectNameFromPath = pathParts [ 1 ] ; // /contact -> contact
123+ // Filter out empty parts
124+ const cleanParts = pathParts . filter ( p => p ) ;
125+ // [apps, crm, contact]
126+ let objectNameFromPath = cleanParts [ 2 ] ;
127+ if ( objectNameFromPath === 'view' || objectNameFromPath === 'record' || objectNameFromPath === 'page' || objectNameFromPath === 'dashboard' ) {
128+ objectNameFromPath = '' ; // Not an object root
129+ }
130+
124131 const currentObjectDef = allObjects . find ( ( o : any ) => o . name === objectNameFromPath ) ;
125132
126133 const handleEdit = ( record : any ) => {
@@ -129,25 +136,19 @@ export function AppContent() {
129136 } ;
130137
131138 const handleRowClick = ( record : any ) => {
132- // Check for both string ID and Mongo/ObjectQL _id
133139 const id = record . _id || record . id ;
134- if ( id && currentObjectDef ) {
135- navigate ( `/${ currentObjectDef . name } /${ id } ` ) ;
140+ if ( id ) {
141+ // Open Drawer
142+ setSearchParams ( prev => {
143+ const next = new URLSearchParams ( prev ) ;
144+ next . set ( 'recordId' , id ) ;
145+ return next ;
146+ } ) ;
136147 }
137148 } ;
138149
139- const handleAppChange = ( appName : string ) => {
140- setActiveAppName ( appName ) ;
141- const app = apps . find ( ( a : any ) => a . name === appName ) ;
142- if ( app ) {
143- // Navigate to homePageId if defined in spec, otherwise first nav item
144- if ( app . homePageId ) {
145- navigate ( app . homePageId ) ;
146- } else {
147- const firstRoute = findFirstRoute ( app . navigation ) ;
148- navigate ( firstRoute ) ;
149- }
150- }
150+ const handleAppChange = ( newAppName : string ) => {
151+ navigate ( `/apps/${ newAppName } ` ) ;
151152 } ;
152153
153154 if ( ! client || ! dataSource ) return < LoadingScreen /> ;
@@ -161,31 +162,47 @@ export function AppContent() {
161162
162163 return (
163164 < ConsoleLayout
164- activeAppName = { activeAppName }
165+ activeAppName = { activeApp . name }
165166 activeApp = { activeApp }
166167 onAppChange = { handleAppChange }
167168 objects = { allObjects }
168169 >
169170 < SchemaRendererProvider dataSource = { dataSource || { } } >
170171 < Routes >
171172 < Route path = "/" element = {
173+ // Redirect to first route within the app
172174 < Navigate to = { findFirstRoute ( activeApp . navigation ) } replace />
173175 } />
174- < Route path = "/:objectName" element = {
176+
177+ { /* List View */ }
178+ < Route path = ":objectName" element = {
175179 < ObjectView
176180 dataSource = { dataSource }
177181 objects = { allObjects }
178182 onEdit = { handleEdit }
179183 onRowClick = { handleRowClick }
180184 />
181185 } />
182- < Route path = "/:objectName/:recordId" element = {
186+
187+ { /* List View with specific view */ }
188+ < Route path = ":objectName/view/:viewId" element = {
189+ < ObjectView
190+ dataSource = { dataSource }
191+ objects = { allObjects }
192+ onEdit = { handleEdit }
193+ onRowClick = { handleRowClick }
194+ />
195+ } />
196+
197+ { /* Detail Page */ }
198+ < Route path = ":objectName/record/:recordId" element = {
183199 < RecordDetailView key = { refreshKey } dataSource = { dataSource } objects = { allObjects } onEdit = { handleEdit } />
184200 } />
185- < Route path = "/dashboard/:dashboardName" element = {
201+
202+ < Route path = "dashboard/:dashboardName" element = {
186203 < DashboardView />
187204 } />
188- < Route path = "/ page/:pageName" element = {
205+ < Route path = "page/:pageName" element = {
189206 < PageView />
190207 } />
191208 </ Routes >
@@ -209,7 +226,6 @@ export function AppContent() {
209226 recordId : editingRecord ?. id ,
210227 layout : 'vertical' ,
211228 columns : 1 ,
212- // Support both KV object and array format for fields
213229 fields : currentObjectDef . fields
214230 ? ( Array . isArray ( currentObjectDef . fields )
215231 ? currentObjectDef . fields . map ( ( f : any ) => typeof f === 'string' ? f : f . name )
@@ -235,18 +251,30 @@ export function AppContent() {
235251
236252// Helper to find first valid route in navigation tree
237253function findFirstRoute ( items : any [ ] ) : string {
238- if ( ! items || items . length === 0 ) return '/ ' ;
254+ if ( ! items || items . length === 0 ) return '' ;
239255 for ( const item of items ) {
240- if ( item . type === 'object' ) return `/ ${ item . objectName } ` ;
241- if ( item . type === 'page' ) return item . pageName ? `/ page/${ item . pageName } ` : '/ ' ;
242- if ( item . type === 'dashboard' ) return item . dashboardName ? `/ dashboard/${ item . dashboardName } ` : '/ ' ;
256+ if ( item . type === 'object' ) return `${ item . objectName } ` ;
257+ if ( item . type === 'page' ) return item . pageName ? `page/${ item . pageName } ` : '' ;
258+ if ( item . type === 'dashboard' ) return item . dashboardName ? `dashboard/${ item . dashboardName } ` : '' ;
243259 if ( item . type === 'url' ) continue ; // Skip external URLs
244260 if ( item . type === 'group' && item . children ) {
245261 const childRoute = findFirstRoute ( item . children ) ; // Recurse
246- if ( childRoute !== '/ ' ) return childRoute ;
262+ if ( childRoute !== '' ) return childRoute ;
247263 }
248264 }
249- return '/' ;
265+ return '' ;
266+ }
267+
268+ // Redirect root to default app
269+ function RootRedirect ( ) {
270+ const apps = appConfig . apps || [ ] ;
271+ const activeApps = apps . filter ( ( a : any ) => a . active !== false ) ;
272+ const defaultApp = activeApps . find ( ( a : any ) => a . isDefault === true ) || activeApps [ 0 ] ;
273+
274+ if ( defaultApp ) {
275+ return < Navigate to = { `/apps/${ defaultApp . name } ` } replace /> ;
276+ }
277+ return < LoadingScreen /> ;
250278}
251279
252280import { ThemeProvider } from './components/theme-provider' ;
@@ -255,7 +283,10 @@ export function App() {
255283 return (
256284 < ThemeProvider defaultTheme = "system" storageKey = "object-ui-theme" >
257285 < BrowserRouter >
258- < AppContent />
286+ < Routes >
287+ < Route path = "/apps/:appName/*" element = { < AppContent /> } />
288+ < Route path = "/" element = { < RootRedirect /> } />
289+ </ Routes >
259290 </ BrowserRouter >
260291 </ ThemeProvider >
261292 ) ;
0 commit comments