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' ;
72import { useState , useEffect } from 'react' ;
83import { ObjectStackClient } from '@objectstack/client' ;
9- import { ObjectForm } from '@object-ui/plugin-form ' ;
4+ import { AppShell , SidebarNav } from '@object-ui/layout ' ;
105import { ObjectGrid } from '@object-ui/plugin-grid' ;
6+ import { ObjectForm } from '@object-ui/plugin-form' ;
117import { Dialog , DialogContent , DialogHeader , DialogTitle , DialogDescription , Button } from '@object-ui/components' ;
128import { 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