Skip to content

Commit 059a052

Browse files
os-zhuangclaude
andauthored
feat(report)!: drop SpecReportColumn/SpecReportGrouping re-exports + retire the legacy ReportViewer chart fallback (#3463) (#2816)
Cross-repo close-out of the ADR-0021 report cleanup (framework #3463). Upstream @objectstack/spec removed the dead ReportColumnSchema / ReportGroupingSchema and the unread report chart.groupBy; this drops their objectui mirrors and the now-orphaned legacy report chart path. - types: removed the SpecReportColumn / SpecReportColumnInput / SpecReportGrouping / SpecReportGroupingInput type re-exports and the SpecReportColumnSchema / SpecReportGroupingSchema value re-exports (they aliased the deleted upstream symbols). The live shape is dataset-bound: SpecReport with dataset + values (measure names) + rows/columns (dimension names). - app-shell: ReportView now renders every report through the spec ReportRenderer dispatcher (dataset -> DatasetReportRenderer, stored pre-9.0 JSON -> bridge, pre-spec { data, columns } -> LegacyReportRenderer). Deleted the ReportViewer last-resort branch, the mapReportForViewer spec->legacy chart-section adapter (sole producer of xAxisField/yAxisFields), and the now-dead data-fetch loading flag. No shipped report metadata reached the removed branch. - plugin-report: removed the ReportViewer chart-section branch. It read the invented xAxisField/yAxisFields (never the spec's xAxis/yAxis) and was only fed by the deleted mapReportForViewer. ReportViewer itself is retained -- its table/summary/text sections back the report-viewer component + pre-9.0 bridge. Browser-verified against the showcase backend: the summary, matrix and joined reports all render through DatasetReportRenderer unchanged. Refs #3463 Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent c7cff19 commit 059a052

6 files changed

Lines changed: 47 additions & 248 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
"@object-ui/types": minor
3+
"@object-ui/plugin-report": minor
4+
"@object-ui/app-shell": minor
5+
---
6+
7+
feat(report)!: drop `SpecReportColumn`/`SpecReportGrouping` re-exports + retire the legacy ReportViewer chart fallback (#3463)
8+
9+
Cross-repo close-out of the ADR-0021 report cleanup (framework #3463). Upstream
10+
`@objectstack/spec` removed the dead `ReportColumnSchema` / `ReportGroupingSchema`
11+
and the unread report `chart.groupBy`; this drops their objectui mirrors and the
12+
now-orphaned legacy report chart path.
13+
14+
- **types**: removed the `SpecReportColumn` / `SpecReportColumnInput` /
15+
`SpecReportGrouping` / `SpecReportGroupingInput` type re-exports and the
16+
`SpecReportColumnSchema` / `SpecReportGroupingSchema` value re-exports from
17+
`@object-ui/types` (they aliased the deleted upstream symbols). The live
18+
report shape is dataset-bound — `SpecReport` with `dataset` + `values`
19+
(measure names) + `rows` / `columns` (dimension names).
20+
- **app-shell**: `ReportView` now renders every report through the spec
21+
`ReportRenderer` dispatcher (dataset → `DatasetReportRenderer`, stored pre-9.0
22+
JSON → presentation bridge, pre-spec `{ data, columns }``LegacyReportRenderer`).
23+
Deleted the `ReportViewer` last-resort branch, the `mapReportForViewer`
24+
spec→legacy chart-section adapter (the sole producer of `xAxisField` /
25+
`yAxisFields`), and the now-dead data-fetch loading flag. No shipped report
26+
metadata reached the removed branch — the Studio inspector only ever writes
27+
the dataset-bound shape.
28+
- **plugin-report**: removed the `ReportViewer` chart-section branch. It read
29+
the invented `xAxisField` / `yAxisFields` (never the spec's `xAxis` / `yAxis`)
30+
and was only fed by the deleted `mapReportForViewer`. `ReportViewer` itself is
31+
retained — its table / summary / text sections still back the `report-viewer`
32+
registered component and the pre-9.0 presentation bridge.
33+
34+
**Migration**: nothing an author writes changes. TypeScript consumers importing
35+
`SpecReportColumn*` / `SpecReportGrouping*` from `@object-ui/types` have no
36+
replacement type — model report columns as the dataset's measure names and
37+
grouping as its dimension names.

packages/app-shell/src/views/ReportView.tsx

Lines changed: 10 additions & 182 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import { useState, useEffect, useCallback, useMemo, lazy, Suspense } from 'react';
22
import { useParams } from 'react-router-dom';
3-
const ReportViewer = lazy(() =>
4-
import('@object-ui/plugin-report').then((m) => ({ default: m.ReportViewer })),
5-
);
63
const ReportRenderer = lazy(() =>
74
import('@object-ui/plugin-report').then((m) => ({ default: m.ReportRenderer })),
85
);
@@ -20,7 +17,7 @@ import { useAdapter } from '../providers/AdapterProvider';
2017
import { useMetadataClient } from './metadata-admin/useMetadata';
2118
import { persistRuntimeMetadata } from './runtime-metadata-persistence';
2219
import { useIsWorkspaceAdmin } from '@object-ui/auth';
23-
import type { DataSource, ReportViewerSchema } from '@object-ui/types';
20+
import type { DataSource } from '@object-ui/types';
2421
import type { DatasetDrillArgs } from '@object-ui/plugin-report';
2522
import { DrillDownDrawer } from '@object-ui/plugin-dashboard';
2623
import { 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>

packages/plugin-report/src/ReportViewer.tsx

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import React from 'react';
1010
import { Card, CardContent, CardHeader, CardTitle, CardDescription, Button, Badge, Skeleton } from '@object-ui/components';
1111
import { SchemaRenderer } from '@object-ui/react';
12-
import { ComponentRegistry } from '@object-ui/core';
1312
import type { ReportViewerSchema, ReportSection, ReportExportFormat, ReportField, ReportGroupBy } from '@object-ui/types';
1413
import { Download, Printer, RefreshCw } from 'lucide-react';
1514
import { exportReport } from './ReportExportEngine';
@@ -301,52 +300,6 @@ export const ReportViewer: React.FC<ReportViewerProps> = ({ schema, onRefresh })
301300
</div>
302301
)}
303302

304-
{section.type === 'chart' && section.chart && (
305-
<div className="min-h-[300px]">
306-
{(() => {
307-
// 1. Determine Component Type
308-
// If explicit 'type' is missing, but 'chartType' exists (e.g. "line"), infer 'chart'
309-
let type = section.chart.type;
310-
const hasChartType = !!section.chart.chartType;
311-
312-
// If no strict type but has chartType, assume 'chart' generic renderer
313-
if (!type && hasChartType) {
314-
type = 'chart';
315-
}
316-
317-
// Fallback validation: If resolved type is not registered, try 'chart'
318-
const isRegistered = type && !!ComponentRegistry.get(type);
319-
if (!isRegistered) {
320-
// Even if 'line' was somehow passed as type, fallback to 'chart'
321-
type = 'chart';
322-
}
323-
324-
// 2. Data Adapter (Report Schema -> Chart Component Schema)
325-
// The generic 'chart' component needs mapped props (xAxisKey, series)
326-
// whereas Report schema uses (xAxisField, yAxisFields)
327-
const isGenericChart = type === 'chart';
328-
const adapterProps = isGenericChart ? {
329-
xAxisKey: section.chart.xAxisKey || section.chart.xAxisField || 'name',
330-
series: section.chart.series || (section.chart.yAxisFields ? section.chart.yAxisFields.map((f: any) => ({ dataKey: f })) : []),
331-
// Ensure chartType is passed if we are using the generic renderer
332-
chartType: section.chart.chartType || 'bar',
333-
} : {};
334-
335-
// 3. Construct Safe Schema
336-
const safeSchema = {
337-
...section.chart,
338-
type,
339-
...adapterProps,
340-
data: data || section.chart.data,
341-
// Force explicit height to preventing Recharts "height(-1)" error
342-
className: section.chart.className || 'w-full h-[350px]'
343-
};
344-
345-
return <SchemaRenderer schema={safeSchema} />;
346-
})()}
347-
</div>
348-
)}
349-
350303
{section.type === 'table' && (
351304
<div className="border rounded-lg overflow-x-auto">
352305
<table className="w-full text-sm min-w-[600px]">

packages/types/src/__tests__/spec-ui-schema-reexports.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,6 @@ describe('spec/ui …Schema re-exports (#2561, decision (a))', () => {
147147
expect(typeof Types.defineStack).toBe('function');
148148
expect(Types.ObjectStackSchema).toBeDefined();
149149
expect(Types.SpecReportSchema).toBeDefined();
150-
expect(Types.SpecReportColumnSchema).toBeDefined();
151150
expect(Types.SpecReportTypeEnum).toBeDefined();
152151
expect(Types.ACTION_LOCATIONS).toBeDefined();
153152
expect(Types.ActionLocationSchema).toBeDefined();

packages/types/src/index.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -691,10 +691,6 @@ export type {
691691
// ---------------------------------------------------------------------------
692692
export type {
693693
SpecReportInput,
694-
SpecReportColumn,
695-
SpecReportColumnInput,
696-
SpecReportGrouping,
697-
SpecReportGroupingInput,
698694
SpecReportChart,
699695
SpecReportChartInput,
700696
SpecReportTypeName,
@@ -708,8 +704,6 @@ export type {
708704

709705
export {
710706
SpecReportSchema,
711-
SpecReportColumnSchema,
712-
SpecReportGroupingSchema,
713707
SpecReportChartSchema,
714708
SpecReportTypeEnum,
715709
SpecReport,

0 commit comments

Comments
 (0)