1- import { useState , useEffect , useMemo } from 'react' ;
1+ import { useState , useEffect , useCallback , useMemo } from 'react' ;
22import { 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' ;
66import { MetadataToggle , MetadataPanel , useMetadataInspector } from './MetadataInspector' ;
7+ import { DesignDrawer } from './DesignDrawer' ;
78import { useMetadata } from '../context/MetadataProvider' ;
89import type { DataSource } from '@object-ui/types' ;
910
1011// Fallback fields when no schema is available
1112const 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
2223export 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