@object-ui/plugin-dashboard 17.6.0 → 17.7.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 +4519 -0
- package/README.md +424 -83
- package/dist/DashboardConfigPanel.d.ts +1 -1
- package/dist/DashboardFilterBar.d.ts.map +1 -1
- package/dist/DashboardGridLayout.d.ts +3 -2
- package/dist/DashboardGridLayout.d.ts.map +1 -1
- package/dist/DashboardRenderer.d.ts +32 -2
- package/dist/DashboardRenderer.d.ts.map +1 -1
- package/dist/DashboardWithConfig.d.ts +1 -1
- package/dist/DashboardWithConfig.d.ts.map +1 -1
- package/dist/DatasetWidget.d.ts +42 -14
- package/dist/DatasetWidget.d.ts.map +1 -1
- package/dist/DrillDownDrawer.d.ts +14 -5
- package/dist/DrillDownDrawer.d.ts.map +1 -1
- package/dist/MetricCard.d.ts +1 -1
- package/dist/MetricWidget.d.ts +27 -2
- package/dist/MetricWidget.d.ts.map +1 -1
- package/dist/ObjectDataTable.d.ts +407 -18
- package/dist/ObjectDataTable.d.ts.map +1 -1
- package/dist/ObjectMetricWidget.d.ts +35 -8
- package/dist/ObjectMetricWidget.d.ts.map +1 -1
- package/dist/ObjectPivotTable.d.ts +46 -7
- package/dist/ObjectPivotTable.d.ts.map +1 -1
- package/dist/PivotTable.d.ts +20 -3
- package/dist/PivotTable.d.ts.map +1 -1
- package/dist/RecordDetailDrawer.d.ts +8 -0
- package/dist/RecordDetailDrawer.d.ts.map +1 -1
- package/dist/WidgetConfigPanel.d.ts +2 -2
- package/dist/WidgetConfigPanel.d.ts.map +1 -1
- package/dist/WidgetEmptyState.d.ts +27 -0
- package/dist/WidgetEmptyState.d.ts.map +1 -0
- package/dist/domPassthroughPins.d.ts.map +1 -1
- package/dist/index.d.ts +22 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1733 -1570
- package/dist/index.umd.cjs +2 -2
- package/dist/legacyRetiredWidget.d.ts +5 -3
- package/dist/legacyRetiredWidget.d.ts.map +1 -1
- package/dist/measureRefusal.d.ts +50 -0
- package/dist/measureRefusal.d.ts.map +1 -0
- package/dist/recordFields.d.ts +119 -8
- package/dist/recordFields.d.ts.map +1 -1
- package/dist/retired-node-types.d.ts +29 -0
- package/dist/retired-node-types.d.ts.map +1 -0
- package/dist/useDashboardAutoRefresh.d.ts +47 -0
- package/dist/useDashboardAutoRefresh.d.ts.map +1 -0
- package/dist/utils.d.ts +36 -7
- package/dist/utils.d.ts.map +1 -1
- package/dist/widgetDispatch.d.ts +144 -18
- package/dist/widgetDispatch.d.ts.map +1 -1
- package/dist/widgetSubCaption.d.ts +40 -0
- package/dist/widgetSubCaption.d.ts.map +1 -0
- package/package.json +15 -16
package/README.md
CHANGED
|
@@ -66,30 +66,54 @@ const schema = {
|
|
|
66
66
|
### What the side-effect import registers
|
|
67
67
|
|
|
68
68
|
That single import is the whole of registration — there is no components map to
|
|
69
|
-
iterate over. Importing the entry runs the
|
|
69
|
+
iterate over. Importing the entry runs the seven live `ComponentRegistry.register(...)`
|
|
70
70
|
calls in `src/index.tsx`, which claim exactly these schema types. The keys below
|
|
71
71
|
are read off those calls:
|
|
72
72
|
|
|
73
73
|
| Namespaced key | Bare-name fallback | Renderer behind it |
|
|
74
74
|
| --- | --- | --- |
|
|
75
|
-
| `
|
|
76
|
-
| `plugin-dashboard:metric` | `
|
|
77
|
-
| `plugin-dashboard:metric-card` | `
|
|
75
|
+
| `plugin-dashboard:dashboard` | `dashboard` | `DashboardRenderer` — the widget container |
|
|
76
|
+
| `plugin-dashboard:metric` | none — `skipFallback: true` | `MetricWidget` — one KPI value |
|
|
77
|
+
| `plugin-dashboard:metric-card` | none — `skipFallback: true` | `MetricCard` — KPI with trend and icon |
|
|
78
78
|
| `plugin-dashboard:object-metric` | `object-metric` | internal wrapper around `ObjectMetricWidget` — aggregates over an object |
|
|
79
79
|
| `plugin-dashboard:pivot` | `pivot` | `PivotTable` — pivot over rows you pass in |
|
|
80
|
-
| `plugin-dashboard:object-pivot` | `object-pivot` |
|
|
81
|
-
| `plugin-dashboard:dashboard-grid` | `dashboard-grid` | `DashboardGridLayout` — the drag/resize editable grid |
|
|
80
|
+
| `plugin-dashboard:object-pivot` | `object-pivot` | `ObjectPivotTable` — a pivot queried from an object; its props go in `properties` |
|
|
82
81
|
| `plugin-dashboard:object-data-table` | `object-data-table` | `ObjectDataTable` — table queried from an object |
|
|
83
82
|
|
|
84
83
|
`ComponentRegistry.register` publishes `namespace:type`, and — unless the call
|
|
85
84
|
passes `skipFallback: true` — the bare `type` as a back-compat fallback
|
|
86
|
-
(`packages/core/src/registry/Registry.ts:194`, fallback branch at `:226`).
|
|
87
|
-
call
|
|
88
|
-
|
|
85
|
+
(`packages/core/src/registry/Registry.ts:194`, fallback branch at `:226`). Every
|
|
86
|
+
call behind the table above leaves `skipFallback` unset except `metric` and
|
|
87
|
+
`metric-card`, so each other type there resolves under both spellings.
|
|
88
|
+
|
|
89
|
+
`metric` and `metric-card` are two vocabularies that share a spelling: the
|
|
90
|
+
dashboard WIDGET types (`widgets[].type: 'metric'`, and the `metric-card` slot
|
|
91
|
+
entry) and the internal NODE keys the dashboard renders them through. The widget
|
|
92
|
+
types are unchanged. The node keys register with `skipFallback: true`
|
|
93
|
+
(objectui#10859 batch 8), and both dashboard surfaces emit the namespaced keys, so
|
|
94
|
+
only `plugin-dashboard:metric` / `plugin-dashboard:metric-card` resolve as nodes.
|
|
95
|
+
|
|
96
|
+
`dashboard-grid` is no longer registered (objectui#10859 batch 8: builder chrome,
|
|
97
|
+
with no producer and no runtime emitter). `DashboardGridLayout` is still exported;
|
|
98
|
+
mount it directly (see "DashboardGridLayout — persisting drag / resize edits"). The two `object-*` types are served by internal wrappers that
|
|
89
99
|
first resolve the spec's per-element `dataSource` binding (through
|
|
90
100
|
`ElementDataSourceGate` from `@object-ui/react`) and then render the exported
|
|
91
101
|
component, which is why those rows name a wrapper rather than an export.
|
|
92
102
|
|
|
103
|
+
⛔ One further registration is deliberately absent from the table above. Until
|
|
104
|
+
objectui#9533 the dashboard renderer was published as `view:dashboard`, while
|
|
105
|
+
`apps/console`'s lazy stubs and the CLI's known-type whitelist already spelled it
|
|
106
|
+
`plugin-dashboard:dashboard`; the bare `dashboard` key therefore declared one
|
|
107
|
+
namespace before the chunk loaded and the other after it, and the
|
|
108
|
+
`plugin-dashboard:dashboard` stub was never cleared, so that spelling could never
|
|
109
|
+
resolve. The renderer now registers under `plugin-dashboard`, and the retired
|
|
110
|
+
`view:dashboard` key is answered by a tombstone widget that refuses BY NAME and
|
|
111
|
+
names `plugin-dashboard:dashboard` as its replacement — so an authored
|
|
112
|
+
`view:dashboard` gets a visible refusal carrying its own migration, never a
|
|
113
|
+
silent fall-through. That tombstone registration passes `skipFallback: true`, so
|
|
114
|
+
it claims no bare key, and `view:dashboard` is deliberately NOT a renderable key:
|
|
115
|
+
`objectui check` reports it as unknown.
|
|
116
|
+
|
|
93
117
|
### Registering a component under your own key
|
|
94
118
|
|
|
95
119
|
To serve one of this package's components under a key of your own, register the
|
|
@@ -107,11 +131,14 @@ ComponentRegistry.register('my-metric', MetricCard, {
|
|
|
107
131
|
```
|
|
108
132
|
|
|
109
133
|
There is also a `dashboardComponents` export: the manual-integration map, keyed
|
|
110
|
-
by the **same
|
|
111
|
-
from component class names
|
|
134
|
+
by the **same seven schema types** as the table above (objectui#5064 re-keyed it
|
|
135
|
+
from component class names; since objectui#10859 batch 8 the `metric` and
|
|
136
|
+
`metric-card` entries are keyed `plugin-dashboard:metric` /
|
|
137
|
+
`plugin-dashboard:metric-card`, the type each serves, so iterating the map cannot
|
|
138
|
+
re-create their retired bare keys). Each key maps to the exact component the import
|
|
112
139
|
registers for that type — for the two `object-*` types that is the internal
|
|
113
140
|
data-source-gate wrapper, not the exported widget. Iterating it with
|
|
114
|
-
`ComponentRegistry.register(type, component)` therefore re-registers the
|
|
141
|
+
`ComponentRegistry.register(type, component)` therefore re-registers the seven
|
|
115
142
|
types the import has already claimed, which is still not the manual registration
|
|
116
143
|
above: each such call passes no `meta`, so it trips the no-namespace deprecation
|
|
117
144
|
warning in `register` (`packages/core/src/registry/Registry.ts:198`) and
|
|
@@ -126,32 +153,90 @@ whole of registration.
|
|
|
126
153
|
Container for dashboard widgets:
|
|
127
154
|
|
|
128
155
|
```typescript
|
|
129
|
-
{
|
|
156
|
+
import type { DashboardComponentSchema, DashboardWidgetSchema } from '@object-ui/types';
|
|
157
|
+
|
|
158
|
+
declare const widgets: DashboardWidgetSchema[];
|
|
159
|
+
|
|
160
|
+
const schema: DashboardComponentSchema = {
|
|
130
161
|
type: 'dashboard',
|
|
131
|
-
widgets
|
|
132
|
-
columns
|
|
133
|
-
gap
|
|
134
|
-
className
|
|
135
|
-
}
|
|
162
|
+
widgets,
|
|
163
|
+
columns: 3, // Grid columns (default: 3)
|
|
164
|
+
gap: 4, // Gap between widgets
|
|
165
|
+
className: 'w-full',
|
|
166
|
+
};
|
|
136
167
|
```
|
|
137
168
|
|
|
169
|
+
`type` and `widgets` are the required keys; every other key above is optional.
|
|
170
|
+
|
|
138
171
|
### Metric Card
|
|
139
172
|
|
|
140
|
-
Display a single metric or KPI:
|
|
173
|
+
Display a single metric or KPI as a dashboard widget-slot entry:
|
|
141
174
|
|
|
142
175
|
```typescript
|
|
143
|
-
{
|
|
176
|
+
import type { ComponentProps } from 'react';
|
|
177
|
+
import { MetricCard } from '@object-ui/plugin-dashboard';
|
|
178
|
+
|
|
179
|
+
// A `metric-card` entry sits directly in a dashboard's `widgets[]` — the closed
|
|
180
|
+
// component slot of the 2026-08-14 ruling. Its keys besides `type` are
|
|
181
|
+
// `MetricCard`'s own props. That props interface is not on this package's export
|
|
182
|
+
// surface (see "TypeScript Support" below), so they are read off the shipped
|
|
183
|
+
// component rather than restated here — a renamed or retyped prop stops this
|
|
184
|
+
// block compiling.
|
|
185
|
+
type MetricCardEntry = { type: 'metric-card' } & ComponentProps<typeof MetricCard>;
|
|
186
|
+
|
|
187
|
+
const card: MetricCardEntry = {
|
|
144
188
|
type: 'metric-card',
|
|
145
|
-
title:
|
|
146
|
-
value:
|
|
147
|
-
icon
|
|
148
|
-
trend
|
|
149
|
-
trendValue
|
|
150
|
-
description
|
|
151
|
-
className
|
|
152
|
-
}
|
|
189
|
+
title: 'Total Sales',
|
|
190
|
+
value: '$123,456',
|
|
191
|
+
icon: 'trending-up', // Lucide icon name
|
|
192
|
+
trend: 'up', // 'up' | 'down' | 'neutral'
|
|
193
|
+
trendValue: '+12%',
|
|
194
|
+
description: 'vs last month',
|
|
195
|
+
className: 'col-span-2',
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const schema = { type: 'dashboard', widgets: [card] };
|
|
153
199
|
```
|
|
154
200
|
|
|
201
|
+
The dashboard renders the entry as the `plugin-dashboard:metric-card` node. The
|
|
202
|
+
bare `metric-card` NODE key is retired (objectui#10859), so a `metric-card`
|
|
203
|
+
outside a dashboard's `widgets[]` renders the "Unknown component type" panel.
|
|
204
|
+
|
|
205
|
+
`value` is the only required key. `title` and `description` take a plain string
|
|
206
|
+
or the spec's inline per-locale map (`I18nLabel`).
|
|
207
|
+
|
|
208
|
+
A card with no `value` is refused by `@object-ui/types/zod`, on the tolerant
|
|
209
|
+
face and the strict one. `metric-card` is not a widget type, so a card is never
|
|
210
|
+
read through widget keys alone, and no widget key stands in for `value`: a
|
|
211
|
+
`dataset` makes the dashboard draw its dataset tile in the card's place, and
|
|
212
|
+
`options` is not a binding (objectui#11483). For a figure queried from a
|
|
213
|
+
dataset, write a `metric` widget with `dataset` and `values` (see "TypeScript
|
|
214
|
+
Support" below).
|
|
215
|
+
|
|
216
|
+
#### Percent `format` patterns (`'0%'`, `'0.00%'`)
|
|
217
|
+
|
|
218
|
+
A numeral pattern ending in `%` is handed whole to `formatPercent`
|
|
219
|
+
(`@object-ui/fields`), the same call the list-view percent cell and this
|
|
220
|
+
package's own record-field renderer already make. Two consequences, both of
|
|
221
|
+
them shared with every other percent surface in the console rather than decided
|
|
222
|
+
by the tile:
|
|
223
|
+
|
|
224
|
+
- **Magnitude** is read at a STATED storage, never guessed from the value. A
|
|
225
|
+
`metric` tile holds no field, only a value and a pattern, and numeral's `%`
|
|
226
|
+
multiplies by 100, so the pattern states a fraction: `0.25` reads `25%`, `1`
|
|
227
|
+
reads `100%`, `-0.05` reads `-5%`. An `object-metric` tile over a `percent`
|
|
228
|
+
field renders at that field's own storage instead, the one the list cell
|
|
229
|
+
reads (`percentScaleOf` in `@objectstack/spec/data`: a fraction unless the
|
|
230
|
+
field declares a `max` above 1), so a field declaring `max: 100` that
|
|
231
|
+
averages `50` reads `50%`.
|
|
232
|
+
- **The percent sign is the locale's**, not a literal `%`: a `de-DE` session
|
|
233
|
+
gets the no-break space German writes before the sign, and grouping follows
|
|
234
|
+
the locale (a fraction of `12.5` reads `1,250%` in `en`).
|
|
235
|
+
|
|
236
|
+
The pattern's decimal count still belongs to the tile — `'0.00%'` renders two
|
|
237
|
+
decimals — because that is an author declaration on the widget rather than a
|
|
238
|
+
guess about the value.
|
|
239
|
+
|
|
155
240
|
## Examples
|
|
156
241
|
|
|
157
242
|
### Basic Dashboard
|
|
@@ -205,26 +290,35 @@ const schema = {
|
|
|
205
290
|
value: '$123,456'
|
|
206
291
|
},
|
|
207
292
|
{
|
|
208
|
-
|
|
293
|
+
id: 'sales_trend',
|
|
294
|
+
type: 'line',
|
|
209
295
|
title: 'Sales Trend',
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
height: 300
|
|
214
|
-
}
|
|
296
|
+
dataset: 'sales',
|
|
297
|
+
dimensions: ['month'],
|
|
298
|
+
values: ['revenue']
|
|
215
299
|
},
|
|
216
300
|
{
|
|
217
|
-
|
|
301
|
+
id: 'revenue_by_category',
|
|
302
|
+
type: 'pie',
|
|
218
303
|
title: 'Category Distribution',
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
304
|
+
dataset: 'sales',
|
|
305
|
+
dimensions: ['category'],
|
|
306
|
+
values: ['revenue']
|
|
223
307
|
}
|
|
224
308
|
]
|
|
225
309
|
};
|
|
226
310
|
```
|
|
227
311
|
|
|
312
|
+
A chart widget names its family in `type` — one of the spec's chart families,
|
|
313
|
+
the closed vocabulary `DashboardWidgetTypeName` declares — and binds a
|
|
314
|
+
`dataset` (ADR-0021), selecting the dimension it plots in `dimensions` and its
|
|
315
|
+
measures in `values`. It never carries rows: `options.data`,
|
|
316
|
+
`options.xField` and `options.yField` are not widget keys, and
|
|
317
|
+
`@object-ui/types/zod`'s `StrictAnyComponentSchema` refuses them by name
|
|
318
|
+
(objectui#11228). There is no `card` widget family and no nested `body` slot: a
|
|
319
|
+
widget whose `type` is outside that vocabulary is refused at validation, by
|
|
320
|
+
`@object-ui/types/zod`'s `DashboardComponentSchema`.
|
|
321
|
+
|
|
228
322
|
### Responsive Dashboard
|
|
229
323
|
|
|
230
324
|
```typescript
|
|
@@ -239,30 +333,68 @@ const schema = {
|
|
|
239
333
|
|
|
240
334
|
## Integration with Data Sources
|
|
241
335
|
|
|
242
|
-
Connect dashboard to live data:
|
|
336
|
+
Connect dashboard to live data. `metric-card` renders the `value` it is handed:
|
|
337
|
+
it has no row in the spec's expression carriage map, so a `${…}` written in
|
|
338
|
+
`value` or `trend` reaches the screen as those characters. A widget that should
|
|
339
|
+
read live data is one of the `object-*` types above — they resolve the spec's
|
|
340
|
+
per-element `dataSource` binding and query the object themselves.
|
|
243
341
|
|
|
244
|
-
|
|
245
|
-
|
|
342
|
+
**The adapter is not a schema key.** A schema is a serialisable document; a live
|
|
343
|
+
adapter is an object with methods, so it cannot travel in one. An `object-*`
|
|
344
|
+
widget reads its adapter from React context — `useContext(SchemaRendererContext)`
|
|
345
|
+
at `src/ObjectMetricWidget.tsx:159`, with an explicit `dataSource` prop taking
|
|
346
|
+
precedence when the host renders the widget directly.
|
|
246
347
|
|
|
247
|
-
|
|
248
|
-
baseUrl: 'https://api.example.com',
|
|
249
|
-
token: 'your-auth-token'
|
|
250
|
-
});
|
|
348
|
+
So the document stays plain data — every value in it survives `JSON.stringify`:
|
|
251
349
|
|
|
252
|
-
|
|
350
|
+
```typescript
|
|
351
|
+
import type { DashboardComponentSchema } from '@object-ui/types';
|
|
352
|
+
|
|
353
|
+
const schema: DashboardComponentSchema = {
|
|
253
354
|
type: 'dashboard',
|
|
254
|
-
dataSource,
|
|
255
355
|
widgets: [
|
|
256
356
|
{
|
|
257
357
|
type: 'metric-card',
|
|
258
358
|
title: 'Total Users',
|
|
259
|
-
value:
|
|
260
|
-
trend: '
|
|
359
|
+
value: 12480,
|
|
360
|
+
trend: 'up'
|
|
261
361
|
}
|
|
262
362
|
]
|
|
263
363
|
};
|
|
264
364
|
```
|
|
265
365
|
|
|
366
|
+
The adapter is installed once, above the whole tree, and every `object-*` widget
|
|
367
|
+
underneath reads it from context:
|
|
368
|
+
|
|
369
|
+
```tsx
|
|
370
|
+
import { SchemaRendererProvider, SchemaRenderer } from '@object-ui/react';
|
|
371
|
+
import { createObjectStackAdapter } from '@object-ui/data-objectstack';
|
|
372
|
+
import '@object-ui/plugin-dashboard';
|
|
373
|
+
import type { DashboardComponentSchema } from '@object-ui/types';
|
|
374
|
+
|
|
375
|
+
// The document from the block above.
|
|
376
|
+
declare const schema: DashboardComponentSchema;
|
|
377
|
+
|
|
378
|
+
const dataSource = createObjectStackAdapter({
|
|
379
|
+
baseUrl: 'https://api.example.com',
|
|
380
|
+
token: 'your-auth-token'
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
export const App = () => (
|
|
384
|
+
<SchemaRendererProvider dataSource={dataSource}>
|
|
385
|
+
<SchemaRenderer schema={schema} />
|
|
386
|
+
</SchemaRendererProvider>
|
|
387
|
+
);
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
> A `dataSource` key **does** mean something on a schema node, but it is not this:
|
|
391
|
+
> it is the spec's element **binding** (`PageComponentSchema.dataSource`) — a
|
|
392
|
+
> declarative descriptor such as `{ object: 'orders', view: 'my_view' }`, resolved
|
|
393
|
+
> against the host and mapped onto the widget's own keys by the `object-*`
|
|
394
|
+
> registry shells in `src/index.tsx`. It belongs on the widget that reads it, not
|
|
395
|
+
> on the dashboard node, and a live adapter written into that slot wires nothing
|
|
396
|
+
> up — it is a different kind of thing wearing the same name.
|
|
397
|
+
|
|
266
398
|
## Dashboard-level filters
|
|
267
399
|
|
|
268
400
|
A dashboard can declare top-level filters — a date range and any number of
|
|
@@ -286,16 +418,21 @@ into each bound widget's inline query (`AND`-combined with the widget's own
|
|
|
286
418
|
"name": "region", // stable filter name (defaults to field)
|
|
287
419
|
"field": "region", // default binding target
|
|
288
420
|
"label": "Region",
|
|
421
|
+
// optional "object": the object `field` lives on — its fields.<object>.<field> / fieldOptions.<object>.<field>.<value> bundle entries then win, "label" is the fallback
|
|
289
422
|
"type": "select", // text | select | date | number | lookup
|
|
290
423
|
// Canonical @objectstack/spec pair form — the only form the platform
|
|
291
|
-
// accepts at publish
|
|
292
|
-
//
|
|
424
|
+
// accepts at publish, and the only form `@object-ui/types` validates
|
|
425
|
+
// (objectui#7759). The bare-string shorthand (["EMEA", …]) is NOT
|
|
426
|
+
// accepted: the runtime no longer lifts it (objectui#4356) — a bare member
|
|
427
|
+
// yields no option, and a dev-mode warning names it. Rewrite each X as
|
|
428
|
+
// { "value": X, "label": X }.
|
|
293
429
|
"options": [
|
|
294
430
|
{ "value": "EMEA", "label": "EMEA" },
|
|
295
431
|
{ "value": "APAC", "label": "APAC" },
|
|
296
432
|
{ "value": "AMER", "label": "AMER" }
|
|
297
433
|
]
|
|
298
|
-
// or dynamic: "optionsFrom": { "object": "accounts", "valueField": "region" }
|
|
434
|
+
// or dynamic: "optionsFrom": { "object": "accounts", "valueField": "region", "labelField": "region" }
|
|
435
|
+
// (`labelField` is required by the spec, even when it names the value field)
|
|
299
436
|
}
|
|
300
437
|
],
|
|
301
438
|
"widgets": [
|
|
@@ -303,9 +440,13 @@ into each bound widget's inline query (`AND`-combined with the widget's own
|
|
|
303
440
|
// dimensions/measures by name. The pre-ADR-0021 top-level `object` +
|
|
304
441
|
// `categoryField`/`valueField`/`aggregate` shape was REMOVED — a widget
|
|
305
442
|
// still carrying it renders "This widget uses a retired data format.
|
|
306
|
-
// Edit it to bind a dataset." instead of a chart.
|
|
307
|
-
//
|
|
308
|
-
//
|
|
443
|
+
// Edit it to bind a dataset." instead of a chart. Inline widget data
|
|
444
|
+
// (`options.data`, `options.xField` / `options.yField`) is not an
|
|
445
|
+
// authoring surface either: the strict authoring face refuses those keys
|
|
446
|
+
// by name (objectui#11228). A stored single-value widget (`metric`,
|
|
447
|
+
// `gauge`, `solid-gauge`, `kpi`, `bullet`, or one with no `type`) whose
|
|
448
|
+
// `options.data` is a `{ "provider": "object", … }` query draws that same
|
|
449
|
+
// retired-format prompt instead of its number (objectui#11525).
|
|
309
450
|
//
|
|
310
451
|
// Default binding: the filter's own `field` (dateRange → created_at).
|
|
311
452
|
{ "id": "w1", "type": "bar", "dataset": "invoices", "dimensions": ["status"], "values": ["count"] },
|
|
@@ -338,8 +479,6 @@ Notes:
|
|
|
338
479
|
query time, so widgets resolve them exactly like hand-authored filters.
|
|
339
480
|
- Dataset-bound widgets receive the merged filter through the dataset
|
|
340
481
|
query's `runtimeFilter`.
|
|
341
|
-
- Static-data widgets (inline `data` arrays) have no query to scope and are
|
|
342
|
-
not filtered.
|
|
343
482
|
- Filter values are also readable in widget expressions as `page.<name>`
|
|
344
483
|
(e.g. `page.region`), since they are hosted as dashboard variables.
|
|
345
484
|
- `optionsFrom` resolves distinct option values server-side (a dataset
|
|
@@ -358,7 +497,9 @@ A KPI widget can declare a semantic accent. The vocabulary is
|
|
|
358
497
|
tokens the designer's swatch picker offers:
|
|
359
498
|
|
|
360
499
|
```typescript
|
|
361
|
-
{
|
|
500
|
+
import type { DashboardWidgetSchema } from '@object-ui/types';
|
|
501
|
+
|
|
502
|
+
const atRisk: DashboardWidgetSchema = {
|
|
362
503
|
id: 'kpi_at_risk',
|
|
363
504
|
type: 'metric',
|
|
364
505
|
title: 'At-Risk Projects',
|
|
@@ -366,14 +507,14 @@ tokens the designer's swatch picker offers:
|
|
|
366
507
|
values: ['project_count'],
|
|
367
508
|
filter: { health: 'red' },
|
|
368
509
|
colorVariant: 'danger', // tints the value; card chrome stays neutral
|
|
369
|
-
}
|
|
510
|
+
};
|
|
370
511
|
```
|
|
371
512
|
|
|
372
513
|
Where the accent lands depends on the layout, not on the token:
|
|
373
514
|
|
|
374
515
|
| Layout | Accent |
|
|
375
516
|
| --- | --- |
|
|
376
|
-
| Card chrome (`MetricWidget`,
|
|
517
|
+
| Card chrome (`MetricWidget`, an `object-metric` block) | the icon chip's background + foreground |
|
|
377
518
|
| Chrome-less (`MetricWidget variant: 'bare'`, and every dataset-bound `metric`) | the big number's text colour |
|
|
378
519
|
|
|
379
520
|
Both read one shared table (`src/colorVariants.ts`), so the same declaration
|
|
@@ -383,6 +524,74 @@ the enum gets no accent and is not aliased to a nearby colour: it is invalid
|
|
|
383
524
|
metadata, rejected where it is authored and published rather than reinterpreted
|
|
384
525
|
here.
|
|
385
526
|
|
|
527
|
+
## A widget with no `type`, or a `type` that names no family
|
|
528
|
+
|
|
529
|
+
A widget that declares no `type` is a `metric` widget. `@objectstack/spec`'s
|
|
530
|
+
`DashboardWidget.type` defaults to `metric`, and both dashboard surfaces
|
|
531
|
+
(`DashboardRenderer` and `DashboardGridLayout`) read that default from the spec,
|
|
532
|
+
so the widget draws exactly as the same widget with `type: 'metric'` does,
|
|
533
|
+
inline or bound to a dataset (objectui#11514). objectui's validator accepts the
|
|
534
|
+
widget without a `type` and does not write the default in, so the surfaces are
|
|
535
|
+
where it resolves.
|
|
536
|
+
|
|
537
|
+
objectui's legacy `component` envelope (`{ id, component, layout }`) is not the
|
|
538
|
+
spec's widget, and the default is not applied to it: an envelope with no `type`
|
|
539
|
+
draws its `component` under its card heading, as it always did. One node is
|
|
540
|
+
retired there: an `object-metric` (under `object-metric` or
|
|
541
|
+
`plugin-dashboard:object-metric`) draws the same retired-format prompt as a
|
|
542
|
+
dataset-less metric widget and sends no query (objectui#11466). Bind a metric
|
|
543
|
+
widget to a dataset instead. Every other envelope node draws as written, and the
|
|
544
|
+
filter bar still scopes an envelope's `object-chart` and `object-data-table`.
|
|
545
|
+
|
|
546
|
+
A `type` that names no widget family and no component type (a typo, or a family
|
|
547
|
+
the spec no longer has) is refused by both validator faces at `type`. A stored
|
|
548
|
+
one draws the labelled placeholder "「type」chart type is not supported yet", as
|
|
549
|
+
a known family with no renderer (`heatmap`) does, instead of the renderer's red
|
|
550
|
+
"Unknown component type" panel.
|
|
551
|
+
|
|
552
|
+
## How many measures a widget renders
|
|
553
|
+
|
|
554
|
+
A dataset-bound widget queries every measure in `values`. What it renders
|
|
555
|
+
depends on its shape:
|
|
556
|
+
|
|
557
|
+
- **A metric-family widget** (`metric`, `kpi`, `gauge`, `solid-gauge`,
|
|
558
|
+
`bullet`) is a one-number tile. It takes exactly one measure, and
|
|
559
|
+
`@objectstack/spec` refuses a second one at its door; several numbers is a
|
|
560
|
+
different visual.
|
|
561
|
+
- **A widget with no `dimensions` and several measures** renders every one of
|
|
562
|
+
them when its type is `table` / `pivot` (one row of measures) or a chart of
|
|
563
|
+
the bar family (`bar`, `column`, `horizontal-bar`) or `line` / `area` /
|
|
564
|
+
`combo` (one mark per measure, the measures' labels on the category axis,
|
|
565
|
+
which runs down the side on a `horizontal-bar`).
|
|
566
|
+
- **Any other dimensionless widget with several measures** still renders the
|
|
567
|
+
first one as a tile.
|
|
568
|
+
- **A `pie`, `donut`, `funnel`, `treemap` or `sankey` with a dimension** draws
|
|
569
|
+
one series, the first measure, however many it declares: the shared chart
|
|
570
|
+
renderer reads the first series only on those families.
|
|
571
|
+
|
|
572
|
+
When a widget keeps a declared measure off the screen, `DatasetWidget` logs
|
|
573
|
+
one console warning naming the widget, the measures it renders, and the ones it
|
|
574
|
+
queried and never displayed. Three shapes do that. Two take the tile: a metric
|
|
575
|
+
tile stored before the spec narrowed, and a widget the third bullet above
|
|
576
|
+
covers (no `dimensions`, several measures, and a type the second bullet does not
|
|
577
|
+
list, so a dimensionless `pie` is one). The third is a `pie`, `donut`,
|
|
578
|
+
`funnel`, `treemap` or `sankey` with a dimension and several measures
|
|
579
|
+
(objectui#11417). For a tile the warning
|
|
580
|
+
then names the spec's ADR-0087 entry
|
|
581
|
+
`dashboard-widget-metric-family-multi-measure-refused`, whose `replacement`
|
|
582
|
+
says what to author instead of a one-number tile with several measures. For
|
|
583
|
+
one of those five charts it says that the chart family draws a single series,
|
|
584
|
+
the first declared measure, and points to no entry: that one answers for a
|
|
585
|
+
tile. Nothing about what is drawn changes. Every door accepts the chart shape
|
|
586
|
+
today; the spec is to refuse it (objectstack#21293).
|
|
587
|
+
The widget panel (`WidgetConfigPanel`) asks the same door the spec runs: it
|
|
588
|
+
offers no further measure the door would refuse, and shows the door's own
|
|
589
|
+
message under measures it already refuses.
|
|
590
|
+
|
|
591
|
+
The behaviour is pinned by `src/__tests__/DatasetWidget.unrenderedMeasures-8894.test.tsx`,
|
|
592
|
+
`src/__tests__/DatasetWidget.dimensionlessMeasures-11261.test.tsx` and
|
|
593
|
+
`src/__tests__/WidgetConfigPanel.measureDoor-8894.test.tsx`.
|
|
594
|
+
|
|
386
595
|
## TypeScript Support
|
|
387
596
|
|
|
388
597
|
This package ships **components, not schema types** — its whole type export
|
|
@@ -392,14 +601,16 @@ The authored shape is typed by `@object-ui/types`:
|
|
|
392
601
|
|
|
393
602
|
| Import from `@object-ui/types` | What it types |
|
|
394
603
|
| --- | --- |
|
|
395
|
-
| `DashboardComponentSchema` | the whole `type: 'dashboard'` node — `columns`, `gap`, `widgets`, `header`, `globalFilters`, `dateRange`, `
|
|
604
|
+
| `DashboardComponentSchema` | the whole `type: 'dashboard'` node — `columns`, `gap`, `widgets`, `header`, `globalFilters`, `dateRange`, `refreshIntervalSeconds`, … |
|
|
396
605
|
| `DashboardWidgetSchema` | one entry of `widgets[]` — the spec's `DashboardWidget` keys, plus objectui's own (`component`, `layout`, `options`, …) |
|
|
606
|
+
| `DashboardWidgetSlotComponentSchema` | the other kind of `widgets[]` entry — a component node placed directly in the slot, `type` one of the closed component set (`metric-card`); its other keys are that component's own props, which it declares (`title`, `value`, `icon`, `trend`, `trendValue`, and `description` from `BaseSchema`), except the spec's widget `layout`, which places the node |
|
|
397
607
|
| `DashboardWidgetLayout` | a widget's `{ x, y, w, h }` grid box |
|
|
398
608
|
|
|
399
609
|
```typescript
|
|
400
610
|
import type {
|
|
401
611
|
DashboardComponentSchema,
|
|
402
612
|
DashboardWidgetSchema,
|
|
613
|
+
DashboardWidgetSlotComponentSchema,
|
|
403
614
|
} from '@object-ui/types';
|
|
404
615
|
|
|
405
616
|
// Dataset-bound KPI — the widget vocabulary (see "Dashboard-level filters").
|
|
@@ -421,7 +632,8 @@ const users: DashboardWidgetSchema = {
|
|
|
421
632
|
};
|
|
422
633
|
|
|
423
634
|
// Component format: a registered component node in the widget's `component`
|
|
424
|
-
// slot. Its keys are that COMPONENT's props, not widget keys
|
|
635
|
+
// slot. Its keys are that COMPONENT's props, not widget keys; a `metric-card`
|
|
636
|
+
// here is typed by the same arm as `kpi` below.
|
|
425
637
|
const custom: DashboardWidgetSchema = {
|
|
426
638
|
id: 'kpi_custom',
|
|
427
639
|
component: {
|
|
@@ -434,11 +646,23 @@ const custom: DashboardWidgetSchema = {
|
|
|
434
646
|
layout: { x: 0, y: 0, w: 3, h: 2 },
|
|
435
647
|
};
|
|
436
648
|
|
|
649
|
+
// Component node directly in the slot — the shape every `metric-card` example
|
|
650
|
+
// above uses. `type` is one of the closed component set; the other keys are
|
|
651
|
+
// the component's own props, which `DashboardWidgetSlotComponentSchema`
|
|
652
|
+
// declares.
|
|
653
|
+
const kpi: DashboardWidgetSlotComponentSchema = {
|
|
654
|
+
type: 'metric-card',
|
|
655
|
+
title: 'Revenue',
|
|
656
|
+
value: '$123,456',
|
|
657
|
+
trend: 'up',
|
|
658
|
+
trendValue: '+12%',
|
|
659
|
+
};
|
|
660
|
+
|
|
437
661
|
const dashboard: DashboardComponentSchema = {
|
|
438
662
|
type: 'dashboard',
|
|
439
663
|
columns: 3,
|
|
440
664
|
gap: 4,
|
|
441
|
-
widgets: [revenue, users, custom],
|
|
665
|
+
widgets: [revenue, users, custom, kpi],
|
|
442
666
|
};
|
|
443
667
|
```
|
|
444
668
|
|
|
@@ -447,8 +671,65 @@ covers every `type` (`metric`, `bar`, `table`, …) and the family-specific
|
|
|
447
671
|
settings live under `options`. `MetricCard`'s own props — `value`, `trend`,
|
|
448
672
|
`trendValue` — are the component's, not the widget's: `DashboardWidgetSchema`
|
|
449
673
|
declares none of them, and the component's props interface is not on this
|
|
450
|
-
package's export surface either.
|
|
451
|
-
|
|
674
|
+
package's export surface either. A `metric-card` node is typed as a COMPONENT
|
|
675
|
+
node in both places it can appear: in a widget's `component` slot (`custom`
|
|
676
|
+
above) and directly in `widgets[]` (`kpi` above, `DashboardWidgetSlotComponentSchema`
|
|
677
|
+
— the component arm of `DashboardComponentSchema['widgets']`, first in the
|
|
678
|
+
declaration as in the zod schema's two-arm slot). Either way its keys are
|
|
679
|
+
checked against that arm, which declares the card's registered inputs, never
|
|
680
|
+
as widget keys:
|
|
681
|
+
|
|
682
|
+
| Key | Type |
|
|
683
|
+
| --- | --- |
|
|
684
|
+
| `value` | `string \| number` — required |
|
|
685
|
+
| `title` | `string` or an inline per-locale map (`I18nLabel`) |
|
|
686
|
+
| `icon` | `string` — a Lucide icon name |
|
|
687
|
+
| `trend` | `'up' \| 'down' \| 'neutral'` — drawn with `trendValue` |
|
|
688
|
+
| `trendValue` | `string` — drawn with `trend` |
|
|
689
|
+
| `description` | `string` or `I18nLabel`, from `BaseSchema` |
|
|
690
|
+
|
|
691
|
+
`@object-ui/types/zod` judges the same members, so `objectui validate` refuses
|
|
692
|
+
a card directly in `widgets[]` whose `trend` is outside the three, or whose
|
|
693
|
+
`value` is neither a string nor a number.
|
|
694
|
+
|
|
695
|
+
### Reading a widget key off `widgets[]`
|
|
696
|
+
|
|
697
|
+
The widget keys (`colorVariant`, `filter`, `dataset`, …) are declared on the
|
|
698
|
+
widget arm, `DashboardWidgetSchema`, which takes them from the spec's
|
|
699
|
+
`DashboardWidget` row. The component arm declares none of them. Read straight
|
|
700
|
+
off a `widgets[]` entry, such a key is typed `any`, supplied by the component
|
|
701
|
+
arm's passthrough. This package's own readers take an entry by the slot's
|
|
702
|
+
element type, `DashboardComponentSchema['widgets'][number]`. The component arm
|
|
703
|
+
is not assignable to `DashboardWidgetSchema`, whose `type` names no component
|
|
704
|
+
type, so narrow an entry on `type` first: `metric-card` is the one component
|
|
705
|
+
type the slot holds, and any other entry is the widget arm. The compiler checks
|
|
706
|
+
the narrowing, not an annotation, and the key gets its declared type:
|
|
707
|
+
|
|
708
|
+
```typescript
|
|
709
|
+
import type { DashboardComponentSchema } from '@object-ui/types';
|
|
710
|
+
|
|
711
|
+
declare const dashboard: DashboardComponentSchema;
|
|
712
|
+
|
|
713
|
+
const accents = dashboard.widgets.map((w) =>
|
|
714
|
+
w.type === 'metric-card' ? 'default' : (w.colorVariant ?? 'default'),
|
|
715
|
+
);
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
`title` and `layout` are the two widget keys both arms declare. `title` is the
|
|
719
|
+
card's heading on the component arm, typed as `MetricCard` reads it, so it reads
|
|
720
|
+
as a string or an `I18nLabel` straight off a `widgets[]` entry. The component
|
|
721
|
+
arm takes the spec's widget `layout` by reference, because Save Layout writes it
|
|
722
|
+
onto every entry of `widgets[]`, a `metric-card` node included (see
|
|
723
|
+
[DashboardGridLayout](#dashboardgridlayout--persisting-drag--resize-edits)). So
|
|
724
|
+
`layout` reads with the spec's type straight off a `widgets[]` entry, and the
|
|
725
|
+
strict authoring face accepts it on a component node. A malformed one (not
|
|
726
|
+
four numbers, or a key besides `x`, `y`, `w` and `h`) is refused on either arm.
|
|
727
|
+
|
|
728
|
+
A widget with no `layout` is auto-placed by the grid. `DashboardWithConfig`'s
|
|
729
|
+
width and height sliders write the whole box: the dimension a slider does not
|
|
730
|
+
edit, and `x` and `y`, come from where the grid auto-places that widget
|
|
731
|
+
(`completeWidgetLayout` and `defaultWidgetPlacement` from `@object-ui/types`,
|
|
732
|
+
objectui#11388), so a one-dimension edit never stores a box the spec refuses.
|
|
452
733
|
|
|
453
734
|
## Customization
|
|
454
735
|
|
|
@@ -456,10 +737,15 @@ All components support Tailwind CSS classes:
|
|
|
456
737
|
|
|
457
738
|
```typescript
|
|
458
739
|
const schema = {
|
|
459
|
-
type: '
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
740
|
+
type: 'dashboard',
|
|
741
|
+
widgets: [
|
|
742
|
+
{
|
|
743
|
+
type: 'metric-card',
|
|
744
|
+
title: 'Custom Metric',
|
|
745
|
+
value: '100',
|
|
746
|
+
className: 'bg-gradient-to-r from-blue-500 to-purple-600 text-white'
|
|
747
|
+
}
|
|
748
|
+
]
|
|
463
749
|
};
|
|
464
750
|
```
|
|
465
751
|
|
|
@@ -484,10 +770,29 @@ object schema once and infers the renderer from the bound field:
|
|
|
484
770
|
| `date` / `datetime` | Locale-formatted date |
|
|
485
771
|
| `currency` | Locale currency (or honour `format: '$0,0'`) |
|
|
486
772
|
| `percent` | `0%` / `0.0%` formatted (honour `format`) |
|
|
773
|
+
| `password` / `secret` | `••••••`, and the column is flagged `masked` (see below) |
|
|
487
774
|
|
|
488
775
|
Author overrides always win — pass `type`, `format`, `options`,
|
|
489
|
-
`
|
|
490
|
-
auto-detection.
|
|
776
|
+
`currency`, or your own `cell` function on a column to bypass
|
|
777
|
+
auto-detection. An explicit `currency` (ISO 4217 code, e.g. `"EUR"`)
|
|
778
|
+
wins over both the symbol inferred from `format` and the tenant default
|
|
779
|
+
currency. A lookup column's related-object target always comes from the
|
|
780
|
+
object schema's own field definition — there is no column-level override
|
|
781
|
+
for it (objectui#6597: measured no authoring story for one).
|
|
782
|
+
|
|
783
|
+
A masked column (objectui#10657) is one the table withholds: no Ctrl+C / Cmd+C
|
|
784
|
+
copy, no tooltip, no CSV export column, no match in the search box, no header
|
|
785
|
+
sort, and a width sized from its header rather than its values. The widget
|
|
786
|
+
decides it with `isMaskedFieldType()` from `@object-ui/fields`, over the
|
|
787
|
+
column's authored `type` and the object's field type, so this is the one place
|
|
788
|
+
an author override does not win: `type: 'text'` over a `secret` field keeps the
|
|
789
|
+
flag, though the cell then draws the value as text. Rows handed in as `data`
|
|
790
|
+
or through `bind` can be drawn before the object schema arrives, and a failed
|
|
791
|
+
schema read never delivers one; in that window the widget flags every column,
|
|
792
|
+
and a column with no `type` of its own draws as the mask, never as text, until
|
|
793
|
+
the schema arrives, or for good when the read failed (objectui#10657). A column
|
|
794
|
+
that authors its `type` draws from it meanwhile. The record drawer a row opens
|
|
795
|
+
(record drill-down) draws every value as the mask in that window too.
|
|
491
796
|
|
|
492
797
|
```jsonc
|
|
493
798
|
{
|
|
@@ -518,25 +823,61 @@ calls `onWidgetsReorder(nextWidgets)` with the reordered array; the host (e.g.
|
|
|
518
823
|
A 5px pointer-activation distance keeps click-to-select working on the same
|
|
519
824
|
widget surface.
|
|
520
825
|
|
|
826
|
+
## DashboardRenderer — a document the server already translated (`localized`)
|
|
827
|
+
|
|
828
|
+
A dashboard read from ObjectStack's `/meta` route arrives translated for the
|
|
829
|
+
request's locale. The server resolves the packaged translation catalog, and it
|
|
830
|
+
keeps a published edit over that catalog: an explicit override beats the
|
|
831
|
+
packaged default. Pass `localized={true}` when your document came from such a
|
|
832
|
+
read. The renderer then draws these texts as given:
|
|
833
|
+
|
|
834
|
+
- the widget `title`, `description` and sub-caption (`options.description`);
|
|
835
|
+
- its own header `label` and `description`.
|
|
836
|
+
|
|
837
|
+
An inline per-locale map is still collapsed to the active language. The client
|
|
838
|
+
bundle is not consulted again. A second lookup would let the packaged catalog
|
|
839
|
+
win back over a published edit (objectui#11295). The console's dashboard page
|
|
840
|
+
(`DashboardView`) sets the prop.
|
|
841
|
+
|
|
842
|
+
Leave it unset for a document the server never translated: an inline block, a
|
|
843
|
+
preview or a design surface. The bundle keys under
|
|
844
|
+
`dashboards.<name>.widgets.<id>.*` are then that document's one translation
|
|
845
|
+
pass, as before. Header-action labels always go through the bundle, because the
|
|
846
|
+
server does not translate them.
|
|
847
|
+
|
|
521
848
|
## DashboardGridLayout — persisting drag / resize edits
|
|
522
849
|
|
|
523
|
-
`DashboardGridLayout` (
|
|
850
|
+
`DashboardGridLayout` (a React component you mount directly; its old schema
|
|
851
|
+
`type: 'dashboard-grid'` was retired by objectui#10859) has an
|
|
524
852
|
inline **"Edit Layout"** mode that lets users drag and resize widgets via
|
|
525
853
|
`react-grid-layout`. When the user clicks **Save Layout**, the new grid
|
|
526
854
|
coordinates are merged back into `schema.widgets[].layout` and handed off
|
|
527
855
|
through the `onSchemaChange` callback.
|
|
528
856
|
|
|
529
857
|
```tsx
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
858
|
+
import { DashboardGridLayout } from '@object-ui/plugin-dashboard';
|
|
859
|
+
import type { ObjectStackAdapter } from '@object-ui/data-objectstack';
|
|
860
|
+
import type { DashboardComponentSchema } from '@object-ui/types';
|
|
861
|
+
|
|
862
|
+
// `dashboard` is the node being edited; `adapter` is host-provided.
|
|
863
|
+
declare const dashboard: DashboardComponentSchema;
|
|
864
|
+
declare const adapter: ObjectStackAdapter;
|
|
865
|
+
|
|
866
|
+
function DashboardEditor() {
|
|
867
|
+
const client = adapter.getClient();
|
|
868
|
+
return (
|
|
869
|
+
<DashboardGridLayout
|
|
870
|
+
schema={dashboard}
|
|
871
|
+
// ✅ Preferred — write the updated schema through your data adapter.
|
|
872
|
+
onSchemaChange={(next) => {
|
|
873
|
+
// `name` is optional on the schema, and `saveItem` requires it — decide
|
|
874
|
+
// what an unnamed dashboard means to your host instead of writing through.
|
|
875
|
+
if (!next.name) return;
|
|
876
|
+
client.meta.saveItem('dashboard', next.name, next);
|
|
877
|
+
}}
|
|
878
|
+
/>
|
|
879
|
+
);
|
|
880
|
+
}
|
|
540
881
|
```
|
|
541
882
|
|
|
542
883
|
The guard is not defensive noise: the callback receives a
|