@object-ui/plugin-dashboard 17.5.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +5042 -0
  2. package/README.md +544 -74
  3. package/dist/DashboardConfigPanel.d.ts +3 -0
  4. package/dist/DashboardConfigPanel.d.ts.map +1 -1
  5. package/dist/DashboardFilterBar.d.ts.map +1 -1
  6. package/dist/DashboardGridLayout.d.ts +3 -2
  7. package/dist/DashboardGridLayout.d.ts.map +1 -1
  8. package/dist/DashboardRenderer.d.ts +32 -2
  9. package/dist/DashboardRenderer.d.ts.map +1 -1
  10. package/dist/DashboardWithConfig.d.ts +1 -1
  11. package/dist/DashboardWithConfig.d.ts.map +1 -1
  12. package/dist/DatasetWidget.d.ts +43 -119
  13. package/dist/DatasetWidget.d.ts.map +1 -1
  14. package/dist/DrillDownDrawer.d.ts +14 -5
  15. package/dist/DrillDownDrawer.d.ts.map +1 -1
  16. package/dist/MetricCard.d.ts +1 -1
  17. package/dist/MetricWidget.d.ts +27 -2
  18. package/dist/MetricWidget.d.ts.map +1 -1
  19. package/dist/ObjectDataTable.d.ts +473 -19
  20. package/dist/ObjectDataTable.d.ts.map +1 -1
  21. package/dist/ObjectMetricWidget.d.ts +65 -27
  22. package/dist/ObjectMetricWidget.d.ts.map +1 -1
  23. package/dist/ObjectPivotTable.d.ts +46 -7
  24. package/dist/ObjectPivotTable.d.ts.map +1 -1
  25. package/dist/PivotTable.d.ts +20 -3
  26. package/dist/PivotTable.d.ts.map +1 -1
  27. package/dist/RecordDetailDrawer.d.ts +8 -0
  28. package/dist/RecordDetailDrawer.d.ts.map +1 -1
  29. package/dist/WidgetConfigPanel.d.ts +29 -1
  30. package/dist/WidgetConfigPanel.d.ts.map +1 -1
  31. package/dist/WidgetEmptyState.d.ts +27 -0
  32. package/dist/WidgetEmptyState.d.ts.map +1 -0
  33. package/dist/domPassthroughPins.d.ts.map +1 -1
  34. package/dist/index.d.ts +30 -25
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +2306 -1858
  37. package/dist/index.umd.cjs +2 -2
  38. package/dist/legacyRetiredWidget.d.ts +5 -3
  39. package/dist/legacyRetiredWidget.d.ts.map +1 -1
  40. package/dist/measureRefusal.d.ts +50 -0
  41. package/dist/measureRefusal.d.ts.map +1 -0
  42. package/dist/recordFields.d.ts +159 -9
  43. package/dist/recordFields.d.ts.map +1 -1
  44. package/dist/retired-node-types.d.ts +29 -0
  45. package/dist/retired-node-types.d.ts.map +1 -0
  46. package/dist/useConfigPanelTranslation.d.ts +35 -0
  47. package/dist/useConfigPanelTranslation.d.ts.map +1 -0
  48. package/dist/useDashboardAutoRefresh.d.ts +47 -0
  49. package/dist/useDashboardAutoRefresh.d.ts.map +1 -0
  50. package/dist/utils.d.ts +36 -7
  51. package/dist/utils.d.ts.map +1 -1
  52. package/dist/widgetDispatch.d.ts +144 -18
  53. package/dist/widgetDispatch.d.ts.map +1 -1
  54. package/dist/widgetSubCaption.d.ts +40 -0
  55. package/dist/widgetSubCaption.d.ts.map +1 -0
  56. package/package.json +15 -15
@@ -1,21 +1,15 @@
1
1
  import { default as React } from 'react';
2
+ import { ObjectDataTableSchema, TableColumn } from '@object-ui/types';
3
+ import { FieldMeta } from './recordFields.js';
2
4
  export interface ObjectDataTableProps {
3
- schema: {
4
- type: string;
5
- objectName?: string;
6
- dataProvider?: {
7
- provider: string;
8
- object?: string;
9
- };
10
- bind?: string;
11
- filter?: any;
12
- data?: any[];
13
- columns?: any[];
14
- searchable?: boolean;
15
- pagination?: boolean;
16
- className?: string;
17
- [key: string]: any;
18
- };
5
+ /**
6
+ * The `object-data-table` node — anchored to the exported schema type
7
+ * (objectui#6576 / #6914). The hand-rolled literal that stood here carried
8
+ * its own string index signature and had drifted: `drillDown` and
9
+ * `onRowClick` were read behind casts and declared nowhere. Both are declared
10
+ * on the schema type now; `bind` / `className` are inherited from `BaseSchema`.
11
+ */
12
+ schema: ObjectDataTableSchema;
19
13
  dataSource?: any;
20
14
  className?: string;
21
15
  }
@@ -25,12 +19,425 @@ interface NormalizedColumn {
25
19
  accessorKey: string;
26
20
  [key: string]: any;
27
21
  }
22
+ /**
23
+ * What this widget's column producer is allowed to EMIT (objectui#6373).
24
+ *
25
+ * `enrich` below hands its result to `data-table`, whose columns slot is
26
+ * `DataTableSchema.columns: TableColumn[]`. It used to return
27
+ * `NormalizedColumn`, whose `[key: string]: any` accepts anything, so nothing
28
+ * checked the emit against the slot's declaration at all: `{ ...col,
29
+ * ...fieldMeta }` wrote SEVEN keys `TableColumn` does not declare — `name`,
30
+ * `label`, `options`, `referenceTo`, `format`, `currency`, `decimals`.
31
+ *
32
+ * ## ⭐ Why this is not just `: TableColumn`
33
+ *
34
+ * Measured on this program before choosing the shape: annotating the return
35
+ * `TableColumn` and leaving the spread in place raises NO error. TypeScript's
36
+ * excess-property check is a FRESHNESS check on the properties an object
37
+ * literal WRITES OUT; properties arriving through a spread are exempt. So the
38
+ * plain annotation the card suggested is blind to the exact defect the card is
39
+ * about — it would have type-checked the seam without enforcing it, converting
40
+ * a surfaced-key census into a silenced one.
41
+ *
42
+ * The `?: never` members are what make the annotation able to fail. They are
43
+ * ADR-0049 retirement tombstones — this repo's convention for a key that is
44
+ * refused rather than merely absent (`StaticTableColumn` in
45
+ * `@object-ui/types`, `crud.ts` `confirm`) — and they bite by ASSIGNABILITY,
46
+ * not freshness: `FieldMeta['label']` is `string`, which is not assignable to
47
+ * `undefined`, so re-introducing `{ ...fieldMeta }` here is a compile error
48
+ * (TS2322) naming the first offending key. Writing one out explicitly is the
49
+ * other error (TS2353). Both directions were run before this type was written.
50
+ *
51
+ * ## The rule, so the next producer gets the same answer
52
+ *
53
+ * A producer may write into a `TableColumn[]` slot only keys the CONSUMER of
54
+ * that slot reads, and the consumer's read set is MEASURED from the consumer's
55
+ * source, never assumed. Then: a key the consumer reads and `TableColumn`
56
+ * declares is written; a key the consumer reads and `TableColumn` does not
57
+ * declare is held as an alias only where a ruling already holds it; a key the
58
+ * consumer does not read is RETIRED from the emit — never declared, because
59
+ * declaring a key nothing reads is the same `declared != enforced` defect
60
+ * facing the other way (objectui#5453's forwarded `wrap` key).
61
+ *
62
+ * Measured read set of the consumer (`data-table.tsx`, comments stripped):
63
+ * `accessorKey`, `width`, `align`, `header`, `className`, `cellClassName`,
64
+ * `sortable`, `resizable`, `editable`, `type`, `cell`, `headerIcon`,
65
+ * `fitContent`, and `name`. Not one of `label`, `options`, `referenceTo`,
66
+ * `format`, `currency`, `decimals` appears — so all six retired from the
67
+ * emit, and this producer still SOURCES none of them from `fieldMeta`.
68
+ *
69
+ * ⚠️ Three of the six — `format`, `options`, `currency` — have since been
70
+ * DECLARED on `TableColumn` itself (objectui#6425, maintainer ruling
71
+ * 2026-08-27), so they are no longer tombstoned on this emit type: the band
72
+ * below derives them away the moment the declaration landed. That is not a
73
+ * softening of the emit rule but its other branch — an AUTHORED value now
74
+ * passes through `{ ...col }` as declared metadata (the cell pipeline reads
75
+ * it back off the authored column), while writing one out of `fieldMeta`
76
+ * remains retired, pinned by the runtime census in
77
+ * `ObjectDataTable.emitBoundary-6373.test.tsx`.
78
+ *
79
+ * Retiring them is behaviour-preserving because none of them was the live path
80
+ * for its own value: everything they carried is read off `fieldMeta` by the
81
+ * `cell` closure below, which is where this widget's type-aware rendering
82
+ * actually happens. That check — does the value still reach its consumer by
83
+ * another road? — is part of the rule, not an aside: a key with no second road
84
+ * is not inert, and retiring it would change behaviour.
85
+ *
86
+ * `type` is not adjudicated here. `fc62bb490` already settled it at this
87
+ * seam, and its fold (`normalizeTableColumnType`) stands unchanged.
88
+ *
89
+ * `name` is HELD, not retired, and not adjudicated here either: it is
90
+ * objectui#5120's, still open. `data-table` reads `col.accessorKey || col.name`
91
+ * and holds that alias deliberately, because two PUBLISHED skill guides teach a
92
+ * `data-table` column spelled `{ name, label }`. So this producer keeps writing
93
+ * it, byte for byte what it wrote before, and the hold is now DECLARED at the
94
+ * seam instead of arriving anonymously inside a spread. When #5120 retires the
95
+ * consumer alias, this member becomes a tombstone with it.
96
+ */
97
+ /**
98
+ * ⭐ `decimals` — RETIRED from `FieldMeta` ITSELF (objectui#6625), and this
99
+ * hand-written tombstone is now the ONLY enforcement of that key's refusal at
100
+ * BOTH halves of this seam. ⛔ Do not "tidy" it away as redundant.
101
+ *
102
+ * Until objectui#6625 the key needed no tombstone: it was a `FieldMeta` member,
103
+ * so BOTH bands below — {@link EnrichedColumn}'s write-side tombstones
104
+ * (objectui#6373) and {@link UnheldFieldMetaOverrideKey}'s read-side refusal
105
+ * (objectui#6425) — derived it for free. Refusal by MEMBERSHIP. #6625 then
106
+ * retired the member (written from the schema def, read by nothing), which
107
+ * removed it from `keyof FieldMeta` and silently ended that enforcement at both
108
+ * ends. Nothing would have gone red at the moment of loss: on the read side the
109
+ * suite's `@ts-expect-error` would merely have turned TS2578-unused, and on the
110
+ * write side the hand-written pin would have gone on passing on the
111
+ * excess-property check alone — a pin passing because its subject stopped
112
+ * existing.
113
+ *
114
+ * This is the exact mirror of the rule `ObjectGrid`'s sibling tombstone
115
+ * records (`ObjectGridRetiredOptionsTombstone`, objectui#6425): *a pin enforced
116
+ * by a key's non-membership silently stops enforcing the moment the key becomes
117
+ * a member.* Read from this end: **a refusal DERIVED from a key's membership
118
+ * silently stops enforcing the moment that member is deleted.** Same seam, same
119
+ * blindness, opposite direction — which is why the retirement had to be
120
+ * re-stated by hand rather than inherited.
121
+ *
122
+ * objectui#6425's verdict is unchanged — no reader for an authored `decimals`
123
+ * existed then and none exists now — and so is the runtime behaviour: the value
124
+ * the retired `buildFieldMeta` write resolved reached nothing. Only the
125
+ * refusal's mechanism moves, from derived to hand-written. It is intersected
126
+ * into BOTH types below, because the retirement belongs to the SEAM, not to one
127
+ * of its two halves — the same reason `ObjectGrid` intersects its own tombstone
128
+ * into both its draft and its post-fold column. ⭐ A future reader for decimal
129
+ * places reads `scale`, never this key (objectui#6625; `NumberCellRenderer`
130
+ * already does).
131
+ */
132
+ type ObjectDataTableRetiredDecimalsTombstone = {
133
+ decimals?: never;
134
+ };
135
+ /**
136
+ * ⭐ `referenceTo` — RETIRED from `FieldMeta` ITSELF (objectui#6597), the same
137
+ * mechanism and the same reason as {@link ObjectDataTableRetiredDecimalsTombstone}
138
+ * above — read that docblock for the full mechanics of why a hand-written
139
+ * tombstone, not the derived band, is what has to carry this refusal now that
140
+ * the key has left `keyof FieldMeta`.
141
+ *
142
+ * The verdict: `referenceTo` was documented by this package's README as an
143
+ * author-facing column override, and objectui#6425's ruling routed it to this
144
+ * card (⛔ "not declared as spelled") rather than deciding it, because the
145
+ * card that could declare or retire a HELD key cannot also be the card that
146
+ * MEASURES whether a real authoring story exists for it. objectui#6597 did
147
+ * that measurement: `LookupCellRenderer` resolves its lookup target from
148
+ * `field.reference_to` / `field.reference`, never `field.referenceTo`, and
149
+ * `computeLookupExpand` builds `$expand` from the OBJECT SCHEMA's field types,
150
+ * never from an authored column override — so the key reached nothing, HELD
151
+ * or not, pinned by the `referenceTo`-vs-`options` positive control in
152
+ * `ObjectDataTable.overrideSource-6425.test.tsx`. No authoring story survived
153
+ * the search either: `ObjectGrid`'s own relational-meta pass-through
154
+ * (`applyRelationalMeta`) copies `reference_to` / `reference` from the SCHEMA
155
+ * field def only, never from an authored override, and nothing in this repo's
156
+ * docs/examples/fixtures shows a table column pinning a lookup's target away
157
+ * from what its schema field already says. Under the maintainer's standing
158
+ * startup-stage rule (2026-08-27: no measured demand retires immediately,
159
+ * no transition window), that measurement selects RETIRE — the README line is
160
+ * withdrawn with it. ⭐ A future reader for the resolved reference target
161
+ * reads `reference` off the schema field def — the spelling
162
+ * `LookupCellRenderer` and `computeLookupExpand` actually use (the cell read
163
+ * `reference_to` too until objectui#11070 round 4) — never this key.
164
+ */
165
+ type ObjectDataTableRetiredReferenceToTombstone = {
166
+ referenceTo?: never;
167
+ };
168
+ /**
169
+ * ⭐ `reference_to` — RETIRED from `FieldMeta` ITSELF (objectui#11070 round 4),
170
+ * the third key to leave that type, by the same mechanism as
171
+ * {@link ObjectDataTableRetiredDecimalsTombstone} and
172
+ * {@link ObjectDataTableRetiredReferenceToTombstone}: read either docblock for
173
+ * why a hand-written tombstone, not the derived band, carries the refusal once
174
+ * a key leaves `keyof FieldMeta`.
175
+ *
176
+ * The verdict is unchanged and is objectui#6694's: an AUTHORED column may not
177
+ * source a lookup's reference target, and nothing this seam emits carries it
178
+ * either. What changed is the key's standing. `LookupCellRenderer` read
179
+ * `reference_to || reference` until that round and reads `reference` alone
180
+ * now; the ingestion choke point no longer stamps `reference_to` and no
181
+ * in-repo producer writes it, so `buildFieldMeta` stopped copying it and the
182
+ * member went. Without this tombstone the refusal would have ended at the
183
+ * moment of deletion with nothing going red — the blindness the two tombstones
184
+ * above exist to prevent.
185
+ */
186
+ type ObjectDataTableRetiredReferenceToSnakeTombstone = {
187
+ reference_to?: never;
188
+ };
189
+ /**
190
+ * ⭐ `display_field` — RETIRED from `FieldMeta` ITSELF (objectui#11070's
191
+ * text-family round), the fourth key to leave that type, by the same mechanism
192
+ * as the three tombstones above: read {@link ObjectDataTableRetiredDecimalsTombstone}
193
+ * for why a hand-written tombstone, not the derived band, carries the refusal
194
+ * once a key leaves `keyof FieldMeta`.
195
+ *
196
+ * The verdict is unchanged and is objectui#6694's: an AUTHORED column may not
197
+ * source a lookup's display pointer, and nothing this seam emits carries this
198
+ * spelling either. What changed is the key's standing. `LookupCellRenderer`
199
+ * has resolved the display pointer as `displayField || reference_field` since
200
+ * objectui#7155 retired its `display_field` leg, and the ingestion choke point
201
+ * folds a served `display_field` onto `displayField`, so `buildFieldMeta`'s
202
+ * copy of it reached no reader; the copy stopped and the member went. The
203
+ * spec spelling `displayField` is still a `FieldMeta` member, so the derived
204
+ * band keeps refusing it on an authored column by itself.
205
+ */
206
+ type ObjectDataTableRetiredDisplayFieldSnakeTombstone = {
207
+ display_field?: never;
208
+ };
209
+ export type EnrichedColumn = TableColumn
210
+ /** HELD alias, objectui#5120 — see above. Not declared by `TableColumn`. */
211
+ & {
212
+ name?: string;
213
+ }
214
+ /** RETIRED at this emit seam, objectui#6373 — derived, never hand-listed, so
215
+ * a future `FieldMeta` member is tombstoned by default and has to be
216
+ * adjudicated to escape. */
217
+ & {
218
+ [K in Exclude<keyof FieldMeta, keyof TableColumn | 'name'>]?: never;
219
+ } & ObjectDataTableRetiredDecimalsTombstone & ObjectDataTableRetiredReferenceToTombstone & ObjectDataTableRetiredReferenceToSnakeTombstone & ObjectDataTableRetiredDisplayFieldSnakeTombstone;
220
+ /**
221
+ * What this widget's column producer is allowed to READ off the AUTHORED
222
+ * column as a field-meta override (objectui#6425) — the READ half of the seam
223
+ * {@link EnrichedColumn} above fences on the WRITE side. objectui#6373 fenced
224
+ * what `enrich` emits; nothing yet fenced what it consumes.
225
+ *
226
+ * `enrich` handed `buildFieldMeta` six values taken off the authored column.
227
+ * Five of them — `format`, `options`, `referenceTo`, `currency`, `decimals` —
228
+ * were declared by neither `TableColumn` (`@object-ui/types`) nor its
229
+ * `TableColumnSchema` zod mirror. Three were reached through `(col as any)`;
230
+ * the other two through {@link NormalizedColumn}'s `[key: string]: any`, which
231
+ * answers `any` just as loudly without the tell. So the widget honoured an
232
+ * authoring vocabulary the published types refuse, and no artefact in the repo
233
+ * said which keys those were or why.
234
+ *
235
+ * ## The per-key ruling landed (objectui#6425, maintainer 2026-08-27)
236
+ *
237
+ * This type used to hold all five keys with ⛔ "declares nothing and retires
238
+ * nothing" — both branches were maintainer decisions. The ruling has since
239
+ * disposed of them per key, and this keyhole now CARRIES that disposition:
240
+ *
241
+ * - `format`, `options` — DECLARED on `TableColumn` + its zod mirror
242
+ * (documented AND kept; declaring is truth-maintenance). Read below at
243
+ * their published types via `Pick<TableColumn, …>`.
244
+ * - `currency` — DECLARED (kept in production, never promised before;
245
+ * declaring makes the existing behaviour honest). Same `Pick`.
246
+ * - `decimals` — RETIRED, immediately (neither promised nor kept: zero
247
+ * readers measured, and the authored read below is gone). Refused by
248
+ * {@link ObjectDataTableRetiredDecimalsTombstone} since objectui#6625
249
+ * retired the `FieldMeta` member itself and took the key out of the derived
250
+ * band's pool; the verdict is unchanged, only its mechanism moved.
251
+ * - `referenceTo` — RETIRED (objectui#6597, enforce-or-remove: measured no
252
+ * authoring story for a column-level reference override — see the
253
+ * withdraw verdict above `ObjectDataTableRetiredReferenceToTombstone`).
254
+ * Refused by that tombstone the same way `decimals` is refused by its own.
255
+ *
256
+ * The shape itself is unchanged from what objectui#6461 landed for
257
+ * `plugin-grid` (`ObjectGridColumnHolds` / `RetiredListColumnKey`); the
258
+ * identifiers differ because this producer's seam is a READ, not an emit.
259
+ *
260
+ * ## The band is DERIVED, and it bites by assignability
261
+ *
262
+ * `FieldMeta` is the override vocabulary — `buildFieldMeta`'s `overrides` is a
263
+ * subset of it — so it is the pool a new tolerance would come from, and the
264
+ * same pool {@link EnrichedColumn}'s tombstones derive from. Deriving means a
265
+ * NEW `FieldMeta` member is refused here on the day it is added, without
266
+ * anyone remembering to extend a hand-written list. (Stated without a count on
267
+ * purpose: it read "a seventh" while the type had eight members, and
268
+ * objectui#6625 and objectui#6597 have since retired two. The property is
269
+ * that ADDING is covered by derivation — ⚠️ REMOVING is not, which is why
270
+ * those cards each had to leave a hand-written tombstone behind —
271
+ * {@link ObjectDataTableRetiredDecimalsTombstone} and
272
+ * {@link ObjectDataTableRetiredReferenceToTombstone}.)
273
+ *
274
+ * ⚠️ It derives from the OVERRIDE VOCABULARY, not from the authored input
275
+ * type, and that difference is forced rather than stylistic. `plugin-grid`'s
276
+ * sibling band (`RetiredListColumnKey`, objectui#6461) is
277
+ * `Exclude<keyof ListColumn, …>` — it can derive from its authored type
278
+ * because `ListColumn` DECLARES its keys. This producer's authored type
279
+ * cannot: {@link NormalizedColumn} carries `[key: string]: any`, so `keyof` it
280
+ * is `string | number`. Measured on this program — a band built on it accepts
281
+ * `'totallyMadeUpKey'` as a member, so it would ban nothing at all. The pool
282
+ * therefore has to be the vocabulary the overrides are drawn FROM.
283
+ *
284
+ * That choice has a cost, and it is stated here rather than left to be
285
+ * discovered: a candidate key OUTSIDE `FieldMeta` can never land in the band.
286
+ * It is refused by the other half of this type instead —
287
+ * `AuthoredColumnOverrides` declares no index signature, so reading an
288
+ * unadjudicated key is TS2339 AT THE READ SITE, which is the enforcement
289
+ * `enrich` actually runs on (`(col as any).x` and the bag's index signature
290
+ * both answered `any` there). Both halves are pinned by the suite.
291
+ *
292
+ * ⚠️ Measured on this program, in both directions, before this shape was
293
+ * written:
294
+ * - `?: never` bites by ASSIGNABILITY, not by freshness, so the refusal
295
+ * survives a spread — `const s: AuthoredColumnOverrides = someBag` fails on
296
+ * a source that DECLARES a banded key, and passes on one that does not.
297
+ * - `accessorKey` is required here for a reason that is not decoration:
298
+ * without one property in common, TypeScript's weak-type check rejects the
299
+ * assignment below outright (TS2559), and a type that refuses everything
300
+ * pins nothing. It is the same key {@link NormalizedColumn} already
301
+ * declares required, so this makes no new claim about the authored object.
302
+ *
303
+ * ## A VIEW, not a census of the object
304
+ *
305
+ * The banded members say "this producer must not SOURCE this override from the
306
+ * authored column" — not "this key is absent at run time". `normalizeColumns`
307
+ * deliberately leaves the authored spelling in place, so a column really can
308
+ * carry `name` / `label`; they are refused HERE because this seam already has
309
+ * answers for both and they do not come from the column: `name` comes from
310
+ * `accessorKey` and `label` from `col.header`, which objectui#5351 made the one
311
+ * place the authored label is translated.
312
+ */
313
+ /**
314
+ * The undeclared-but-live override keys this producer still holds. **None
315
+ * remain.** objectui#6425's ruling (maintainer, 2026-08-27) declared `format` /
316
+ * `options` / `currency` on `TableColumn` itself (they are read via
317
+ * `Pick<TableColumn, …>` below, no hold needed) and retired `decimals`
318
+ * outright — refused by {@link ObjectDataTableRetiredDecimalsTombstone} since
319
+ * objectui#6625 retired the `FieldMeta` member that used to carry it into the
320
+ * derived band. `referenceTo` was the last hold — objectui#6425's ruling
321
+ * routed it to objectui#6597 rather than deciding it (⛔ "not declared as
322
+ * spelled"), and that card MEASURED no authoring story for a column-level
323
+ * reference override, so the maintainer's standing startup-stage rule
324
+ * (2026-08-27: no measured demand retires immediately) picked withdraw —
325
+ * refused by {@link ObjectDataTableRetiredReferenceToTombstone} the same way
326
+ * `decimals` is refused by its own tombstone.
327
+ *
328
+ * Kept as an interface — empty rather than deleted — because it is the
329
+ * documented anchor a future per-key ruling holds a new key onto, the same
330
+ * role it played for `format` / `options` / `currency` before objectui#6425
331
+ * declared them and for `referenceTo` before objectui#6597 retired it.
332
+ */
333
+ export interface ObjectDataTableColumnHolds {
334
+ }
335
+ /**
336
+ * The candidate keys this seam refuses — DERIVED from the override vocabulary,
337
+ * never hand-listed, so a future `FieldMeta` member has to be adjudicated onto
338
+ * {@link ObjectDataTableColumnHolds} to escape. Keys `TableColumn` declares
339
+ * leave the pool by declaration: `type` (its VALUE set is `fc62bb490`'s,
340
+ * folded below by `normalizeTableColumnType`) and, since objectui#6425's
341
+ * ruling, `format` / `options` / `currency`.
342
+ *
343
+ * ⚠️ `decimals` and `referenceTo` USED TO land here — that is how objectui#6425's
344
+ * ruling was enforced for `decimals` at the read site, and how `referenceTo`
345
+ * stayed HELD (neither declared nor refused) until objectui#6597 ruled. Neither
346
+ * can any more: objectui#6625 and objectui#6597 each retired the `FieldMeta`
347
+ * member itself, so both keys are out of this Exclude's POOL rather than out of
348
+ * its exclusion list, and a derived band cannot refuse a key that is not in
349
+ * what it derives from. Their refusals are carried by
350
+ * {@link ObjectDataTableRetiredDecimalsTombstone} and
351
+ * {@link ObjectDataTableRetiredReferenceToTombstone} instead, intersected below.
352
+ * ⛔ Do not read this band's silence about either key as a softening — the
353
+ * tombstone and the band together are the same verdict, unchanged since each
354
+ * key's ruling.
355
+ *
356
+ * The pool shrank, then grew again. What THIS band refuses is `name` and
357
+ * `label` — both of them `FieldMeta` members with answers this seam already
358
+ * has (see the docblock above) — plus the relational members objectui#6694 and
359
+ * objectui#6875 added: `reference` and `displayField`. objectui#6694 added
360
+ * `reference_to` and `display_field` too; objectui#11070 retired both members
361
+ * from `FieldMeta` (round 4 and the text-family round), so they left this
362
+ * band's POOL, and {@link ObjectDataTableRetiredReferenceToSnakeTombstone} and
363
+ * {@link ObjectDataTableRetiredDisplayFieldSnakeTombstone} carry their refusals.
364
+ *
365
+ * ⭐ Those members are the derivation working as designed, and their verdict is
366
+ * the one objectui#6597 already reached for `referenceTo`: an AUTHORED column
367
+ * may not source a lookup's reference target. They are refused HERE while
368
+ * `buildFieldMeta` writes them freely, because that write's source is the
369
+ * OBJECT SCHEMA field def and never the column — the same distinction
370
+ * objectui#6597 measured, and the reason adding them needed no new hold. They
371
+ * reached this band without anyone extending a list.
372
+ */
373
+ export type UnheldFieldMetaOverrideKey = Exclude<keyof FieldMeta, keyof TableColumn | keyof ObjectDataTableColumnHolds>;
374
+ /** The keyhole `enrich` reads the authored column through. See the docblock above. */
375
+ export type AuthoredColumnOverrides = {
376
+ accessorKey: string;
377
+ }
378
+ /** DECLARED by `TableColumn`; widened to `string` because the authored value
379
+ * is folded onto the published union downstream, not at the read. */
380
+ & {
381
+ type?: string;
382
+ }
383
+ /** DECLARED by `TableColumn` + its zod mirror (objectui#6425, maintainer
384
+ * ruling 2026-08-27) — the three adjudicated override keys are read at
385
+ * their published types, straight off the declaration. */
386
+ & Pick<TableColumn, 'format' | 'options' | 'currency'> & ObjectDataTableColumnHolds & {
387
+ [K in UnheldFieldMetaOverrideKey]?: never;
388
+ }
389
+ /** RETIRED, objectui#6425's verdict — re-stated by hand because objectui#6625
390
+ * took the key out of the derived band's pool. See the tombstone's docblock. */
391
+ & ObjectDataTableRetiredDecimalsTombstone
392
+ /** RETIRED, objectui#6597's verdict (enforce-or-remove, withdraw) —
393
+ * re-stated by hand for the same reason: the key left `keyof FieldMeta`,
394
+ * so the derived band can no longer reach it. See the tombstone's docblock. */
395
+ & ObjectDataTableRetiredReferenceToTombstone
396
+ /** RETIRED from `FieldMeta` by objectui#11070 round 4 — re-stated by hand for
397
+ * the same reason. See the tombstone's docblock. */
398
+ & ObjectDataTableRetiredReferenceToSnakeTombstone
399
+ /** RETIRED from `FieldMeta` by objectui#11070's text-family round —
400
+ * re-stated by hand for the same reason. See the tombstone's docblock. */
401
+ & ObjectDataTableRetiredDisplayFieldSnakeTombstone;
28
402
  /**
29
403
  * Normalize columns to support both string[] shorthand and object[] formats.
30
404
  *
31
405
  * - `string[]` entries are converted to `{ header, accessorKey }` objects,
32
406
  * handling both snake_case and camelCase for header generation.
33
- * - Object entries are returned as-is.
407
+ * - Object entries have their field identity AND their display text RESOLVED
408
+ * here, at the producer, and stamped onto the data-table adapter's own keys
409
+ * (`accessorKey` / `header`). The adapter reads only those two, and no longer
410
+ * falls back to `name` / `label` (objectui#5120, objectui#5351).
411
+ *
412
+ * Object entries used to be returned raw (objectui#5120). `accessorKey` is the
413
+ * table LIBRARY's column key — `column-identity.ts` names it
414
+ * `TABLE_ADAPTER_COLUMN_KEY` and deliberately holds the metadata-identity fold
415
+ * away from it — so a column authored in the spec-canonical spelling
416
+ * (`{ field: 'stage' }`) reached the adapter carrying no `accessorKey` at all
417
+ * and rendered a header over `row[undefined]`: blank cells, no warning. The
418
+ * `$expand` whitelist in `computeLookupExpand` missed it for the same reason,
419
+ * so a `field`-spelled lookup column also lost its related record.
420
+ *
421
+ * Resolving it HERE is the move objectui#5022 made in `RelatedList` and
422
+ * objectui#5068 generalized in `ObjectGrid`: metadata vocabulary in, adapter
423
+ * vocabulary out, one translation in one place. The adapter stays monolingual;
424
+ * the producer owns the translation.
425
+ *
426
+ * Mirror, don't move — the same three rules `RelatedList` states:
427
+ * - an author-supplied `accessorKey` is NEVER overwritten; a deliberate
428
+ * divergence between the table slot and the metadata key belongs to the
429
+ * author;
430
+ * - the authored spelling is left in place, so a host reading `field` / `name`
431
+ * back off these columns keeps working;
432
+ * - an entry with neither a resolvable identity nor any display text is
433
+ * returned UNTOUCHED — nothing is invented for it. It behaves exactly as it
434
+ * does today: a header over empty cells, silently. Whether that silence
435
+ * deserves a dev-time diagnostic is objectui#5349's question, and is
436
+ * deliberately NOT answered here.
437
+ *
438
+ * Returning the INPUT entry by reference when there is nothing to add is load
439
+ * bearing: data-table re-seeds its column state whenever the list is a new
440
+ * object (objectui#4618), and this widget rebuilds its node on every render.
34
441
  */
35
442
  export declare function normalizeColumns(columns: (string | Record<string, any>)[]): NormalizedColumn[];
36
443
  /**
@@ -52,9 +459,56 @@ export declare function normalizeColumns(columns: (string | Record<string, any>)
52
459
  /**
53
460
  * Compute the list of lookup-typed accessors that should be expanded when
54
461
  * fetching rows. Returns column accessors whose object schema field type is
55
- * a relation (lookup/reference/master_detail/user/owner). Used by the
56
- * dashboard table widget to ask the data adapter to populate referenced
462
+ * a relation. Neither the type family nor the test itself is restated here:
463
+ * this delegates to {@link isLookupType} in `recordFields.tsx`, the package's
464
+ * single relation predicate, which reads `EXPANDABLE_FIELD_TYPES` — the family
465
+ * `@object-ui/core` publishes. Used
466
+ * by the dashboard table widget to ask the data adapter to populate referenced
57
467
  * records (e.g. `account: { id, name }`) so cells don't show raw FK ids.
468
+ *
469
+ * THE GATE (objectui#4914, ruling B) runs ahead of the relation test. Measured
470
+ * before the ruling: a `record_owner: { type: 'owner' }` column was ACTIVELY
471
+ * requested for `$expand` — the retired spelling got the full relational read
472
+ * path while the same field's editor answered with a tombstone. It is refused
473
+ * now, loudly and once, and the cell shows the raw id it was always going to
474
+ * show once the spelling stopped being a relation. That the author is TOLD is
475
+ * the whole difference between this and the mechanical deletion the
476
+ * measurement rejected.
477
+ *
478
+ * ## The relation test is core's object, not a private copy (objectui#5692)
479
+ *
480
+ * It used to be the inline literal
481
+ * `t === 'lookup' || t === 'reference' || t === 'master_detail' || t === 'user'`
482
+ * — one of TWO copies this package held (the other `LOOKUP_TYPES` in
483
+ * `recordFields.tsx`), neither deriving from nor pinned against the family core
484
+ * publishes. objectui#5312's claim to have converted "the LAST private copy"
485
+ * was false by these two; they predate that sweep and were outside its file
486
+ * surface.
487
+ *
488
+ * This is the LIVE half of that convergence — `computeLookupExpand` drives a
489
+ * real `$expand` on every dashboard table fetch — so both membership deltas are
490
+ * observable here, and both were decided by measurement (see `isLookupType` in
491
+ * `recordFields.tsx` for the full record):
492
+ *
493
+ * - a `tree` column now GETS `$expand`-ed, the same treatment the form / grid
494
+ * road already gives it;
495
+ * - a `reference` column no longer does, and that is a no-op on spec-compliant
496
+ * data: the spelling is absent from `@objectstack/spec`'s closed `FieldType`
497
+ * and refused by `FieldSchema.safeParse`, so no object schema can declare a
498
+ * field whose stored type is `reference`.
499
+ *
500
+ * ## One predicate, not two that agree by coincidence (objectui#5876)
501
+ *
502
+ * This function used to carry its own `isLookup`, byte-identical to
503
+ * `isLookupType` once objectui#5692 had pointed both at the same set — two
504
+ * bodies that agreed because one sweep aligned them, with nothing keeping them
505
+ * aligned afterwards. The test IS `isLookupType` now, so this module no longer
506
+ * IMPORTS the shared family or the retirement gate and no longer CALLS either
507
+ * (they are named in this prose and nowhere else in the file). That absence is
508
+ * the assertion: a BEHAVIOURAL test cannot see this change, because a
509
+ * byte-identical local copy satisfies every boolean claim you can make about
510
+ * `$expand`. The pin that can see it is the identity pin in
511
+ * `__tests__/expandableFamily.identity-5692.test.ts`.
58
512
  */
59
513
  export declare function computeLookupExpand(schema: {
60
514
  columns?: any[];
@@ -1 +1 @@
1
- {"version":3,"file":"ObjectDataTable.d.ts","sourceRoot":"","sources":["../src/ObjectDataTable.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAgE,MAAM,OAAO,CAAC;AAerF,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,YAAY,CAAC,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QACrD,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,GAAG,CAAC;QACb,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC;QAChB,UAAU,CAAC,EAAE,OAAO,CAAC;QACrB,UAAU,CAAC,EAAE,OAAO,CAAC;QACrB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;KACpB,CAAC;IACF,UAAU,CAAC,EAAE,GAAG,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,6EAA6E;AAC7E,UAAU,gBAAgB;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,EAAE,GAAG,gBAAgB,EAAE,CAS9F;AAED;;;;;;;;;;;;;;;GAeG;AACH;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE;IAAE,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,EAChD,YAAY,EAAE,GAAG,GAChB,MAAM,EAAE,CAgCV;AAED,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CA2T1D,CAAC"}
1
+ {"version":3,"file":"ObjectDataTable.d.ts","sourceRoot":"","sources":["../src/ObjectDataTable.tsx"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { default as React } from 'react';
2
- import { DrillDownConfig } from '@object-ui/types';
3
- import { CompareToConfig } from './utils';
2
+ import { I18nLabel, ObjectChartSchema, ObjectMetricDrillDownConfig } from '@object-ui/types';
3
+ import { CompareToConfig } from './utils.js';
4
4
  /**
5
5
  * ObjectMetricWidget — Data-bound metric widget.
6
6
  *
@@ -19,39 +19,69 @@ import { CompareToConfig } from './utils';
19
19
  export interface ObjectMetricWidgetProps {
20
20
  /** The object/resource name to query */
21
21
  objectName: string;
22
- /** Aggregation config (field, function, groupBy) */
22
+ /**
23
+ * Aggregation config (field, function, groupBy).
24
+ *
25
+ * `groupBy` is the contract's own union — BY REFERENCE through
26
+ * `ObjectChartSchema['aggregate']`, which holds `ChartAggregate` from
27
+ * `@objectstack/spec/ui` by reference in turn, never a local near-copy of it
28
+ * (`check:spec-symbols`). It is the same authored key the `object-chart` node
29
+ * carries, so a second spelling here could only be a way for the two to
30
+ * disagree. (Both dashboard relays used to compose this node and the chart's
31
+ * out of one `provider: 'object'` block; since objectui#11525 a dataset-less
32
+ * metric draws the retired-format placeholder, and this node is authored.)
33
+ *
34
+ * It used to say `string`, which was a claim about the AUTHOR that nothing
35
+ * upstream backed: the value crosses two `any` seams on its way in
36
+ * (`isObjectProvider` narrows the widget data to `aggregate?: any`, and
37
+ * `computeOne` takes the datasource untyped), so the declaration refused the
38
+ * structured `{ field, dateGranularity }` node at neither compile time nor
39
+ * runtime — it merely hid it from the reader, and from anyone asking whether
40
+ * `computeOne` handled it (objectui#8613). Optional here, unlike the chart's,
41
+ * because a metric paints ONE number and floors an absent `groupBy` at
42
+ * `'_all'`.
43
+ */
23
44
  aggregate?: {
24
45
  field: string;
25
46
  function: string;
26
- groupBy?: string;
47
+ groupBy?: NonNullable<ObjectChartSchema['aggregate']>['groupBy'];
27
48
  };
28
49
  /** Filter conditions */
29
50
  filter?: any;
30
- /** Static label for the metric */
31
- label: string | {
32
- key?: string;
33
- defaultValue?: string;
34
- };
51
+ /**
52
+ * The KPI's heading, in @objectstack/spec's `I18nLabel` vocabulary — a plain
53
+ * string or an inline per-locale map (`{ en: 'Revenue', 'zh-CN': '收入' }`).
54
+ *
55
+ * Was `string | { key?, defaultValue? }` — the key-reference form
56
+ * objectstack#5055 RETIRED at rc.6. The sibling `MetricWidgetProps` was
57
+ * migrated for this reason in objectui#4358; this interface was missed in
58
+ * that pass, and the omission was not inert (objectui#5264). This component
59
+ * forwards `label` straight to `MetricWidget`, which resolves it with
60
+ * `pickLocalized`. The retired object hits no locale limb, so resolution
61
+ * falls through to the last resort — the FIRST string property in insertion
62
+ * order — and `{ key, defaultValue }` written in that natural order paints
63
+ * the raw dotted translation key onto the card as its visible label.
64
+ *
65
+ * The producer side already says so: `ObjectMetricPropsSchema.label` in
66
+ * `@objectstack/spec/ui` is `I18nLabelSchema`, and its inline-map key regex
67
+ * rejects `key`/`defaultValue` by name. This declaration now agrees with it.
68
+ */
69
+ label: string | I18nLabel;
35
70
  /** Fallback static value (used when no dataSource or in demo mode) */
36
71
  fallbackValue?: string | number;
37
72
  /** Trend info */
38
73
  trend?: {
39
74
  value: number;
40
- label?: string | {
41
- key?: string;
42
- defaultValue?: string;
43
- };
75
+ /** Trend caption, same `I18nLabel` vocabulary as {@link ObjectMetricWidgetProps.label}. */
76
+ label?: string | I18nLabel;
44
77
  direction?: 'up' | 'down' | 'neutral';
45
78
  };
46
79
  /** Icon name or ReactNode */
47
80
  icon?: React.ReactNode | string;
48
81
  /** Additional CSS class */
49
82
  className?: string;
50
- /** Description */
51
- description?: string | {
52
- key?: string;
53
- defaultValue?: string;
54
- };
83
+ /** Sub-caption under the value, same `I18nLabel` vocabulary as {@link ObjectMetricWidgetProps.label}. */
84
+ description?: string | I18nLabel;
55
85
  /** External data source (overrides context) */
56
86
  dataSource?: any;
57
87
  /** Visual color variant for the icon container */
@@ -72,16 +102,24 @@ export interface ObjectMetricWidgetProps {
72
102
  */
73
103
  invert?: boolean;
74
104
  /**
75
- * Drill-down config. When enabled, clicking the metric card opens a
76
- * drawer (or modal) showing the underlying records that contributed
77
- * to this metric, filtered by the same `filter` used for aggregation.
105
+ * Drill-down config. When enabled, clicking the metric card opens a drawer
106
+ * (or dialog) listing the records behind the number, scoped by this
107
+ * widget's own `filter`, the one the aggregate runs over.
108
+ *
109
+ * Typed `ObjectMetricDrillDownConfig`, not the shared `DrillDownConfig`:
110
+ * `drillDown.filter` and `drillDown.mode` are refused by name on this block
111
+ * (objectui#9002, ruling B). A metric has no click event for a drill filter
112
+ * to interpolate against and no row to open as a record. The shared type
113
+ * keeps both members for the blocks that read them.
78
114
  */
79
- drillDown?: DrillDownConfig;
80
- /** Title for the drill-down panel; defaults to the metric label. */
81
- title?: string | {
82
- key?: string;
83
- defaultValue?: string;
84
- };
115
+ drillDown?: ObjectMetricDrillDownConfig;
116
+ /**
117
+ * Title for the drill-down panel; defaults to the metric label. Same
118
+ * `I18nLabel` vocabulary as {@link ObjectMetricWidgetProps.label}, and
119
+ * resolved through the same `pickLocalized` — see `drawerTitle` below, which
120
+ * used to read the retired form's `defaultValue` limb directly.
121
+ */
122
+ title?: string | I18nLabel;
85
123
  /**
86
124
  * Period-over-period comparison configuration — the executor's own
87
125
  * `{ kind, dimension? }` contract since objectstack#5011. When set, the