Skip to content

Commit 3212c74

Browse files
committed
Update App.tsx
1 parent 6ad0234 commit 3212c74

File tree

1 file changed

+127
-50
lines changed
  • examples/msw-object-form/src

1 file changed

+127
-50
lines changed

examples/msw-object-form/src/App.tsx

Lines changed: 127 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,40 @@
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';
33
import { 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';
56
import { ObjectGrid } from '@object-ui/plugin-grid';
67
import { ObjectForm } from '@object-ui/plugin-form';
78
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Button } from '@object-ui/components';
89
import { 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';
1111
import 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+
84144
function 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+
204281
export function App() {
205282
return (
206283
<BrowserRouter>

0 commit comments

Comments
 (0)