11import { useState , useEffect , useCallback , useMemo , lazy , Suspense } from 'react' ;
22import { useParams } from 'react-router-dom' ;
3- const ReportViewer = lazy ( ( ) =>
4- import ( '@object-ui/plugin-report' ) . then ( ( m ) => ( { default : m . ReportViewer } ) ) ,
5- ) ;
63const ReportRenderer = lazy ( ( ) =>
74 import ( '@object-ui/plugin-report' ) . then ( ( m ) => ( { default : m . ReportRenderer } ) ) ,
85) ;
@@ -20,7 +17,7 @@ import { useAdapter } from '../providers/AdapterProvider';
2017import { useMetadataClient } from './metadata-admin/useMetadata' ;
2118import { persistRuntimeMetadata } from './runtime-metadata-persistence' ;
2219import { useIsWorkspaceAdmin } from '@object-ui/auth' ;
23- import type { DataSource , ReportViewerSchema } from '@object-ui/types' ;
20+ import type { DataSource } from '@object-ui/types' ;
2421import type { DatasetDrillArgs } from '@object-ui/plugin-report' ;
2522import { DrillDownDrawer } from '@object-ui/plugin-dashboard' ;
2623import { DrillNavigationProvider } from '@object-ui/react' ;
@@ -64,7 +61,6 @@ export function ReportView({ dataSource }: { dataSource?: DataSource }) {
6461
6562 // State for report runtime data
6663 const [ reportRuntimeData , setReportRuntimeData ] = useState < any [ ] > ( [ ] ) ;
67- const [ dataLoading , setDataLoading ] = useState ( false ) ;
6864
6965 // Drill-through (ADR-0021 D2): clicking an aggregated row/cell opens the
7066 // underlying records in an in-place drawer (peek without leaving the report),
@@ -272,7 +268,6 @@ export function ReportView({ dataSource }: { dataSource?: DataSource }) {
272268 // If report has a dataSource config, fetch data using it
273269 if ( dataFetchSource . dataSource ) {
274270 const fetchDataFromSource = async ( ) => {
275- setDataLoading ( true ) ;
276271 try {
277272 // Use the dataSource configuration to fetch data
278273 const resource = dataFetchSource . dataSource . object || dataFetchSource . dataSource . resource ;
@@ -292,8 +287,6 @@ export function ReportView({ dataSource }: { dataSource?: DataSource }) {
292287 } catch ( error ) {
293288 console . error ( 'ReportView: Failed to load data from dataSource' , error ) ;
294289 setReportRuntimeData ( [ ] ) ;
295- } finally {
296- setDataLoading ( false ) ;
297290 }
298291 } ;
299292
@@ -304,7 +297,6 @@ export function ReportView({ dataSource }: { dataSource?: DataSource }) {
304297 // If report has an objectName, fetch data from that object
305298 if ( dataFetchSource . objectName ) {
306299 const fetchDataFromObject = async ( ) => {
307- setDataLoading ( true ) ;
308300 try {
309301 const result = await dataSource . find ( dataFetchSource . objectName , {
310302 $filter : dataFetchSource . filters ,
@@ -316,8 +308,6 @@ export function ReportView({ dataSource }: { dataSource?: DataSource }) {
316308 } catch ( error ) {
317309 console . error ( 'ReportView: Failed to load data from objectName' , error ) ;
318310 setReportRuntimeData ( [ ] ) ;
319- } finally {
320- setDataLoading ( false ) ;
321311 }
322312 } ;
323313
@@ -368,172 +358,14 @@ export function ReportView({ dataSource }: { dataSource?: DataSource }) {
368358 ) ;
369359 }
370360
371- // Wrap the report definition in the ReportViewer schema
372- // The ReportViewer expects a schema property which is of type ReportViewerSchema
373- // That schema has a 'report' property which is the actual report definition (ReportSchema)
374- // Map @objectstack /spec report format to @object -ui/types ReportSchema:
375- // - 'label' → 'title'
376- // - 'columns' (with 'field') → 'fields' (with 'name') + auto-generate 'sections'
377- // - Hydrate type/options/referenceTo from the bound object's field metadata
378- // so the type-aware cell renderer can show select badges, lookup links,
379- // boolean ✓/✗, email/url/phone links, etc. instead of raw values.
380- const mapReportForViewer = ( src : any ) => {
381- const mapped : any = { ...src } ;
382- if ( ! mapped . title && mapped . label ) {
383- mapped . title = mapped . label ;
384- }
385-
386- // Build a lookup of object-field metadata to hydrate column type info.
387- const objName = mapped . objectName || mapped . dataSource ?. object || mapped . dataSource ?. resource ;
388- const objDef = objName ? objects ?. find ( ( o : any ) => o . name === objName ) : null ;
389- const objFieldsArr : any [ ] = Array . isArray ( objDef ?. fields )
390- ? objDef . fields
391- : objDef ?. fields
392- ? Object . entries ( objDef . fields ) . map ( ( [ name , def ] : [ string , any ] ) => ( { name, ...def } ) )
393- : [ ] ;
394- const objFieldMap : Record < string , any > = { } ;
395- for ( const f of objFieldsArr ) {
396- if ( f && f . name ) objFieldMap [ f . name ] = f ;
397- }
398-
399- const hydrate = ( col : any ) : any => {
400- const name = col . name || col . field ;
401- const meta = name ? objFieldMap [ name ] : undefined ;
402- if ( ! meta ) return col ;
403- // Author-provided values win; only fill in what's missing.
404- const out = { ...col } ;
405- if ( out . type === undefined && meta . type !== undefined ) out . type = meta . type ;
406- if ( out . options === undefined && Array . isArray ( meta . options ) ) out . options = meta . options ;
407- if ( out . referenceTo === undefined ) {
408- // Metadata-store object defs key the lookup target as `reference`
409- // (string, ObjectStack convention); `reference_to` covers normalized /
410- // ObjectUI-authored defs (#2407 / PR #2587).
411- const ref =
412- meta . reference_to ||
413- meta . referenceTo ||
414- ( typeof meta . reference === 'string' ? meta . reference : meta . reference ?. to ) ||
415- meta . target ;
416- if ( ref ) out . referenceTo = ref ;
417- }
418- if ( out . label === undefined && meta . label ) out . label = meta . label ;
419- return out ;
420- } ;
421-
422- // Map spec 'columns' (field/label/aggregate) → ReportSchema 'fields' (name/label/aggregation)
423- if ( ! mapped . fields && Array . isArray ( mapped . columns ) ) {
424- mapped . fields = mapped . columns . map ( ( col : any ) => {
425- const hydrated = hydrate ( col ) ;
426- return {
427- name : hydrated . field || hydrated . name ,
428- label : hydrated . label ,
429- type : hydrated . type ,
430- options : hydrated . options ,
431- referenceTo : hydrated . referenceTo ,
432- format : hydrated . format ,
433- renderAs : hydrated . renderAs ,
434- colorMap : hydrated . colorMap ,
435- ...( hydrated . aggregate ? { aggregation : hydrated . aggregate , showInSummary : true } : { } ) ,
436- } ;
437- } ) ;
438- } else if ( Array . isArray ( mapped . fields ) ) {
439- mapped . fields = mapped . fields . map ( hydrate ) ;
440- }
441- // Always regenerate sections from current fields so that live config
442- // changes (e.g. field picker updates) are immediately reflected in
443- // the preview. This fixes the linkage bug where config panel edits
444- // did not update the rendered report.
445- if ( mapped . fields && Array . isArray ( mapped . fields ) && mapped . fields . length > 0 ) {
446- const hasSummaryFields = mapped . fields . some ( ( f : any ) => f . showInSummary || f . aggregation ) ;
447- // Spec key is `type`; legacy renderer used `reportType`. Accept either.
448- const reportType = mapped . type || mapped . reportType || 'tabular' ;
449- const sections : any [ ] = [ ] ;
450- if ( reportType === 'summary' || hasSummaryFields ) {
451- sections . push ( { type : 'summary' , title : 'Key Metrics' } ) ;
452- }
453- sections . push ( {
454- type : 'table' ,
455- title : 'Details' ,
456- columns : mapped . fields . map ( ( f : any ) => ( {
457- name : f . name ,
458- label : f . label ,
459- type : f . type ,
460- options : f . options ,
461- referenceTo : f . referenceTo ,
462- format : f . format ,
463- renderAs : f . renderAs ,
464- colorMap : f . colorMap ,
465- } ) ) ,
466- } ) ;
467- // Generate chart section from chart config if configured.
468- // Spec keys: type / xAxis / yAxis. Legacy: chartType / xAxisField / yAxisFields[0].
469- const chartCfg = mapped . chart || mapped . chartConfig ;
470- const chartTypeVal = chartCfg ?. type || chartCfg ?. chartType ;
471- if ( chartTypeVal ) {
472- const xField = chartCfg . xAxis || chartCfg . xAxisField ;
473- const yField = chartCfg . yAxis || chartCfg . yAxisFields ?. [ 0 ] ;
474- sections . push ( {
475- type : 'chart' ,
476- title : 'Chart' ,
477- chart : {
478- type : 'chart' ,
479- chartType : chartTypeVal ,
480- xAxisField : xField ,
481- yAxisFields : yField ? [ yField ] : chartCfg . yAxisFields ,
482- } ,
483- } ) ;
484- }
485- // Preserve any user-defined chart sections from the original schema
486- if ( Array . isArray ( src . sections ) ) {
487- const chartSections = src . sections . filter ( ( s : any ) => s . type === 'chart' && ! chartTypeVal ) ;
488- sections . push ( ...chartSections ) ;
489- }
490- mapped . sections = sections ;
491- } else if ( ! mapped . sections ) {
492- // No fields and no sections — leave empty
493- mapped . sections = [ ] ;
494- }
495- return mapped ;
496- } ;
497-
498361 // Use live-edited schema for preview (persists after closing panel until metadata refreshes)
499362 const previewReport = editSchema || reportData ;
500- // Route any object-backed spec report (matrix/joined/tabular/summary) through
501- // the spec ReportRenderer dispatcher. It handles aggregation, charts, KPIs
502- // and drill protocol end-to-end. The legacy ReportViewer is only used as a
503- // last resort for fully-legacy schemas that lack `objectName` (e.g. inline
504- // `fields` + `data` arrays from older app code).
505- // ADR-0021 single-form: a report bound to a semantic-layer `dataset` (no
506- // `objectName`/`columns`) still routes through the spec ReportRenderer, which
507- // dispatches it to the dataset path (queryDataset + grouped table / joined
508- // blocks). Without this it would fall to the legacy ReportViewer, which has no
509- // data source to fetch from → a blank page.
510- const isDatasetBound = Boolean (
511- previewReport &&
512- ( typeof previewReport . dataset === 'string' ||
513- ( previewReport . type === 'joined' &&
514- Array . isArray ( previewReport . blocks ) &&
515- previewReport . blocks . some ( ( b : any ) => typeof b ?. dataset === 'string' ) ) ) ,
516- ) ;
517- const useSpecRenderer = isDatasetBound || Boolean (
518- previewReport &&
519- previewReport . objectName &&
520- ( previewReport . type === 'matrix' ||
521- previewReport . type === 'joined' ||
522- previewReport . type === 'summary' ||
523- previewReport . type === 'tabular' ||
524- previewReport . type === undefined ||
525- ( Array . isArray ( previewReport . groupingsAcross ) && previewReport . groupingsAcross . length > 0 ) ||
526- Array . isArray ( previewReport . columns ) ) ,
527- ) ;
528- const reportForViewer = mapReportForViewer ( previewReport ) ;
529- const viewerSchema : ReportViewerSchema = {
530- type : 'report-viewer' ,
531- report : reportForViewer , // The report definition
532- data : reportRuntimeData , // Runtime data fetched from the data source
533- showToolbar : true ,
534- allowExport : true ,
535- loading : dataLoading , // Loading state for data fetching
536- } ;
363+ // Every report renders through the spec ReportRenderer dispatcher: it routes
364+ // dataset-bound reports to DatasetReportRenderer (aggregation, charts, KPIs,
365+ // drill protocol end-to-end), bridges stored pre-9.0 spec JSON to the
366+ // presentation viewer, and falls back to LegacyReportRenderer for pre-spec
367+ // `{ data, columns }` shapes. `reportRuntimeData` feeds the bridge/legacy
368+ // paths; DatasetReportRenderer fetches its own rows via `useDatasetRows`.
537369
538370 return (
539371 < DrillNavigationProvider value = { { openRecordList } } >
@@ -564,13 +396,9 @@ export function ReportView({ dataSource }: { dataSource?: DataSource }) {
564396 < div className = "flex-1 min-w-0 overflow-auto p-4 sm:p-6 lg:p-8 bg-muted/5" >
565397 < div className = "w-full shadow-sm border rounded-lg sm:rounded-xl bg-background overflow-hidden min-h-150" >
566398 < Suspense fallback = { < div className = "p-8 text-sm text-muted-foreground" > { t ( 'common.loading' , { defaultValue : 'Loading…' } ) } </ div > } >
567- { useSpecRenderer ? (
568- < div className = "p-4 sm:p-6" >
569- < ReportRenderer schema = { previewReport } dataSource = { dataSource as any } rows = { reportRuntimeData } onDrill = { handleDatasetDrill } />
570- </ div >
571- ) : (
572- < ReportViewer schema = { viewerSchema } />
573- ) }
399+ < div className = "p-4 sm:p-6" >
400+ < ReportRenderer schema = { previewReport } dataSource = { dataSource as any } rows = { reportRuntimeData } onDrill = { handleDatasetDrill } />
401+ </ div >
574402 </ Suspense >
575403 </ div >
576404 </ div >
0 commit comments