Skip to content

Commit 7355fa6

Browse files
Copilothotlong
andcommitted
feat: add ViewDesigner component and console integration for creating/editing views
- Add ViewDesignerSchema and ViewDesignerColumn types to @object-ui/types - Create ViewDesigner component in @object-ui/plugin-designer with 3-panel layout - Add ViewDesignerPage to console app with routes for new/edit views - Add Edit View and New View buttons to console ObjectView header Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 83423fc commit 7355fa6

9 files changed

Lines changed: 1010 additions & 1 deletion

File tree

apps/console/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"@object-ui/plugin-charts": "workspace:*",
3939
"@object-ui/plugin-dashboard": "workspace:*",
4040
"@object-ui/plugin-detail": "workspace:*",
41+
"@object-ui/plugin-designer": "workspace:*",
4142
"@object-ui/plugin-form": "workspace:*",
4243
"@object-ui/plugin-gantt": "workspace:*",
4344
"@object-ui/plugin-grid": "workspace:*",

apps/console/src/App.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { RecordDetailView } from './components/RecordDetailView';
1919
import { DashboardView } from './components/DashboardView';
2020
import { PageView } from './components/PageView';
2121
import { ReportView } from './components/ReportView';
22+
import { ViewDesignerPage } from './components/ViewDesignerPage';
2223
import { ExpressionProvider } from './context/ExpressionProvider';
2324
import { ConditionalAuthWrapper } from './components/ConditionalAuthWrapper';
2425
import { KeyboardShortcutsDialog } from './components/KeyboardShortcutsDialog';
@@ -246,6 +247,14 @@ export function AppContent() {
246247
<RecordDetailView key={refreshKey} dataSource={dataSource} objects={allObjects} onEdit={handleEdit} />
247248
} />
248249

250+
{/* View Designer - Create/Edit Views */}
251+
<Route path=":objectName/views/new" element={
252+
<ViewDesignerPage objects={allObjects} />
253+
} />
254+
<Route path=":objectName/views/:viewId" element={
255+
<ViewDesignerPage objects={allObjects} />
256+
} />
257+
249258
<Route path="dashboard/:dashboardName" element={
250259
<DashboardView dataSource={dataSource} />
251260
} />

apps/console/src/components/ObjectView.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import '@object-ui/plugin-grid';
2020
import '@object-ui/plugin-kanban';
2121
import '@object-ui/plugin-calendar';
2222
import { Button, Empty, EmptyTitle, EmptyDescription, Sheet, SheetContent } from '@object-ui/components';
23-
import { Plus, Table as TableIcon } from 'lucide-react';
23+
import { Plus, Table as TableIcon, Settings2 } from 'lucide-react';
2424
import type { ListViewSchema } from '@object-ui/types';
2525
import { MetadataToggle, MetadataPanel, useMetadataInspector } from './MetadataInspector';
2626
import { useObjectActions } from '../hooks/useObjectActions';
@@ -226,6 +226,26 @@ export function ObjectView({ dataSource, objects, onEdit, onRowClick }: any) {
226226

227227
<div className="flex items-center gap-1.5 sm:gap-2 shrink-0">
228228
<MetadataToggle open={showDebug} onToggle={toggleDebug} className="hidden sm:flex" />
229+
<Button
230+
size="sm"
231+
variant="outline"
232+
onClick={() => navigate(viewId ? `../../views/${viewId}` : `views/${activeViewId}`)}
233+
className="shadow-none gap-1.5 h-8 sm:h-9 hidden sm:flex"
234+
title="Edit current view layout"
235+
>
236+
<Settings2 className="h-4 w-4" />
237+
<span className="hidden lg:inline">Edit View</span>
238+
</Button>
239+
<Button
240+
size="sm"
241+
variant="outline"
242+
onClick={() => navigate(viewId ? '../../views/new' : 'views/new')}
243+
className="shadow-none gap-1.5 h-8 sm:h-9 hidden sm:flex"
244+
title="Create a new view"
245+
>
246+
<Plus className="h-4 w-4" />
247+
<span className="hidden lg:inline">New View</span>
248+
</Button>
229249
<Button size="sm" onClick={actions.create} className="shadow-none gap-1.5 sm:gap-2 h-8 sm:h-9">
230250
<Plus className="h-4 w-4" />
231251
<span className="hidden sm:inline">New</span>
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/**
2+
* ViewDesignerPage
3+
*
4+
* Console page that wraps the ViewDesigner component for creating
5+
* and editing list views for a given object.
6+
*
7+
* Routes:
8+
* - /:objectName/views/new → Create new view
9+
* - /:objectName/views/:viewId → Edit existing view
10+
*/
11+
12+
import { useMemo, useCallback } from 'react';
13+
import { useParams, useNavigate } from 'react-router-dom';
14+
import { ViewDesigner } from '@object-ui/plugin-designer';
15+
import type { ViewDesignerConfig } from '@object-ui/plugin-designer';
16+
import { toast } from 'sonner';
17+
18+
export function ViewDesignerPage({ objects }: { objects: any[] }) {
19+
const navigate = useNavigate();
20+
const { objectName, viewId } = useParams();
21+
22+
const objectDef = objects.find((o: any) => o.name === objectName);
23+
24+
// Build available fields from object definition
25+
const availableFields = useMemo(() => {
26+
if (!objectDef?.fields) return [];
27+
const fields = objectDef.fields;
28+
if (Array.isArray(fields)) {
29+
return fields.map((f: any) =>
30+
typeof f === 'string'
31+
? { name: f, label: f, type: 'text' }
32+
: { name: f.name, label: f.label || f.name, type: f.type || 'text' },
33+
);
34+
}
35+
return Object.entries(fields).map(([name, def]: [string, any]) => ({
36+
name,
37+
label: def.label || name,
38+
type: def.type || 'text',
39+
}));
40+
}, [objectDef]);
41+
42+
// Resolve existing view for editing
43+
const existingView = useMemo(() => {
44+
if (!viewId || viewId === 'new' || !objectDef?.list_views) return null;
45+
return objectDef.list_views[viewId] || null;
46+
}, [viewId, objectDef]);
47+
48+
const handleSave = useCallback(
49+
(config: ViewDesignerConfig) => {
50+
// In a real implementation this would persist the view config.
51+
// For now, log and show toast.
52+
console.log('[ViewDesigner] Save view config:', config);
53+
toast.success(
54+
existingView
55+
? `View "${config.viewLabel}" updated`
56+
: `View "${config.viewLabel}" created`,
57+
);
58+
navigate(-1);
59+
},
60+
[existingView, navigate],
61+
);
62+
63+
const handleCancel = useCallback(() => {
64+
navigate(-1);
65+
}, [navigate]);
66+
67+
if (!objectDef) {
68+
return (
69+
<div className="h-full flex items-center justify-center text-muted-foreground">
70+
Object &quot;{objectName}&quot; not found
71+
</div>
72+
);
73+
}
74+
75+
return (
76+
<div className="h-full flex flex-col">
77+
<ViewDesigner
78+
objectName={objectDef.name}
79+
viewId={viewId === 'new' ? undefined : viewId}
80+
viewLabel={existingView?.label ?? ''}
81+
viewType={existingView?.type ?? 'grid'}
82+
columns={
83+
existingView?.columns
84+
? existingView.columns.map((c: string, i: number) => ({
85+
field: c,
86+
label: availableFields.find((f: any) => f.name === c)?.label ?? c,
87+
visible: true,
88+
order: i,
89+
}))
90+
: []
91+
}
92+
filters={existingView?.filter ?? []}
93+
sort={
94+
existingView?.sort?.map((s: any) => ({
95+
field: s.field,
96+
direction: s.order || s.direction || 'asc',
97+
})) ?? []
98+
}
99+
availableFields={availableFields}
100+
options={existingView?.options ?? {}}
101+
onSave={handleSave}
102+
onCancel={handleCancel}
103+
className="flex-1"
104+
/>
105+
</div>
106+
);
107+
}

0 commit comments

Comments
 (0)