Skip to content

Commit 89bd89c

Browse files
committed
Enhance routing and navigation in App component, add drawer for record details in ObjectView, and improve sidebar navigation with active app context
1 parent dcb8570 commit 89bd89c

3 files changed

Lines changed: 138 additions & 57 deletions

File tree

apps/console/src/App.tsx

Lines changed: 70 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
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';
22
import { useState, useEffect } from 'react';
33
import { ObjectStackClient } from '@objectstack/client';
44
import { 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';
66
import { SchemaRendererProvider } from '@object-ui/react';
77
import { ObjectStackDataSource } from './dataSource';
88
import 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
237253
function 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

252280
import { 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
);

apps/console/src/components/AppSidebar.tsx

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
145145
</SidebarHeader>
146146

147147
<SidebarContent>
148-
<NavigationTree items={activeApp.navigation || []} />
148+
<NavigationTree items={activeApp.navigation || []} activeAppName={activeAppName} />
149149
</SidebarContent>
150150

151151
<SidebarFooter>
@@ -207,7 +207,7 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
207207
);
208208
}
209209

210-
function NavigationTree({ items }: { items: any[] }) {
210+
function NavigationTree({ items, activeAppName }: { items: any[], activeAppName: string }) {
211211
const hasGroups = items.some(i => i.type === 'group');
212212

213213
// If no explicit groups, wrap everything in one default group
@@ -216,7 +216,7 @@ function NavigationTree({ items }: { items: any[] }) {
216216
<SidebarGroup>
217217
<SidebarGroupContent>
218218
<SidebarMenu>
219-
{items.map(item => <NavigationItemRenderer key={item.id} item={item} />)}
219+
{items.map(item => <NavigationItemRenderer key={item.id} item={item} activeAppName={activeAppName} />)}
220220
</SidebarMenu>
221221
</SidebarGroupContent>
222222
</SidebarGroup>
@@ -236,7 +236,7 @@ function NavigationTree({ items }: { items: any[] }) {
236236
<SidebarGroupContent>
237237
<SidebarMenu>
238238
{currentBuffer.map(item => (
239-
<NavigationItemRenderer key={item.id} item={item} />
239+
<NavigationItemRenderer key={item.id} item={item} activeAppName={activeAppName} />
240240
))}
241241
</SidebarMenu>
242242
</SidebarGroupContent>
@@ -248,7 +248,7 @@ function NavigationTree({ items }: { items: any[] }) {
248248
items.forEach((item, index) => {
249249
if (item.type === 'group') {
250250
flushBuffer(`auto-${index}`);
251-
renderedItems.push(<NavigationItemRenderer key={item.id} item={item} />);
251+
renderedItems.push(<NavigationItemRenderer key={item.id} item={item} activeAppName={activeAppName} />);
252252
} else {
253253
currentBuffer.push(item);
254254
}
@@ -259,7 +259,7 @@ function NavigationTree({ items }: { items: any[] }) {
259259
return <>{renderedItems}</>;
260260
}
261261

262-
function NavigationItemRenderer({ item }: { item: any }) {
262+
function NavigationItemRenderer({ item, activeAppName }: { item: any, activeAppName: string }) {
263263
const Icon = getIcon(item.icon);
264264
const location = useLocation();
265265
const [isOpen, setIsOpen] = React.useState(item.expanded !== false);
@@ -285,7 +285,7 @@ function NavigationItemRenderer({ item }: { item: any }) {
285285
<SidebarGroupContent>
286286
<SidebarMenu>
287287
{item.children?.map((child: any) => (
288-
<NavigationItemRenderer key={child.id} item={child} />
288+
<NavigationItemRenderer key={child.id} item={child} activeAppName={activeAppName} />
289289
))}
290290
</SidebarMenu>
291291
</SidebarGroupContent>
@@ -298,28 +298,29 @@ function NavigationItemRenderer({ item }: { item: any }) {
298298
// Determine href based on navigation item type
299299
let href = '#';
300300
let isExternal = false;
301+
const baseUrl = `/apps/${activeAppName}`;
301302

302303
if (item.type === 'object') {
303-
href = `/${item.objectName}`;
304+
href = `${baseUrl}/${item.objectName}`;
304305
// Add view parameter if specified
305306
if (item.viewName) {
306-
href += `?view=${item.viewName}`;
307+
href += `/view/${item.viewName}`;
307308
}
308309
} else if (item.type === 'page') {
309-
href = item.pageName ? `/page/${item.pageName}` : '#';
310+
href = item.pageName ? `${baseUrl}/page/${item.pageName}` : '#';
310311
// Add URL parameters if specified
311312
if (item.params) {
312313
const params = new URLSearchParams(item.params);
313314
href += `?${params.toString()}`;
314315
}
315316
} else if (item.type === 'dashboard') {
316-
href = item.dashboardName ? `/dashboard/${item.dashboardName}` : '#';
317+
href = item.dashboardName ? `${baseUrl}/dashboard/${item.dashboardName}` : '#';
317318
} else if (item.type === 'url') {
318319
href = item.url || '#';
319320
isExternal = item.target === '_blank';
320321
}
321322

322-
const isActive = location.pathname === href; // Simple active check
323+
const isActive = location.pathname.startsWith(href) && href !== '#';
323324

324325
return (
325326
<SidebarMenuItem>

0 commit comments

Comments
 (0)