Skip to content

Commit d488d81

Browse files
committed
Refactor dashboard UI and improve component structure
Introduces a new AppSidebar component, updates Dashboard layout to use SidebarProvider and Breadcrumbs, and refactors ObjectListView, ObjectDetailView, ObjectForm, and SettingsView for improved UI consistency and usability. Removes AgGrid in favor of custom Table and Grid views, updates form handling to use react-hook-form, and applies design system colors and spacing. Also updates several UI components for better accessibility and code clarity.
1 parent 3b9a533 commit d488d81

File tree

11 files changed

+451
-362
lines changed

11 files changed

+451
-362
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import * as React from "react"
2+
import {
3+
Sidebar,
4+
SidebarContent,
5+
SidebarGroup,
6+
SidebarGroupContent,
7+
SidebarGroupLabel,
8+
SidebarHeader,
9+
SidebarMenu,
10+
SidebarMenuButton,
11+
SidebarMenuItem,
12+
SidebarRail,
13+
SidebarFooter,
14+
} from "@objectql/ui"
15+
import { Database, Settings, FileText } from "lucide-react"
16+
import { useRouter } from "../hooks/useRouter"
17+
18+
export function AppSidebar({ objects, ...props }: React.ComponentProps<typeof Sidebar> & { objects: Record<string, any> }) {
19+
const { path, navigate } = useRouter()
20+
21+
return (
22+
<Sidebar collapsible="icon" {...props}>
23+
<SidebarHeader>
24+
<SidebarMenu>
25+
<SidebarMenuItem>
26+
<SidebarMenuButton size="lg" onClick={() => navigate('/')}>
27+
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
28+
<Database className="size-4" />
29+
</div>
30+
<div className="grid flex-1 text-left text-sm leading-tight">
31+
<span className="truncate font-semibold">ObjectQL</span>
32+
<span className="truncate text-xs">Data Browser</span>
33+
</div>
34+
</SidebarMenuButton>
35+
</SidebarMenuItem>
36+
</SidebarMenu>
37+
</SidebarHeader>
38+
<SidebarContent>
39+
<SidebarGroup>
40+
<SidebarGroupLabel>Collections</SidebarGroupLabel>
41+
<SidebarGroupContent>
42+
<SidebarMenu>
43+
{Object.entries(objects).map(([name, schema]) => (
44+
<SidebarMenuItem key={name}>
45+
<SidebarMenuButton
46+
isActive={path.startsWith(`/object/${name}`)}
47+
onClick={() => navigate(`/object/${name}`)}
48+
>
49+
<FileText />
50+
<span>{schema.label || schema.title || name}</span>
51+
</SidebarMenuButton>
52+
</SidebarMenuItem>
53+
))}
54+
</SidebarMenu>
55+
</SidebarGroupContent>
56+
</SidebarGroup>
57+
</SidebarContent>
58+
<SidebarFooter>
59+
<SidebarMenu>
60+
<SidebarMenuItem>
61+
<SidebarMenuButton
62+
isActive={path === '/settings'}
63+
onClick={() => navigate('/settings')}
64+
>
65+
<Settings />
66+
<span>Settings</span>
67+
</SidebarMenuButton>
68+
</SidebarMenuItem>
69+
</SidebarMenu>
70+
</SidebarFooter>
71+
<SidebarRail />
72+
</Sidebar>
73+
)
74+
}

packages/client/src/components/dashboard/ObjectDetailView.tsx

Lines changed: 47 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { useState, useEffect } from 'react';
2-
import { Button, Modal, Spinner, AutoForm } from '@objectql/ui';
2+
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, Spinner } from '@objectql/ui';
33
import { getHeaders } from '../../lib/api';
4+
import { ChevronLeft, Pencil, Trash } from 'lucide-react';
5+
import { ObjectForm } from './ObjectForm';
46

57
interface ObjectDetailViewProps {
68
objectName: string;
@@ -59,77 +61,76 @@ export function ObjectDetailView({ objectName, recordId, navigate, objectSchema
5961
};
6062

6163
if (loading) return (
62-
<div className="flex flex-col h-full bg-white rounded-xl border border-gray-200/60 shadow-sm overflow-hidden p-8 items-center justify-center">
63-
<Spinner className="w-6 h-6 text-gray-400" />
64+
<div className="flex flex-col h-full bg-background rounded-xl border shadow-sm overflow-hidden p-8 items-center justify-center">
65+
<Spinner className="w-6 h-6" />
6466
</div>
6567
);
6668

6769
if (!data) return <div>Record not found</div>;
6870

6971
return (
70-
<div className="flex flex-col h-full bg-white rounded-xl border border-gray-200/60 shadow-sm overflow-hidden animate-[fadeIn_0.3s_ease-out]">
72+
<div className="flex flex-col h-full bg-background rounded-xl border shadow-sm overflow-hidden">
7173
{/* Header */}
72-
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-white">
74+
<div className="px-6 py-4 border-b flex justify-between items-center bg-card">
7375
<div className="flex items-center gap-4">
74-
<button onClick={() => navigate(`/object/${objectName}`)} className="p-2 -ml-2 hover:bg-gray-100 rounded-full transition-colors text-gray-500">
75-
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg>
76-
</button>
76+
<Button variant="ghost" size="icon" onClick={() => navigate(`/object/${objectName}`)} className="rounded-full">
77+
<ChevronLeft className="w-5 h-5"/>
78+
</Button>
7779
<div>
78-
<div className="flex items-center gap-2 text-xs text-gray-500 uppercase font-medium tracking-wider mb-0.5">
80+
<div className="flex items-center gap-2 text-xs text-muted-foreground uppercase font-medium tracking-wider mb-0.5">
7981
{label}
80-
<span className="text-gray-300">/</span>
81-
<span className="text-gray-400">{recordId}</span>
82+
<span className="text-muted-foreground/30">/</span>
83+
<span className="text-muted-foreground">{recordId}</span>
8284
</div>
83-
<h1 className="text-xl font-bold text-gray-900">{data.name || data.title || recordId}</h1>
85+
<h1 className="text-xl font-bold text-foreground">{data.name || data.title || recordId}</h1>
8486
</div>
8587
</div>
8688

8789
<div className="flex gap-2">
88-
<Button variant="secondary" onClick={() => setIsEditing(true)} className="gap-2">
89-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
90+
<Button variant="outline" onClick={() => setIsEditing(true)} className="gap-2">
91+
<Pencil className="w-4 h-4" />
9092
Edit
9193
</Button>
92-
<Button variant="secondary" onClick={handleDelete} className="hover:bg-red-50 hover:text-red-600 gap-2 border-transparent">
93-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
94+
<Button variant="outline" onClick={handleDelete} className="hover:bg-destructive/10 hover:text-destructive gap-2 border-transparent text-destructive">
95+
<Trash className="w-4 h-4" />
9496
Delete
9597
</Button>
9698
</div>
9799
</div>
98100

99101
{/* Content */}
100-
<div className="flex-1 overflow-auto bg-gray-50/50 p-6">
101-
<div className="bg-white rounded-xl border border-gray-200 shadow-sm p-6 max-w-4xl mx-auto">
102-
{schema ? (
103-
<AutoForm
104-
schema={schema}
105-
initialValues={data}
106-
readonly={true}
107-
onSubmit={() => {}}
108-
/>
109-
) : (
110-
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-8">
111-
{Object.entries(data).map(([key, value]) => (
112-
<div key={key} className="space-y-1.5 border-b border-gray-50 pb-2">
113-
<div className="text-xs font-medium text-gray-400 uppercase tracking-wide">{key}</div>
114-
<div className="text-sm text-gray-900 font-medium break-words">
115-
{typeof value === 'object' ? JSON.stringify(value) : String(value ?? '-')}
116-
</div>
117-
</div>
118-
))}
119-
</div>
120-
)}
102+
<div className="flex-1 overflow-auto bg-muted/20 p-6">
103+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl">
104+
{Object.entries(data).map(([key, value]) => {
105+
if (['id', '_id', '__v'].includes(key)) return null;
106+
const fieldLabel = schema?.fields?.[key]?.label || key;
107+
108+
return (
109+
<div key={key} className="space-y-1">
110+
<div className="text-sm font-medium text-muted-foreground capitalize">{fieldLabel}</div>
111+
<div className="text-base font-medium break-words">
112+
{typeof value === 'object' ? JSON.stringify(value) : String(value)}
113+
</div>
114+
</div>
115+
)
116+
})}
121117
</div>
122118
</div>
123119

124-
{/* Edit Modal */}
125-
<Modal isOpen={isEditing} onClose={() => setIsEditing(false)} title={`Edit ${label}`}>
126-
<AutoForm
127-
schema={schema}
128-
initialValues={data}
129-
onSubmit={handleUpdate}
130-
onCancel={() => setIsEditing(false)}
131-
/>
132-
</Modal>
120+
<Dialog open={isEditing} onOpenChange={setIsEditing}>
121+
<DialogContent>
122+
<DialogHeader>
123+
<DialogTitle>Edit {label}</DialogTitle>
124+
</DialogHeader>
125+
<ObjectForm
126+
objectName={objectName}
127+
initialValues={data}
128+
headers={getHeaders()}
129+
onSubmit={handleUpdate}
130+
onCancel={() => setIsEditing(false)}
131+
/>
132+
</DialogContent>
133+
</Dialog>
133134
</div>
134135
);
135136
}
Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useState, useEffect } from 'react';
2-
import { AutoForm, Spinner } from '@objectql/ui';
2+
import { useForm } from 'react-hook-form';
3+
import { Button, Input, Label, Spinner } from '@objectql/ui';
34

45
interface ObjectFormProps {
56
objectName: string;
@@ -10,23 +11,47 @@ interface ObjectFormProps {
1011
}
1112

1213
export function ObjectForm({ objectName, initialValues, onSubmit, onCancel, headers }: ObjectFormProps) {
13-
const [schema, setSchema] = useState(null);
14-
14+
const [schema, setSchema] = useState<any>(null);
15+
const { register, handleSubmit, reset } = useForm({
16+
defaultValues: initialValues || {}
17+
});
18+
1519
useEffect(() => {
1620
fetch(`/api/object/_schema/object/${objectName}`, { headers })
1721
.then(res => res.json())
18-
.then(setSchema)
22+
.then(res => {
23+
setSchema(res);
24+
if (initialValues) {
25+
reset(initialValues);
26+
}
27+
})
1928
.catch(console.error);
20-
}, [objectName, headers]);
29+
}, [objectName, headers, initialValues, reset]);
30+
31+
if (!schema) return <div className="p-8 flex justify-center"><Spinner className="w-6 h-6" /></div>;
2132

22-
if (!schema) return <div className="p-8 flex justify-center"><Spinner className="w-6 h-6 text-gray-400" /></div>;
33+
const fields = Object.entries(schema.fields || {});
2334

2435
return (
25-
<AutoForm
26-
schema={schema}
27-
initialValues={initialValues}
28-
onSubmit={onSubmit}
29-
onCancel={onCancel}
30-
/>
36+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
37+
{fields.map(([key, field]: [string, any]) => {
38+
if (['id', '_id', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'].includes(key)) return null;
39+
40+
return (
41+
<div key={key} className="space-y-2">
42+
<Label htmlFor={key}>{field.label || field.title || key}</Label>
43+
<Input
44+
id={key}
45+
{...register(key, { required: !field.optional })}
46+
type={field.type === 'number' || field.type === 'integer' || field.type === 'float' ? 'number' : field.type === 'password' ? 'password' : 'text'}
47+
/>
48+
</div>
49+
);
50+
})}
51+
<div className="flex justify-end gap-2 pt-4">
52+
<Button type="button" variant="outline" onClick={onCancel}>Cancel</Button>
53+
<Button type="submit">Save</Button>
54+
</div>
55+
</form>
3156
);
3257
}

0 commit comments

Comments
 (0)