Skip to content

Commit 61fe125

Browse files
committed
feat: refactor App component structure and add new AppHeader, AppSidebar, and ObjectView components
1 parent 6f698da commit 61fe125

5 files changed

Lines changed: 427 additions & 200 deletions

File tree

apps/console/src/App.tsx

Lines changed: 47 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -1,158 +1,33 @@
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';
22
import { useState, useEffect } from 'react';
33
import { ObjectStackClient } from '@objectstack/client';
44
import { 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';
75
import { 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';
97
import { ObjectStackDataSource } from './dataSource';
10-
import { LayoutDashboard, Users, Plus, Database, CheckSquare, Activity, Briefcase, FileText } from 'lucide-react';
118
import 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

14415
function 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
/>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useLocation, useParams } from 'react-router-dom';
2+
import {
3+
Breadcrumb,
4+
BreadcrumbItem,
5+
BreadcrumbLink,
6+
BreadcrumbList,
7+
BreadcrumbPage,
8+
BreadcrumbSeparator,
9+
Separator
10+
} from '@object-ui/components';
11+
12+
export function AppHeader({ appName, objects }: { appName: string, objects: any[] }) {
13+
const location = useLocation();
14+
const { objectName } = useParams();
15+
16+
// Find current object if we are on an object route
17+
// Note: This logic assumes simple paths for now.
18+
const isObjectPage = location.pathname.startsWith('/') && location.pathname.length > 1;
19+
const currentObject = isObjectPage ? objects.find((o: any) => o.name === location.pathname.substring(1)) : null;
20+
21+
return (
22+
<div className="flex items-center gap-2 px-4 h-full">
23+
<Breadcrumb>
24+
<BreadcrumbList>
25+
<BreadcrumbItem className="hidden md:block">
26+
<BreadcrumbLink href="#">
27+
{appName}
28+
</BreadcrumbLink>
29+
</BreadcrumbItem>
30+
{currentObject && (
31+
<>
32+
<BreadcrumbSeparator className="hidden md:block" />
33+
<BreadcrumbItem>
34+
<BreadcrumbPage>{currentObject.label}</BreadcrumbPage>
35+
</BreadcrumbItem>
36+
</>
37+
)}
38+
</BreadcrumbList>
39+
</Breadcrumb>
40+
</div>
41+
);
42+
}

0 commit comments

Comments
 (0)