Skip to content

Commit 992154f

Browse files
authored
Merge pull request #813 from objectstack-ai/copilot/refactor-report-design-pattern
2 parents 5b74901 + a14d4a3 commit 992154f

File tree

5 files changed

+618
-64
lines changed

5 files changed

+618
-64
lines changed

ROADMAP.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,8 @@ The `FlowDesigner` is a canvas-based flow editor that bridges the gap between th
676676
- [ ] ProcessDesigner: Node drag-to-move
677677
- [x] ProcessDesigner/FlowDesigner: Support v3.0.9 node types (`parallel_gateway`, `join_gateway`, `boundary_event`) — implemented in FlowDesigner
678678
- [x] ProcessDesigner/FlowDesigner: Support v3.0.9 conditional edges and default edge marking — implemented in FlowDesigner
679+
- [x] ReportView: Refactored to left-right split layout (preview + DesignDrawer) — consistent with DashboardView/ListView
680+
- [x] ReportConfigPanel: Schema-driven config via ConfigPanelRenderer + useConfigDraft (replaces full-page ReportBuilder in edit mode)
679681
- [ ] ReportDesigner: Element drag-to-reposition within sections
680682
- [x] FlowDesigner: Edge creation UI (click source port → click target port)
681683
- [x] FlowDesigner: Property editing for node labels/types + executor config + boundary config

apps/console/src/components/ReportView.tsx

Lines changed: 76 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,43 @@
1-
import { useState, useEffect, useMemo } from 'react';
1+
import { useState, useEffect, useCallback, useMemo } from 'react';
22
import { useParams } from 'react-router-dom';
3-
import { ReportViewer, ReportBuilder } from '@object-ui/plugin-report';
4-
import { Empty, EmptyTitle, EmptyDescription, Button } from '@object-ui/components';
5-
import { PenLine, ChevronLeft, BarChart3, Loader2 } from 'lucide-react';
3+
import { ReportViewer, ReportConfigPanel } from '@object-ui/plugin-report';
4+
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
5+
import { Pencil, BarChart3, Loader2 } from 'lucide-react';
66
import { MetadataToggle, MetadataPanel, useMetadataInspector } from './MetadataInspector';
7+
import { DesignDrawer } from './DesignDrawer';
78
import { useMetadata } from '../context/MetadataProvider';
89
import type { DataSource } from '@object-ui/types';
910

1011
// Fallback fields when no schema is available
1112
const FALLBACK_FIELDS = [
12-
{ name: 'month', label: 'Month', type: 'string' },
13-
{ name: 'revenue', label: 'Revenue', type: 'number' },
14-
{ name: 'count', label: 'Count', type: 'number' },
15-
{ name: 'region', label: 'Region', type: 'string' },
16-
{ name: 'product', label: 'Product', type: 'string' },
17-
{ name: 'source', label: 'Lead Source', type: 'string' },
18-
{ name: 'stage', label: 'Stage', type: 'string' },
19-
{ name: 'amount', label: 'Amount', type: 'currency' },
13+
{ value: 'month', label: 'Month', type: 'text' },
14+
{ value: 'revenue', label: 'Revenue', type: 'number' },
15+
{ value: 'count', label: 'Count', type: 'number' },
16+
{ value: 'region', label: 'Region', type: 'text' },
17+
{ value: 'product', label: 'Product', type: 'text' },
18+
{ value: 'source', label: 'Lead Source', type: 'text' },
19+
{ value: 'stage', label: 'Stage', type: 'text' },
20+
{ value: 'amount', label: 'Amount', type: 'number' },
2021
];
2122

2223
export function ReportView({ dataSource }: { dataSource?: DataSource }) {
2324
const { reportName } = useParams<{ reportName: string }>();
2425
const { showDebug, toggleDebug } = useMetadataInspector();
25-
const [isEditing, setIsEditing] = useState(false);
26+
const [drawerOpen, setDrawerOpen] = useState(false);
2627

2728
// Find report definition from API-driven metadata
2829
const { reports, objects, loading } = useMetadata();
2930
const initialReport = reports?.find((r: any) => r.name === reportName);
3031
const [reportData, setReportData] = useState(initialReport);
3132

33+
// Local schema state for live preview — initialized from metadata
34+
const [editSchema, setEditSchema] = useState<any>(null);
35+
3236
// State for report runtime data
3337
const [reportRuntimeData, setReportRuntimeData] = useState<any[]>([]);
3438
const [dataLoading, setDataLoading] = useState(false);
3539

36-
// Derive available fields from object schema when report has objectName/dataSource
40+
// Derive available fields from object schema for filter/sort editors
3741
const availableFields = useMemo(() => {
3842
const objName = reportData?.objectName || reportData?.dataSource?.object || reportData?.dataSource?.resource;
3943
if (objName && objects?.length) {
@@ -43,12 +47,12 @@ export function ReportView({ dataSource }: { dataSource?: DataSource }) {
4347
if (Array.isArray(fields)) {
4448
return fields.map((f: any) =>
4549
typeof f === 'string'
46-
? { name: f, label: f, type: 'text' }
47-
: { name: f.name, label: f.label || f.name, type: f.type || 'text' },
50+
? { value: f, label: f, type: 'text' }
51+
: { value: f.name, label: f.label || f.name, type: f.type || 'text' },
4852
);
4953
}
5054
return Object.entries(fields).map(([name, def]: [string, any]) => ({
51-
name,
55+
value: name,
5256
label: def.label || name,
5357
type: def.type || 'text',
5458
}));
@@ -57,6 +61,15 @@ export function ReportView({ dataSource }: { dataSource?: DataSource }) {
5761
return FALLBACK_FIELDS;
5862
}, [reportData, objects]);
5963

64+
const handleOpenDrawer = useCallback(() => {
65+
setEditSchema(reportData);
66+
setDrawerOpen(true);
67+
}, [reportData]);
68+
69+
const handleCloseDrawer = useCallback((open: boolean) => {
70+
setDrawerOpen(open);
71+
}, []);
72+
6073
// Sync reportData when metadata finishes loading or reportName changes
6174
useEffect(() => {
6275
setReportData(initialReport);
@@ -167,46 +180,14 @@ export function ReportView({ dataSource }: { dataSource?: DataSource }) {
167180
);
168181
}
169182

170-
const handleSave = (newReport: any) => {
171-
console.log('Saving report:', newReport);
172-
setReportData(newReport);
173-
setIsEditing(false);
174-
};
175-
176-
if (isEditing) {
177-
return (
178-
<div className="flex flex-col h-full overflow-hidden bg-background">
179-
<div className="flex items-center p-3 sm:p-4 border-b bg-muted/10 gap-2">
180-
<Button variant="ghost" size="sm" onClick={() => setIsEditing(false)} className="shrink-0">
181-
<ChevronLeft className="h-4 w-4 mr-1" />
182-
<span className="hidden sm:inline">Back to View</span>
183-
<span className="sm:hidden">Back</span>
184-
</Button>
185-
<div className="font-medium truncate">Edit Report: {reportData.title || reportData.label}</div>
186-
</div>
187-
<div className="flex-1 overflow-auto">
188-
<ReportBuilder
189-
schema={{
190-
title: 'Report Builder',
191-
report: reportData,
192-
availableFields: availableFields,
193-
onSave: handleSave,
194-
onCancel: () => setIsEditing(false)
195-
}}
196-
/>
197-
</div>
198-
</div>
199-
);
200-
}
201-
202183
// Wrap the report definition in the ReportViewer schema
203184
// The ReportViewer expects a schema property which is of type ReportViewerSchema
204185
// That schema has a 'report' property which is the actual report definition (ReportSchema)
205186
// Map @objectstack/spec report format to @object-ui/types ReportSchema:
206187
// - 'label' → 'title'
207188
// - 'columns' (with 'field') → 'fields' (with 'name') + auto-generate 'sections'
208-
const reportForViewer = (() => {
209-
const mapped: any = { ...reportData };
189+
const mapReportForViewer = (src: any) => {
190+
const mapped: any = { ...src };
210191
if (!mapped.title && mapped.label) {
211192
mapped.title = mapped.label;
212193
}
@@ -242,7 +223,11 @@ export function ReportView({ dataSource }: { dataSource?: DataSource }) {
242223
];
243224
}
244225
return mapped;
245-
})();
226+
};
227+
228+
// Use live-edited schema for preview when the drawer is open
229+
const previewReport = drawerOpen && editSchema ? editSchema : reportData;
230+
const reportForViewer = mapReportForViewer(previewReport);
246231
const viewerSchema = {
247232
type: 'report-viewer',
248233
report: reportForViewer, // The report definition
@@ -254,21 +239,28 @@ export function ReportView({ dataSource }: { dataSource?: DataSource }) {
254239

255240
return (
256241
<div className="flex flex-col h-full overflow-hidden bg-background">
257-
<div className="flex flex-col sm:flex-row justify-between sm:items-center gap-3 sm:gap-4 p-4 sm:p-6 border-b shrink-0 bg-muted/10">
258-
<div className="min-w-0">
259-
{/* Header is handled by ReportViewer usually, but we can have a page header too */}
260-
<h1 className="text-base sm:text-lg font-medium text-muted-foreground truncate">{reportData.title || reportData.label || 'Report Viewer'}</h1>
242+
<div className="flex flex-col sm:flex-row justify-between sm:items-center gap-3 sm:gap-4 p-4 sm:p-6 border-b shrink-0">
243+
<div className="min-w-0 flex-1">
244+
<h1 className="text-lg sm:text-xl md:text-2xl font-bold tracking-tight truncate">{reportData.title || reportData.label || 'Report Viewer'}</h1>
245+
{reportData.description && (
246+
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">{reportData.description}</p>
247+
)}
261248
</div>
262-
<div className="flex items-center gap-2 shrink-0">
263-
<Button variant="outline" size="sm" onClick={() => setIsEditing(true)} className="h-8">
264-
<PenLine className="h-4 w-4 sm:mr-2" />
265-
<span className="hidden sm:inline">Edit Report</span>
266-
</Button>
249+
<div className="shrink-0 flex items-center gap-1.5">
250+
<button
251+
type="button"
252+
onClick={handleOpenDrawer}
253+
className="inline-flex items-center gap-1.5 rounded-md border border-input bg-background px-2.5 py-1.5 text-xs font-medium text-muted-foreground shadow-sm hover:bg-accent hover:text-accent-foreground"
254+
data-testid="report-edit-button"
255+
>
256+
<Pencil className="h-3.5 w-3.5" />
257+
Edit
258+
</button>
267259
<MetadataToggle open={showDebug} onToggle={toggleDebug} />
268260
</div>
269261
</div>
270262

271-
<div className="flex-1 overflow-hidden flex flex-row relative">
263+
<div className="flex-1 overflow-hidden flex flex-col sm:flex-row relative">
272264
<div className="flex-1 overflow-auto p-4 sm:p-6 lg:p-8 bg-muted/5">
273265
<div className="max-w-5xl mx-auto shadow-sm border rounded-lg sm:rounded-xl bg-background overflow-hidden min-h-150">
274266
<ReportViewer schema={viewerSchema} />
@@ -277,9 +269,30 @@ export function ReportView({ dataSource }: { dataSource?: DataSource }) {
277269

278270
<MetadataPanel
279271
open={showDebug}
280-
sections={[{ title: 'Report Configuration', data: reportData }]}
272+
sections={[{ title: 'Report Configuration', data: previewReport }]}
281273
/>
282274
</div>
275+
276+
<DesignDrawer
277+
open={drawerOpen}
278+
onOpenChange={handleCloseDrawer}
279+
title={`Edit Report: ${reportData.title || reportData.label || reportName}`}
280+
schema={editSchema || reportData}
281+
onSchemaChange={setEditSchema}
282+
collection="sys_report"
283+
recordName={reportName!}
284+
>
285+
{(schema, onChange) => (
286+
<ReportConfigPanel
287+
open={true}
288+
onClose={() => setDrawerOpen(false)}
289+
config={schema}
290+
onSave={(updated) => onChange(updated)}
291+
onFieldChange={(field, value) => onChange({ ...schema, [field]: value })}
292+
availableFields={availableFields}
293+
/>
294+
)}
295+
</DesignDrawer>
283296
</div>
284297
);
285298
}

0 commit comments

Comments
 (0)