Skip to content

Commit a6a8aa8

Browse files
committed
Add @object-ui/layout dependency and refactor App component for improved routing and layout
1 parent 7c96bbe commit a6a8aa8

File tree

3 files changed

+236
-181
lines changed

3 files changed

+236
-181
lines changed

examples/msw-object-form/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"dependencies": {
1717
"@object-ui/components": "workspace:*",
1818
"@object-ui/fields": "workspace:*",
19+
"@object-ui/layout": "^0.1.1",
1920
"@object-ui/plugin-dashboard": "workspace:*",
2021
"@object-ui/plugin-form": "workspace:*",
2122
"@object-ui/plugin-grid": "workspace:*",
@@ -29,7 +30,8 @@
2930
"@objectstack/spec": "^0.7.2",
3031
"lucide-react": "^0.563.0",
3132
"react": "^19.0.0",
32-
"react-dom": "^19.0.0"
33+
"react-dom": "^19.0.0",
34+
"react-router-dom": "^7.13.0"
3335
},
3436
"devDependencies": {
3537
"@tailwindcss/postcss": "^4.1.18",
Lines changed: 102 additions & 180 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,19 @@
1-
/**
2-
* App Component
3-
*
4-
* Main application component demonstrating ObjectForm with MSW
5-
*/
6-
1+
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
72
import { useState, useEffect } from 'react';
83
import { ObjectStackClient } from '@objectstack/client';
9-
import { ObjectForm } from '@object-ui/plugin-form';
4+
import { AppShell, SidebarNav } from '@object-ui/layout';
105
import { ObjectGrid } from '@object-ui/plugin-grid';
6+
import { ObjectForm } from '@object-ui/plugin-form';
117
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Button } from '@object-ui/components';
128
import { ObjectStackDataSource } from './dataSource';
13-
import type { Contact } from './types';
14-
import './App.css';
15-
import { Plus, LayoutDashboard, Users, Settings } from 'lucide-react';
9+
import { LayoutDashboard, Users, Settings, Plus } from 'lucide-react';
10+
import appConfig from '../objectstack.config';
1611

17-
export function App() {
12+
function AppContent() {
1813
const [client, setClient] = useState<ObjectStackClient | null>(null);
1914
const [dataSource, setDataSource] = useState<ObjectStackDataSource | null>(null);
2015
const [isDialogOpen, setIsDialogOpen] = useState(false);
21-
const [editingRecord, setEditingRecord] = useState<Contact | null>(null);
22-
const [connected, setConnected] = useState(false);
23-
const [error, setError] = useState<string | null>(null);
16+
const [editingRecord, setEditingRecord] = useState<any>(null);
2417
const [refreshKey, setRefreshKey] = useState(0);
2518

2619
useEffect(() => {
@@ -29,24 +22,13 @@ export function App() {
2922

3023
async function initializeClient() {
3124
try {
32-
// Initialize ObjectStack Client pointing to our mocked API
33-
const stackClient = new ObjectStackClient({
34-
baseUrl: ''
35-
});
36-
37-
// Wait a bit to ensure MSW is fully ready
25+
const stackClient = new ObjectStackClient({ baseUrl: '' });
3826
await new Promise(resolve => setTimeout(resolve, 500));
3927
await stackClient.connect();
40-
41-
const ds = new ObjectStackDataSource(stackClient);
42-
4328
setClient(stackClient);
44-
setDataSource(ds);
45-
setConnected(true);
46-
console.log('✅ ObjectStack Client connected (via MSW)');
29+
setDataSource(new ObjectStackDataSource(stackClient));
4730
} catch (err) {
48-
setError(err instanceof Error ? err.message : 'Failed to initialize client');
49-
console.error('Failed to initialize client:', err);
31+
console.error(err);
5032
}
5133
}
5234

@@ -60,182 +42,122 @@ export function App() {
6042
setIsDialogOpen(true);
6143
};
6244

63-
const handleSuccess = () => {
64-
setIsDialogOpen(false);
65-
setRefreshKey(k => k + 1);
66-
}
45+
if (!client || !dataSource) return <div className="flex items-center justify-center h-screen">Loading ObjectStack...</div>;
6746

68-
if (error) {
69-
return (
70-
<div className="min-h-screen flex items-center justify-center p-6 bg-slate-50">
71-
<div className="max-w-md w-full p-8 border border-red-300 bg-red-50 rounded-lg shadow-sm text-center">
72-
<h1 className="text-xl font-bold text-red-800 mb-2">Connection Error</h1>
73-
<p className="text-red-600 mb-6">{error}</p>
74-
<button
75-
onClick={initializeClient}
76-
className="px-4 py-2 bg-gray-900 text-white rounded-md hover:bg-gray-700 transition-colors"
77-
>
78-
Retry
79-
</button>
80-
</div>
81-
</div>
82-
);
83-
}
47+
const contactsObject = appConfig.objects?.find(o => o.name === 'contact') || appConfig.objects?.[0];
8448

85-
if (!connected || !client || !dataSource) {
86-
return (
87-
<div className="min-h-screen flex flex-col items-center justify-center p-6 space-y-4 bg-slate-50">
88-
<div className="w-8 h-8 rounded-full border-4 border-gray-200 border-t-gray-900 animate-spin"></div>
89-
<div className="text-center">
90-
<h1 className="text-lg font-semibold mb-1 text-gray-900">Connecting to ObjectStack...</h1>
91-
<p className="text-gray-600 text-sm">Initializing MSW and ObjectStack Client...</p>
92-
</div>
93-
</div>
94-
);
95-
}
49+
if (!contactsObject) return <div className="p-4">No object definition found in configuration.</div>;
9650

9751
return (
98-
<div className="flex h-screen w-full bg-slate-50 overflow-hidden">
99-
{/* Sidebar */}
100-
<aside className="w-64 bg-white border-r border-slate-200 hidden md:flex flex-col flex-shrink-0">
101-
<div className="h-16 flex items-center px-6 border-b border-slate-100">
102-
<span className="font-bold text-xl text-slate-800 tracking-tight">ObjectUI</span>
103-
</div>
104-
<nav className="flex-1 p-4 space-y-1 overflow-y-auto">
105-
<Button variant="ghost" className="w-full justify-start font-medium" disabled>
106-
<LayoutDashboard className="mr-2 h-4 w-4 text-slate-500" /> Dashboard
107-
</Button>
108-
<Button variant="secondary" className="w-full justify-start font-medium text-slate-900 bg-slate-100">
109-
<Users className="mr-2 h-4 w-4 text-slate-700" /> Contacts
110-
</Button>
111-
<Button variant="ghost" className="w-full justify-start font-medium text-slate-600 hover:text-slate-900">
112-
<Settings className="mr-2 h-4 w-4 text-slate-500" /> Settings
113-
</Button>
114-
</nav>
115-
<div className="p-4 border-t border-slate-100">
116-
<div className="flex items-center gap-3">
117-
<div className="w-8 h-8 rounded-full bg-slate-200"></div>
118-
<div className="text-sm">
119-
<p className="font-medium text-slate-700">Admin User</p>
120-
<p className="text-slate-500 text-xs">admin@example.com</p>
52+
<AppShell
53+
sidebar={
54+
<SidebarNav
55+
title="ObjectUI CRM"
56+
items={[
57+
{ title: 'Dashboard', href: '/', icon: LayoutDashboard },
58+
{ title: 'Contacts', href: '/contacts', icon: Users },
59+
{ title: 'Settings', href: '/settings', icon: Settings }
60+
]}
61+
/>
62+
}
63+
navbar={
64+
<div className="flex items-center justify-between w-full">
65+
<h2 className="text-lg font-semibold">CRM Workspace</h2>
66+
<div className="flex gap-2">
67+
<Button variant="outline" size="sm">Help</Button>
68+
</div>
69+
</div>
70+
}
71+
>
72+
<Routes>
73+
<Route path="/" element={<Navigate to="/contacts" replace />} />
74+
<Route path="/contacts" element={
75+
<div className="h-full flex flex-col gap-4">
76+
<div className="flex justify-between items-center bg-white p-4 rounded-lg border border-slate-200 shadow-sm">
77+
<div>
78+
<h1 className="text-xl font-bold text-slate-900">{contactsObject.label}</h1>
79+
<p className="text-slate-500 text-sm">Manage all your contacts in one place</p>
12180
</div>
81+
<Button onClick={handleCreate} className="bg-blue-600 hover:bg-blue-700">
82+
<Plus className="mr-2 h-4 w-4" /> New {contactsObject.label}
83+
</Button>
12284
</div>
123-
</div>
124-
</aside>
125-
126-
{/* Main Content */}
127-
<main className="flex-1 flex flex-col min-w-0 h-full relative">
128-
{/* Header */}
129-
<header className="flex-shrink-0 h-16 bg-white border-b border-slate-200 flex items-center justify-between px-6 shadow-sm z-10 w-full">
130-
<div>
131-
<h1 className="text-xl font-semibold text-slate-800">Contacts</h1>
132-
<p className="text-sm text-slate-500">Manage your organization's contacts</p>
133-
</div>
134-
<Button onClick={handleCreate} className="bg-blue-600 hover:bg-blue-700 text-white shadow-sm">
135-
<Plus className="mr-2 h-4 w-4" /> New Contact
136-
</Button>
137-
</header>
13885

139-
{/* Table Area */}
140-
<div className="flex-1 overflow-hidden p-6 relative">
141-
<div className="rounded-xl border border-slate-200 bg-white shadow-sm h-full flex flex-col overflow-hidden">
142-
<ObjectGrid
143-
key={refreshKey}
144-
schema={{
145-
type: 'object-grid',
146-
objectName: 'contact',
147-
// Enable comprehensive features
148-
filterable: true,
149-
searchableFields: ['name', 'email', 'company'],
150-
sortable: true,
151-
resizable: true,
152-
pagination: {
153-
pageSize: 10
154-
},
155-
selection: {
156-
type: 'multiple'
157-
},
158-
columns: [
86+
<div className="flex-1 bg-white rounded-lg border border-slate-200 shadow-sm overflow-hidden p-4">
87+
<ObjectGrid
88+
key={refreshKey}
89+
schema={{
90+
type: 'object-grid',
91+
objectName: contactsObject.name,
92+
filterable: true,
93+
searchableFields: ['name', 'email', 'company'],
94+
columns: [
15995
{ field: 'name', label: 'Name', width: 200, fixed: 'left' },
16096
{ field: 'email', label: 'Email', width: 220 },
16197
{ field: 'phone', label: 'Phone', width: 150 },
16298
{ field: 'company', label: 'Company', width: 180 },
163-
{ field: 'position', label: 'Position', width: 150 },
16499
{ field: 'department', label: 'Department', width: 150 },
165100
{ field: 'priority', label: 'Priority', width: 100 },
166101
{ field: 'salary', label: 'Salary', width: 120 },
167-
{ field: 'commission_rate', label: 'Commission', width: 120 },
168-
{ field: 'birthdate', label: 'Birthdate', width: 120 },
169102
{ field: 'is_active', label: 'Active', width: 100 },
170-
{ field: 'last_contacted', label: 'Last Contacted', width: 180 },
171-
{ field: 'profile_url', label: 'LinkedIn', width: 200 },
172-
]
173-
}}
174-
dataSource={dataSource}
175-
onEdit={handleEdit}
176-
onRowSelect={(selected) => console.log('Selected rows:', selected)}
177-
onDelete={async (record) => {
178-
if (confirm(`Are you sure you want to delete ${record.name}?`)) {
179-
await dataSource?.delete('contact', record.id);
180-
setRefreshKey(k => k + 1);
181-
}
182-
}}
183-
className="h-full w-full"
103+
]
104+
}}
105+
dataSource={dataSource}
106+
onEdit={handleEdit}
107+
onDelete={async (record) => {
108+
if (confirm(`Delete ${record.name}?`)) {
109+
await dataSource.delete(contactsObject.name, record.id);
110+
setRefreshKey(k => k + 1);
111+
}
112+
}}
113+
className="h-full"
184114
/>
185115
</div>
186116
</div>
187-
</main>
117+
} />
118+
<Route path="*" element={<div>Page not found</div>} />
119+
</Routes>
188120

189-
{/* Form Dialog */}
190121
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
191-
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto flex flex-col gap-0 p-0">
122+
<DialogContent className="sm:max-w-3xl max-h-[90vh] flex flex-col p-0 gap-0">
192123
<DialogHeader className="p-6 pb-2 border-b border-slate-100">
193-
<DialogTitle className="text-xl">{editingRecord ? 'Edit Contact' : 'New Contact'}</DialogTitle>
194-
<DialogDescription>
195-
{editingRecord ? 'Update contact details below.' : 'Fill in the information to create a new contact.'}
196-
</DialogDescription>
124+
<DialogTitle>{editingRecord ? 'Edit' : 'Create'} {contactsObject.label}</DialogTitle>
125+
<DialogDescription>Fill out the details below.</DialogDescription>
197126
</DialogHeader>
198-
199-
<div className="p-6 pt-4 flex-1 overflow-y-auto">
200-
<ObjectForm
201-
schema={{
202-
type: 'object-form',
203-
objectName: 'contact',
204-
mode: editingRecord ? 'edit' : 'create',
205-
recordId: editingRecord?.id,
206-
fields: [
207-
'name',
208-
'email',
209-
'phone',
210-
'company',
211-
'position',
212-
'priority',
213-
'salary',
214-
'commission_rate',
215-
'birthdate',
216-
'last_contacted',
217-
'available_time',
218-
'profile_url',
219-
'department',
220-
'resume',
221-
'avatar',
222-
'is_active',
223-
'notes'
224-
],
225-
layout: 'vertical',
226-
columns: 2,
227-
onSuccess: handleSuccess,
228-
onCancel: () => setIsDialogOpen(false),
229-
showSubmit: true,
230-
showCancel: true,
231-
submitText: editingRecord ? 'Save Changes' : 'Create Contact',
232-
}}
233-
dataSource={dataSource}
234-
className="space-y-4"
235-
/>
127+
<div className="flex-1 overflow-y-auto p-6">
128+
<ObjectForm
129+
schema={{
130+
type: 'object-form',
131+
objectName: contactsObject.name,
132+
mode: editingRecord ? 'edit' : 'create',
133+
recordId: editingRecord?.id,
134+
layout: 'vertical',
135+
columns: 2,
136+
fields: [
137+
'name', 'email', 'phone', 'company',
138+
'department', 'priority', 'salary', 'commission_rate',
139+
'birthdate', 'available_time', 'is_active', 'notes',
140+
'profile_url', 'avatar'
141+
],
142+
onSuccess: () => { setIsDialogOpen(false); setRefreshKey(k => k + 1); },
143+
onCancel: () => setIsDialogOpen(false),
144+
showSubmit: true,
145+
showCancel: true,
146+
submitText: editingRecord ? 'Save Changes' : 'Create Record'
147+
}}
148+
dataSource={dataSource}
149+
/>
236150
</div>
237151
</DialogContent>
238152
</Dialog>
239-
</div>
153+
</AppShell>
154+
);
155+
}
156+
157+
export function App() {
158+
return (
159+
<BrowserRouter>
160+
<AppContent />
161+
</BrowserRouter>
240162
);
241163
}

0 commit comments

Comments
 (0)