@object-ui/plugin-report 17.5.0 → 17.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +333 -0
- package/README.md +399 -127
- package/dist/DatasetReportRenderer.d.ts +13 -0
- package/dist/index.js +414 -373
- package/dist/index.umd.cjs +2 -2
- package/dist/useDatasetDimensionLabels.d.ts +12 -1
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# @object-ui/plugin-report
|
|
2
2
|
|
|
3
|
-
Report components for Object UI —
|
|
3
|
+
Report components for Object UI — render, view and export reports, with scheduling support. Report *authoring* is not part of this package.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- 🧩 **Four spec report variants** — `tabular` / `summary` / `matrix` / `joined`, dispatched by a single `<ReportRenderer schema={...}>`
|
|
8
|
-
- 🧮 **Server-side aggregation** —
|
|
9
|
-
- 📅 **
|
|
10
|
-
- 🪜 **Multi-level grouping + totals** — row
|
|
11
|
-
- 🎯 **Cell drill-down** —
|
|
12
|
-
- 🧱 **Joined reports** — vertically stacked sub-reports; each block
|
|
13
|
-
- 🎨 **Type-aware cells** — `select` → Badge, `lookup` → link, `boolean` → ✓/✗, `email`/`url`/`phone` → links, `image` → thumbnail
|
|
8
|
+
- 🧮 **Server-side aggregation** — a dataset-bound report selects measures by name through `dataSource.queryDataset`; totals come back pre-aggregated (ADR-0021)
|
|
9
|
+
- 📅 **Period pivots** — the across axis is a dimension the dataset declares at the grain it needs; a report buckets nothing itself
|
|
10
|
+
- 🪜 **Multi-level grouping + totals** — row subtotals, column subtotals and grand total for `matrix`, a grand-total footer for `summary`, all server-computed
|
|
11
|
+
- 🎯 **Cell drill-down** — aggregated rows and matrix cells emit `DatasetDrillArgs` to the host's `onDrill` callback; the host owns navigation (ADR-0021 D2)
|
|
12
|
+
- 🧱 **Joined reports** — vertically stacked sub-reports; each block names its own `dataset`, filter and ordering
|
|
13
|
+
- 🎨 **Type-aware cells** — the presentation-layer `ReportViewer` renders `select` → Badge, `lookup` → link, `boolean` → ✓/✗, `email`/`url`/`phone` → links, `image` → thumbnail
|
|
14
14
|
- 🖨️ **Multi-format export** — CSV, JSON, HTML, PDF, Excel; live-data and Excel-formula variants
|
|
15
15
|
- 📦 **Auto-registered** — components register with `ComponentRegistry` on import; embed via `{ "type": "spec-report", "report": {...} }`
|
|
16
16
|
|
|
@@ -26,23 +26,55 @@ npm install @object-ui/plugin-report
|
|
|
26
26
|
|
|
27
27
|
## Quick Start
|
|
28
28
|
|
|
29
|
+
`ReportRenderer` is the one entry point — a dispatcher that routes any report
|
|
30
|
+
shape to the right renderer. It takes the report as `schema`; there is no
|
|
31
|
+
report-authoring component in this package (see
|
|
32
|
+
[Legacy presentation layer](#legacy-presentation-layer)).
|
|
33
|
+
|
|
29
34
|
```tsx
|
|
30
|
-
import {
|
|
35
|
+
import { ReportRenderer } from '@object-ui/plugin-report';
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
// `dataSource` and `router` are host-provided.
|
|
38
|
+
function ReportPage() {
|
|
39
|
+
return (
|
|
40
|
+
<ReportRenderer
|
|
41
|
+
schema={{
|
|
42
|
+
name: 'opp_by_stage',
|
|
43
|
+
type: 'summary',
|
|
44
|
+
dataset: 'opportunity_pipeline',
|
|
45
|
+
rows: ['stage'],
|
|
46
|
+
values: ['amount_sum', 'deal_count'],
|
|
47
|
+
}}
|
|
48
|
+
dataSource={dataSource}
|
|
49
|
+
onDrill={({ object }) => router.push(`/records/${object}`)}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
34
52
|
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The dataset-bound renderer can also be addressed directly when the host has
|
|
56
|
+
already decided the shape — it takes the report as `report`, not `schema`:
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import { DatasetReportRenderer, isDatasetReport } from '@object-ui/plugin-report';
|
|
35
60
|
|
|
36
|
-
|
|
37
|
-
return <
|
|
61
|
+
if (isDatasetReport(stored)) {
|
|
62
|
+
return <DatasetReportRenderer report={stored} dataSource={dataSource} />;
|
|
38
63
|
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`ReportViewer` renders a presentation-layer report, and takes a whole
|
|
67
|
+
`ReportViewerSchema` as `schema` — `report` / `showToolbar` are keys *inside*
|
|
68
|
+
that schema, not props of the component:
|
|
39
69
|
|
|
40
|
-
|
|
70
|
+
```tsx
|
|
71
|
+
import { ReportViewer } from '@object-ui/plugin-report';
|
|
72
|
+
|
|
73
|
+
function LegacyReportPage() {
|
|
41
74
|
return (
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
chart={chartConfig}
|
|
75
|
+
<ReportViewer
|
|
76
|
+
schema={{ type: 'report-viewer', report: reportDefinition, data: rows, showToolbar: true }}
|
|
77
|
+
onRefresh={refetch}
|
|
46
78
|
/>
|
|
47
79
|
);
|
|
48
80
|
}
|
|
@@ -50,87 +82,201 @@ function EmbeddedReport() {
|
|
|
50
82
|
|
|
51
83
|
## Spec Reports — the four variants
|
|
52
84
|
|
|
53
|
-
|
|
85
|
+
A report is **dataset-bound**: it names a semantic-layer `dataset` and selects
|
|
86
|
+
that dataset's measures (`values`) grouped by its dimensions (`rows`, and for a
|
|
87
|
+
matrix also `columns`). Every name in a report is a name the dataset defines —
|
|
88
|
+
the report declares no object, no field, no aggregate and no date bucket of its
|
|
89
|
+
own. `defineReport` validates at authoring time and returns the parsed shape
|
|
90
|
+
`ReportRenderer` takes as `schema`:
|
|
54
91
|
|
|
55
92
|
```ts
|
|
56
|
-
import
|
|
93
|
+
import { defineReport } from '@objectstack/spec/ui';
|
|
57
94
|
import { ReportRenderer } from '@object-ui/plugin-report';
|
|
58
95
|
|
|
59
|
-
const report
|
|
96
|
+
const report = defineReport({
|
|
60
97
|
name: 'opp_by_stage',
|
|
61
|
-
|
|
98
|
+
label: 'Opportunities by Stage',
|
|
62
99
|
type: 'summary',
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
groupingsDown: [{ field: 'stage', sortOrder: 'asc' }],
|
|
69
|
-
};
|
|
100
|
+
dataset: 'opportunity_pipeline',
|
|
101
|
+
rows: ['stage'],
|
|
102
|
+
values: ['amount_sum', 'deal_count'],
|
|
103
|
+
order: [{ by: 'amount_sum', direction: 'desc' }],
|
|
104
|
+
});
|
|
70
105
|
|
|
71
106
|
<ReportRenderer schema={report} dataSource={ds} />
|
|
72
107
|
```
|
|
73
108
|
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
109
|
+
| Key | Type | Meaning |
|
|
110
|
+
| --------------- | ---------------------------------------------- | ---------------------------------------------------------------- |
|
|
111
|
+
| `name` | `string` (required) | Identifier, at least 2 characters. |
|
|
112
|
+
| `label` | `string` \| `Record< string, string >` (required) | Display title; the record form is the i18n shape. |
|
|
113
|
+
| `type` | `tabular` \| `summary` \| `matrix` \| `joined` | Defaults to `tabular`. |
|
|
114
|
+
| `dataset` | `string` | The dataset this report reads. |
|
|
115
|
+
| `rows` | `string[]` | Dimension **names** to group down. |
|
|
116
|
+
| `columns` | `string[]` | Dimension **names** across — the matrix pivot axis. |
|
|
117
|
+
| `values` | `string[]` | Measure **names** to display. |
|
|
118
|
+
| `runtimeFilter` | filter condition | Scope filter, merged with the filter the host passes at render. |
|
|
119
|
+
| `order` | `{ by, direction }[]` | Result ordering, most significant key first. |
|
|
120
|
+
| `drilldown` | `boolean` | Defaults to `true`. |
|
|
121
|
+
| `chart` | object | Embedded visualization over the same selection. |
|
|
122
|
+
| `blocks` | report[] | `joined` only — the stacked sub-reports. |
|
|
123
|
+
|
|
124
|
+
The schema is **strict**: a key it does not declare is rejected, not ignored.
|
|
125
|
+
|
|
126
|
+
| `type` | Description |
|
|
127
|
+
| --------- | ------------------------------------------------------------------------------ |
|
|
128
|
+
| `tabular` | The selection as a flat list — `rows` + `values`, no totals |
|
|
129
|
+
| `summary` | The same table plus a server-computed grand-total footer |
|
|
130
|
+
| `matrix` | Cross-tab: `rows` down × `columns` across, measures in the cells, plus subtotals |
|
|
131
|
+
| `joined` | A vertical stack of blocks, each its own dataset-bound table |
|
|
132
|
+
|
|
133
|
+
The **declared type** picks the presentation — never the shape of the data that
|
|
134
|
+
comes back.
|
|
80
135
|
|
|
81
136
|
### Matrix (row × column pivot)
|
|
82
137
|
|
|
138
|
+
`columns` is a second list of dimension **names**, used as the across axis:
|
|
139
|
+
|
|
83
140
|
```ts
|
|
84
|
-
{
|
|
141
|
+
import { defineReport } from '@objectstack/spec/ui';
|
|
142
|
+
|
|
143
|
+
defineReport({
|
|
85
144
|
name: 'pipeline_by_quarter',
|
|
86
|
-
|
|
145
|
+
label: 'Pipeline Coverage by Quarter',
|
|
87
146
|
type: 'matrix',
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
147
|
+
dataset: 'opportunity_pipeline',
|
|
148
|
+
rows: ['forecast_category'],
|
|
149
|
+
columns: ['close_quarter'],
|
|
150
|
+
values: ['amount_sum'],
|
|
151
|
+
});
|
|
92
152
|
```
|
|
93
153
|
|
|
94
|
-
`
|
|
95
|
-
|
|
154
|
+
`close_quarter` is a time dimension the **dataset** declares at that grain. A
|
|
155
|
+
report cannot bucket a date itself: the grain a measure is valid at belongs to
|
|
156
|
+
the semantic layer, not to one report. A `matrix` with no `columns` degrades to
|
|
157
|
+
the flat grouped table.
|
|
96
158
|
|
|
97
|
-
### Joined
|
|
159
|
+
### Joined
|
|
98
160
|
|
|
99
161
|
```ts
|
|
100
|
-
{
|
|
162
|
+
import { defineReport } from '@objectstack/spec/ui';
|
|
163
|
+
|
|
164
|
+
defineReport({
|
|
101
165
|
name: 'churn_signals',
|
|
102
|
-
|
|
166
|
+
label: 'Customer Churn Signals',
|
|
103
167
|
type: 'joined',
|
|
104
|
-
columns: [],
|
|
105
168
|
blocks: [
|
|
106
|
-
{
|
|
107
|
-
|
|
169
|
+
{
|
|
170
|
+
name: 'at_risk',
|
|
171
|
+
label: 'At-Risk Accounts',
|
|
172
|
+
type: 'summary',
|
|
173
|
+
dataset: 'account_health',
|
|
174
|
+
rows: ['industry'],
|
|
175
|
+
values: ['account_count'],
|
|
176
|
+
runtimeFilter: { is_active: true },
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'lost',
|
|
180
|
+
label: 'Recently Lost',
|
|
181
|
+
type: 'summary',
|
|
182
|
+
dataset: 'opportunity_pipeline',
|
|
183
|
+
rows: ['owner'],
|
|
184
|
+
values: ['amount_sum'],
|
|
185
|
+
runtimeFilter: { stage: 'closed_lost' },
|
|
186
|
+
},
|
|
108
187
|
],
|
|
109
|
-
}
|
|
188
|
+
});
|
|
110
189
|
```
|
|
111
190
|
|
|
112
|
-
Block rules:
|
|
113
|
-
|
|
114
|
-
|
|
191
|
+
Block rules: every block names its **own** `dataset` (there is no container
|
|
192
|
+
dataset to inherit); the container's `runtimeFilter` is merged into each block
|
|
193
|
+
and the block's own keys win; each block orders itself through its own `order`;
|
|
194
|
+
a block's declared `type` picks its presentation; `block.type` must not be
|
|
195
|
+
`joined` (no recursion).
|
|
115
196
|
|
|
116
|
-
|
|
197
|
+
### Ordering
|
|
198
|
+
|
|
199
|
+
`order` is a list, most significant key first, lowered onto the dataset
|
|
200
|
+
selection so the **server** orders the query — which is why ordering by a
|
|
201
|
+
derived measure works and the sort applies to the whole result rather than to
|
|
202
|
+
one fetched page. `by` names a dimension the report groups by or a measure it
|
|
203
|
+
displays. For a matrix the across axis reads left-to-right in row-arrival
|
|
204
|
+
order, so ordering by the across dimension is what fixes the column order;
|
|
205
|
+
declaring nothing still reads correctly, because a selected time dimension
|
|
206
|
+
defaults to ascending.
|
|
117
207
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
208
|
+
### Embedded chart
|
|
209
|
+
|
|
210
|
+
`chart.type` is one of the spec's chart names; `xAxis` / `yAxis` are **bare
|
|
211
|
+
dimension and measure names** from the report's own selection, not field paths
|
|
212
|
+
and not expressions:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
import { defineReport } from '@objectstack/spec/ui';
|
|
216
|
+
|
|
217
|
+
defineReport({
|
|
218
|
+
name: 'pipeline_by_stage',
|
|
219
|
+
label: 'Pipeline by Stage',
|
|
220
|
+
type: 'summary',
|
|
221
|
+
dataset: 'opportunity_pipeline',
|
|
222
|
+
rows: ['stage'],
|
|
223
|
+
values: ['amount_sum'],
|
|
224
|
+
chart: {
|
|
225
|
+
type: 'bar',
|
|
226
|
+
title: 'Pipeline by Stage',
|
|
227
|
+
xAxis: 'stage',
|
|
228
|
+
yAxis: 'amount_sum',
|
|
229
|
+
showLegend: false,
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`series`, `colors`, `height`, `showDataLabels`, `annotations` and `interaction`
|
|
235
|
+
are passed through to the chart as authored.
|
|
236
|
+
|
|
237
|
+
## Server-side aggregation + drill-down
|
|
121
238
|
|
|
122
|
-
|
|
239
|
+
A dataset-bound report selects its dimensions (`rows`, and for `matrix` also
|
|
240
|
+
`columns`) and measures (`values`) **by name** and runs them through
|
|
241
|
+
`dataSource.queryDataset` — the same governed semantic-layer path that
|
|
242
|
+
dataset-bound dashboard widgets and the dataset preview use, so the numbers and
|
|
243
|
+
the server-resolved dimension labels match everywhere. Totals (row subtotals,
|
|
244
|
+
column subtotals, grand total) are **server-computed**: the renderer places the
|
|
245
|
+
pre-aggregated rows it is handed and never re-aggregates bucketed values
|
|
246
|
+
client-side, since measures like `avg` cannot be recombined without drifting
|
|
247
|
+
from the semantic layer.
|
|
248
|
+
|
|
249
|
+
Drill-down is a **host callback, not a registered handler**: pass `onDrill` and
|
|
250
|
+
every aggregated row / matrix cell becomes clickable. The report emits *what was
|
|
251
|
+
clicked*; the host decides where that goes, because the renderer only knows
|
|
252
|
+
dimension names (ADR-0021 D2).
|
|
123
253
|
|
|
124
254
|
```tsx
|
|
125
|
-
import {
|
|
126
|
-
|
|
255
|
+
import { ReportRenderer, type DatasetDrillArgs } from '@object-ui/plugin-report';
|
|
256
|
+
|
|
257
|
+
<ReportRenderer
|
|
258
|
+
schema={report}
|
|
259
|
+
dataSource={dataSource}
|
|
260
|
+
onDrill={(args: DatasetDrillArgs) => {
|
|
261
|
+
// The host resolves dataset → object and dimension → field, then navigates.
|
|
262
|
+
const filter = { ...args.objectFilter, ...args.runtimeFilter };
|
|
263
|
+
router.push(`/records/${args.object}?filter=${encodeURIComponent(JSON.stringify(filter))}`);
|
|
264
|
+
}}
|
|
265
|
+
/>
|
|
127
266
|
```
|
|
128
267
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
268
|
+
What a drill click carries:
|
|
269
|
+
|
|
270
|
+
| `DatasetDrillArgs` | Meaning |
|
|
271
|
+
| ------------------ | ------- |
|
|
272
|
+
| `dataset` | Dataset the clicked aggregate was computed over. |
|
|
273
|
+
| `groupKey` | Dimension **name** → clicked bucket value (row dims, plus across dims for a matrix cell). |
|
|
274
|
+
| `runtimeFilter`| The effective render-time scope filter, if any. |
|
|
275
|
+
| `object` | The dataset's base object, when the server supplied it. |
|
|
276
|
+
| `objectFilter` | Exact record-list filter (object **field** name → raw stored value) for the clicked bucket. Present only when the server returned the dimension→field mapping plus raw grouped values — that is what lets select/lookup dimensions filter precisely instead of by display label. |
|
|
277
|
+
|
|
278
|
+
Supply no `onDrill` and nothing is clickable; a report can also opt out with
|
|
279
|
+
`drilldown: false`.
|
|
134
280
|
|
|
135
281
|
## Filter-time date helpers — current limitation
|
|
136
282
|
|
|
@@ -144,21 +290,76 @@ const daysAgo = (n: number): string => {
|
|
|
144
290
|
return d.toISOString().slice(0, 10);
|
|
145
291
|
};
|
|
146
292
|
|
|
147
|
-
|
|
293
|
+
runtimeFilter: { close_date: { $gte: daysAgo(30) } }
|
|
148
294
|
```
|
|
149
295
|
|
|
150
296
|
See the bundled CRM `customer_churn_signals` demo for the full pattern.
|
|
151
297
|
Native filter-time CEL evaluation is tracked for a future major version.
|
|
152
298
|
|
|
153
|
-
##
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
available for
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
299
|
+
## Stored pre-9.0 documents — migration only
|
|
300
|
+
|
|
301
|
+
> ⚠️ **This section is about documents that already exist in storage. The shape
|
|
302
|
+
> it describes is NOT available for authoring** — everything above is how a
|
|
303
|
+
> report is written, nothing here is.
|
|
304
|
+
|
|
305
|
+
Before the ADR-0021 cutover a report carried its own query: an `objectName`,
|
|
306
|
+
`columns` as column *definition objects* with `aggregate`, and
|
|
307
|
+
`groupingsDown` / `groupingsAcross` with `sortOrder` and `dateGranularity`.
|
|
308
|
+
That form is **rejected by the current schema** — `objectName` and
|
|
309
|
+
`groupingsDown` come back as unrecognized keys, and object-shaped `columns`
|
|
310
|
+
fails as the wrong type (`columns` is now a list of dimension names).
|
|
311
|
+
|
|
312
|
+
Stored documents in that shape still render, through a deliberately lossy
|
|
313
|
+
bridge: `ReportRenderer` converts them to a presentation report and hands that
|
|
314
|
+
to `ReportViewer`. The converter reads the *current* keys (`values`, `rows`), so
|
|
315
|
+
a pre-9.0 document arrives with none of them and what comes out is the report's
|
|
316
|
+
title over an **empty column set**. The document renders; its columns,
|
|
317
|
+
groupings, aggregates and sort do not, and nothing reports an error. That is why
|
|
318
|
+
the bridge is a waiting room rather than a supported way to write a report.
|
|
319
|
+
|
|
320
|
+
Migrating one is a re-expression against the dataset:
|
|
321
|
+
|
|
322
|
+
| Pre-9.0 key | Where it goes now |
|
|
323
|
+
| ---------------------------------- | ----------------------------------------------------------------------- |
|
|
324
|
+
| `objectName` | Gone from the report — the `dataset` owns the object. |
|
|
325
|
+
| `columns: [{ field, aggregate }]` | `values: string[]`; the aggregate becomes a **measure** in the dataset. |
|
|
326
|
+
| `columns: [{ field }]` | `rows: string[]`, as a dimension. |
|
|
327
|
+
| `groupingsDown: [{ field }]` | `rows: string[]`. |
|
|
328
|
+
| `groupingsAcross: [{ field }]` | `columns: string[]`. |
|
|
329
|
+
| `sortOrder` on a grouping | `order: [{ by, direction }]` on the report. |
|
|
330
|
+
| `dateGranularity` on a grouping | A time dimension declared at that grain **in the dataset**. |
|
|
331
|
+
| `filter` | `runtimeFilter`. |
|
|
332
|
+
|
|
333
|
+
The part with no mechanical equivalent is the dataset itself: measures and time
|
|
334
|
+
dimensions the old report declared inline must exist in the semantic layer
|
|
335
|
+
before the migrated report can name them.
|
|
336
|
+
|
|
337
|
+
The query-form renderers that used to draw these documents inline
|
|
338
|
+
(`SpecReportGrid`, `MatrixRenderer`, `JoinedReportRenderer`) were removed at the
|
|
339
|
+
cutover and have no replacement export — the bridge above is what renders a
|
|
340
|
+
stored old-shape document now.
|
|
341
|
+
|
|
342
|
+
## Legacy presentation layer
|
|
343
|
+
|
|
344
|
+
`ReportViewer`, `LegacyReportRenderer` and the export functions below remain
|
|
345
|
+
available for pre-spec presentation reports (`{ report, data, columns, chart }`)
|
|
346
|
+
and are not affected by the dataset-bound pipeline above.
|
|
347
|
+
|
|
348
|
+
The authoring components that used to sit alongside them — `ReportBuilder`,
|
|
349
|
+
`ScheduleConfig`, `ChartConfig`, the columns/groupings editors — and the
|
|
350
|
+
`registerDrillHandler`-style drill helpers were **removed** in the 9.0 cutover
|
|
351
|
+
(see `CHANGELOG.md`). They have no replacement export in this package: report
|
|
352
|
+
authoring lives in the console designer, and drill-down is the `onDrill`
|
|
353
|
+
callback above.
|
|
354
|
+
|
|
355
|
+
### Export
|
|
356
|
+
|
|
357
|
+
Every format exporter takes the **report schema first and the rows second** —
|
|
358
|
+
`(report: ReportComponentSchema, data: any[], config?: ReportExportConfig)`. The
|
|
359
|
+
schema is where the exporter reads the columns (`report.fields`) and the download
|
|
360
|
+
name (`config.filename`, else `report.title`) from; there is **no filename
|
|
361
|
+
parameter**. All five return `void` synchronously — they trigger a browser
|
|
362
|
+
download — so there is nothing to `await`:
|
|
162
363
|
|
|
163
364
|
```tsx
|
|
164
365
|
import {
|
|
@@ -169,51 +370,151 @@ import {
|
|
|
169
370
|
exportAsPDF,
|
|
170
371
|
exportAsExcel,
|
|
171
372
|
} from '@object-ui/plugin-report';
|
|
373
|
+
import type { ReportComponentSchema } from '@object-ui/types';
|
|
374
|
+
|
|
375
|
+
const report: ReportComponentSchema = {
|
|
376
|
+
type: 'report',
|
|
377
|
+
title: 'Sales Report',
|
|
378
|
+
fields: [
|
|
379
|
+
{ name: 'account', label: 'Account' },
|
|
380
|
+
{ name: 'amount', label: 'Amount', type: 'currency', aggregation: 'sum' },
|
|
381
|
+
],
|
|
382
|
+
};
|
|
383
|
+
const rows = [{ account: 'Acme', amount: 12000 }];
|
|
384
|
+
|
|
385
|
+
exportAsCSV(report, rows); // downloads "Sales Report.csv"
|
|
386
|
+
exportAsJSON(report, rows); // "Sales Report.json"
|
|
387
|
+
exportAsHTML(report, rows); // "Sales Report.html"
|
|
388
|
+
exportAsPDF(report, rows); // opens a print window; falls back to .html if blocked
|
|
389
|
+
exportAsExcel(report, rows, { format: 'excel', filename: 'sales-2026-q1.tsv' });
|
|
172
390
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
await exportAsExcel(reportData, 'sales-report.xlsx');
|
|
391
|
+
// `exportReport` is the router — the format comes FIRST, then the same triple.
|
|
392
|
+
exportReport('csv', report, rows);
|
|
176
393
|
```
|
|
177
394
|
|
|
395
|
+
`exportAsExcel` writes a BOM-prefixed **TSV** that Excel opens, so its default
|
|
396
|
+
name is `report.title` + `.tsv`, not `.xlsx`. `config.includeHeaders: false` drops the
|
|
397
|
+
header row; `orientation` / `pageSize` apply to the HTML and PDF paths only.
|
|
398
|
+
|
|
178
399
|
### Live Export
|
|
179
400
|
|
|
180
|
-
|
|
401
|
+
`exportWithLiveData` fetches the rows itself, so `dataSource` and `resource` are
|
|
402
|
+
**required** in `LiveExportOptions` — an options object carrying only `format`
|
|
403
|
+
does not compile. It is the one `async` export function, resolving to a
|
|
404
|
+
`LiveExportResult`:
|
|
181
405
|
|
|
182
406
|
```tsx
|
|
183
|
-
import { exportWithLiveData
|
|
407
|
+
import { exportWithLiveData } from '@object-ui/plugin-report';
|
|
184
408
|
|
|
185
|
-
|
|
186
|
-
await
|
|
187
|
-
|
|
409
|
+
// `myAdapter` is a host-provided DataSource (see @object-ui/data-*).
|
|
410
|
+
const result = await exportWithLiveData(report, {
|
|
411
|
+
dataSource: myAdapter,
|
|
412
|
+
resource: 'orders',
|
|
413
|
+
format: 'pdf',
|
|
188
414
|
});
|
|
415
|
+
// -> { success: true, recordCount: 42, format: 'pdf' }
|
|
189
416
|
```
|
|
190
417
|
|
|
191
|
-
|
|
418
|
+
The format falls back to `report.defaultExportFormat`, then `'pdf'`. Failures are
|
|
419
|
+
returned, not thrown: `{ success: false, recordCount: 0, format, error }`.
|
|
192
420
|
|
|
193
|
-
|
|
421
|
+
`exportExcelWithFormulas` is a separate, synchronous three-parameter function —
|
|
422
|
+
`(report, data, options)` — with the column list inside the third argument. A
|
|
423
|
+
column is `{ name, header, width?, numberFormat?, formula? }`: `name` is the key
|
|
424
|
+
read off each row (there is no `field` key), `header` is required, and a `formula`
|
|
425
|
+
is a template in which the `{ROW}` placeholder is replaced by that row's
|
|
426
|
+
spreadsheet line number:
|
|
194
427
|
|
|
195
428
|
```tsx
|
|
196
|
-
import {
|
|
429
|
+
import { exportExcelWithFormulas } from '@object-ui/plugin-report';
|
|
197
430
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
431
|
+
exportExcelWithFormulas(report, rows, {
|
|
432
|
+
columns: [
|
|
433
|
+
{ name: 'amount', header: 'Amount', numberFormat: '#,##0.00' },
|
|
434
|
+
{ name: 'total', header: 'Total', formula: '=B{ROW}*C{ROW}' },
|
|
435
|
+
],
|
|
436
|
+
includeAggregationRow: true,
|
|
437
|
+
});
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Omitting `columns` derives them from `report.fields`. `includeAggregationRow`
|
|
441
|
+
appends a totals row built from each field's `aggregation`.
|
|
442
|
+
|
|
443
|
+
### Scheduled export
|
|
444
|
+
|
|
445
|
+
A schedule is **data on the report schema**, not a component: it is
|
|
446
|
+
`ReportComponentSchema.schedule`, typed `ReportScheduleConfig`. Both types live
|
|
447
|
+
in `@object-ui/types` and are *not* re-exported here — importing them from this
|
|
448
|
+
package is a TS2305.
|
|
449
|
+
|
|
450
|
+
`createScheduleTrigger` turns that declared schedule into a callable a workflow
|
|
451
|
+
engine can invoke. It takes the report, the data source, the resource to query
|
|
452
|
+
and a completion callback:
|
|
202
453
|
|
|
203
|
-
|
|
454
|
+
```ts
|
|
455
|
+
import { createScheduleTrigger } from '@object-ui/plugin-report';
|
|
456
|
+
import type { ReportComponentSchema, ReportScheduleConfig } from '@object-ui/types';
|
|
457
|
+
|
|
458
|
+
// `dataSource` and `notify` are host-provided.
|
|
459
|
+
|
|
460
|
+
const report: ReportComponentSchema = {
|
|
461
|
+
type: 'report',
|
|
462
|
+
title: 'Monthly Sales',
|
|
463
|
+
schedule: {
|
|
464
|
+
enabled: true,
|
|
465
|
+
frequency: 'monthly',
|
|
466
|
+
dayOfMonth: 1,
|
|
467
|
+
time: '07:00',
|
|
468
|
+
formats: ['pdf', 'excel'],
|
|
469
|
+
recipients: ['sales@example.com'],
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const trigger = createScheduleTrigger(
|
|
474
|
+
report,
|
|
475
|
+
dataSource,
|
|
476
|
+
'orders',
|
|
477
|
+
(exported: ReportComponentSchema, schedule: ReportScheduleConfig) => {
|
|
478
|
+
notify(schedule.recipients ?? [], schedule.subject ?? exported.title);
|
|
479
|
+
},
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
const results = await trigger(); // LiveExportResult[] — one entry per scheduled format
|
|
204
483
|
```
|
|
205
484
|
|
|
485
|
+
`trigger()` reads `report.schedule` itself: a schedule that is missing or
|
|
486
|
+
`enabled: false` exports nothing and resolves to `[]`. Otherwise it exports once
|
|
487
|
+
per entry in `schedule.formats` (falling back to `report.defaultExportFormat`,
|
|
488
|
+
then `'pdf'`) and calls the completion callback with the report and the schedule
|
|
489
|
+
it ran.
|
|
490
|
+
|
|
206
491
|
### Schema-Driven Usage
|
|
207
492
|
|
|
208
|
-
|
|
493
|
+
Importing the package registers three component types with `ComponentRegistry`:
|
|
494
|
+
|
|
495
|
+
| `type` | Component | Notes |
|
|
496
|
+
| --------------- | ---------------- | -------------------------------------------------- |
|
|
497
|
+
| `report` | `ReportRenderer` | The dispatcher. |
|
|
498
|
+
| `spec-report` | `ReportRenderer` | Spec-native alias; the report goes under `report`. |
|
|
499
|
+
| `report-viewer` | `ReportViewer` | Presentation-layer viewer. |
|
|
209
500
|
|
|
210
501
|
```json
|
|
211
502
|
{
|
|
212
|
-
"type": "report
|
|
213
|
-
"report": {
|
|
503
|
+
"type": "spec-report",
|
|
504
|
+
"report": {
|
|
505
|
+
"name": "opp_by_stage",
|
|
506
|
+
"type": "summary",
|
|
507
|
+
"dataset": "opportunity_pipeline",
|
|
508
|
+
"rows": ["stage"],
|
|
509
|
+
"values": ["amount_sum"]
|
|
510
|
+
}
|
|
214
511
|
}
|
|
215
512
|
```
|
|
216
513
|
|
|
514
|
+
There is no `report-builder` component type — the authoring component that name
|
|
515
|
+
addressed was removed in the 9.0 cutover, so a node declaring it resolves to
|
|
516
|
+
nothing.
|
|
517
|
+
|
|
217
518
|
### Type-aware cell rendering
|
|
218
519
|
|
|
219
520
|
`ReportViewer` delegates cell rendering to the shared `getCellRenderer`
|
|
@@ -237,40 +538,11 @@ component appropriate for its type — instead of `String(value)`.
|
|
|
237
538
|
| `richtext` / `html` / `markdown` | Sanitised inline content |
|
|
238
539
|
| `json` | Collapsed code preview |
|
|
239
540
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
Minimal report leveraging type-aware cells:
|
|
246
|
-
|
|
247
|
-
```ts
|
|
248
|
-
import type { ReportInput } from '@objectstack/spec/ui';
|
|
249
|
-
|
|
250
|
-
export const ContactsReport: ReportInput = {
|
|
251
|
-
name: 'contacts_by_account',
|
|
252
|
-
label: 'Contacts by Account',
|
|
253
|
-
objectName: 'contact', // ← enables auto-hydration
|
|
254
|
-
type: 'tabular',
|
|
255
|
-
columns: [
|
|
256
|
-
{ field: 'full_name', label: 'Name' },
|
|
257
|
-
{ field: 'email', label: 'Email' }, // → mailto:
|
|
258
|
-
{ field: 'phone', label: 'Phone' }, // → tel:
|
|
259
|
-
{ field: 'is_primary', label: 'Primary' }, // → ✓/✗
|
|
260
|
-
{ field: 'account', label: 'Account' }, // → linked record
|
|
261
|
-
{ field: 'status', label: 'Status' }, // → Badge with option color
|
|
262
|
-
],
|
|
263
|
-
};
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
Override per column when needed:
|
|
267
|
-
|
|
268
|
-
```ts
|
|
269
|
-
columns: [
|
|
270
|
-
{ field: 'tier', label: 'Tier', type: 'select',
|
|
271
|
-
options: [{ value: 'gold', label: 'Gold', color: 'amber' }] },
|
|
272
|
-
]
|
|
273
|
-
```
|
|
541
|
+
These are presentation-layer columns: each entry in `report.fields` carries its
|
|
542
|
+
own `name`, `label` and `type`, and that declared `type` is what selects the
|
|
543
|
+
renderer. This package resolves nothing from object metadata — there is no
|
|
544
|
+
`objectName` on a report to hydrate from, and a column with no `type` renders
|
|
545
|
+
as plain text.
|
|
274
546
|
|
|
275
547
|
Legacy `renderAs: 'badge'` + `colorMap` is still honoured for plain
|
|
276
548
|
string columns.
|