@object-ui/core 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.
Files changed (134) hide show
  1. package/CHANGELOG.md +6610 -0
  2. package/README.md +101 -14
  3. package/dist/actions/ActionEngine.d.ts +4 -2
  4. package/dist/actions/ActionEngine.js +13 -8
  5. package/dist/actions/ActionRunner.d.ts +288 -27
  6. package/dist/actions/ActionRunner.js +443 -63
  7. package/dist/actions/TransactionManager.d.ts +7 -2
  8. package/dist/actions/actionKeys.d.ts +9 -7
  9. package/dist/actions/actionKeys.js +57 -14
  10. package/dist/actions/actionResponse.js +2 -2
  11. package/dist/actions/index.d.ts +1 -0
  12. package/dist/actions/index.js +1 -0
  13. package/dist/actions/recordDelete.d.ts +118 -0
  14. package/dist/actions/recordDelete.js +112 -0
  15. package/dist/adapters/ValueDataSource.d.ts +6 -1
  16. package/dist/adapters/ValueDataSource.js +1090 -98
  17. package/dist/adapters/batchTransaction.d.ts +1 -1
  18. package/dist/adapters/batchTransaction.js +1 -1
  19. package/dist/builder/schema-builder.d.ts +38 -81
  20. package/dist/builder/schema-builder.js +33 -152
  21. package/dist/data-scope/ViewDataProvider.d.ts +1 -1
  22. package/dist/data-scope/ViewDataProvider.js +21 -1
  23. package/dist/data-scope/element-data-source.d.ts +261 -7
  24. package/dist/data-scope/element-data-source.js +342 -52
  25. package/dist/data-scope/index.d.ts +9 -4
  26. package/dist/data-scope/index.js +9 -4
  27. package/dist/evaluator/ExpressionEvaluator.d.ts +86 -0
  28. package/dist/evaluator/ExpressionEvaluator.js +184 -12
  29. package/dist/evaluator/FormulaFunctions.d.ts +0 -10
  30. package/dist/evaluator/FormulaFunctions.js +166 -27
  31. package/dist/evaluator/declaredPredicate.d.ts +93 -9
  32. package/dist/evaluator/declaredPredicate.js +117 -16
  33. package/dist/evaluator/fieldRules.d.ts +88 -4
  34. package/dist/evaluator/fieldRules.js +203 -31
  35. package/dist/evaluator/index.d.ts +1 -0
  36. package/dist/evaluator/index.js +1 -0
  37. package/dist/evaluator/listConditional.d.ts +103 -34
  38. package/dist/evaluator/listConditional.js +127 -50
  39. package/dist/evaluator/optionRules.d.ts +77 -6
  40. package/dist/evaluator/optionRules.js +87 -10
  41. package/dist/evaluator/predicateInput.d.ts +15 -4
  42. package/dist/evaluator/predicateInput.js +35 -2
  43. package/dist/evaluator/rowPredicateCanon.d.ts +7 -18
  44. package/dist/evaluator/rowPredicateCanon.js +65 -115
  45. package/dist/evaluator/subjectPermissions.d.ts +76 -0
  46. package/dist/evaluator/subjectPermissions.js +44 -0
  47. package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
  48. package/dist/evaluator/unevaluablePredicate.js +70 -0
  49. package/dist/index.d.ts +14 -0
  50. package/dist/index.js +80 -2
  51. package/dist/protocols/index.d.ts +0 -1
  52. package/dist/protocols/index.js +9 -1
  53. package/dist/registry/Registry.d.ts +358 -31
  54. package/dist/registry/Registry.js +263 -11
  55. package/dist/registry/WidgetRegistry.js +7 -3
  56. package/dist/registry/html-tier-intrinsics.d.ts +90 -0
  57. package/dist/registry/html-tier-intrinsics.js +110 -0
  58. package/dist/registry/public-blocks.d.ts +8 -0
  59. package/dist/registry/public-blocks.js +57 -0
  60. package/dist/theme/ThemeEngine.js +1 -1
  61. package/dist/utils/chart-category-key.d.ts +82 -0
  62. package/dist/utils/chart-category-key.js +84 -0
  63. package/dist/utils/chart-measure-key.d.ts +64 -0
  64. package/dist/utils/chart-measure-key.js +65 -0
  65. package/dist/utils/chart-presentation.d.ts +95 -112
  66. package/dist/utils/chart-presentation.js +169 -104
  67. package/dist/utils/chart-series.d.ts +131 -5
  68. package/dist/utils/chart-series.js +98 -16
  69. package/dist/utils/column-sortability.d.ts +157 -0
  70. package/dist/utils/column-sortability.js +217 -0
  71. package/dist/utils/dashboard-filters.d.ts +38 -6
  72. package/dist/utils/dashboard-filters.js +107 -61
  73. package/dist/utils/dataset-format.d.ts +211 -30
  74. package/dist/utils/dataset-format.js +389 -46
  75. package/dist/utils/date-display.d.ts +354 -0
  76. package/dist/utils/date-display.js +463 -0
  77. package/dist/utils/date-macros.js +7 -0
  78. package/dist/utils/drill-down.d.ts +78 -0
  79. package/dist/utils/drill-down.js +93 -0
  80. package/dist/utils/emptiness.d.ts +96 -0
  81. package/dist/utils/emptiness.js +101 -0
  82. package/dist/utils/expand-fields.d.ts +76 -6
  83. package/dist/utils/expand-fields.js +76 -6
  84. package/dist/utils/extract-records.d.ts +65 -5
  85. package/dist/utils/extract-records.js +65 -8
  86. package/dist/utils/filter-converter.d.ts +152 -4
  87. package/dist/utils/filter-converter.js +1092 -22
  88. package/dist/utils/filter-tokens.d.ts +49 -5
  89. package/dist/utils/filter-tokens.js +105 -33
  90. package/dist/utils/grouping-fields.d.ts +61 -0
  91. package/dist/utils/grouping-fields.js +84 -0
  92. package/dist/utils/managedBy.d.ts +3 -3
  93. package/dist/utils/managedBy.js +5 -5
  94. package/dist/utils/native-date-value.d.ts +48 -0
  95. package/dist/utils/native-date-value.js +167 -0
  96. package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
  97. package/dist/utils/non-grid-row-ceiling.js +141 -0
  98. package/dist/utils/normalize-list-view.d.ts +121 -1
  99. package/dist/utils/normalize-list-view.js +186 -6
  100. package/dist/utils/number-display.d.ts +63 -14
  101. package/dist/utils/number-display.js +34 -12
  102. package/dist/utils/object-aggregate-query.d.ts +113 -0
  103. package/dist/utils/object-aggregate-query.js +50 -0
  104. package/dist/utils/parent-scope.d.ts +123 -0
  105. package/dist/utils/parent-scope.js +140 -0
  106. package/dist/utils/predicate-fields.d.ts +45 -12
  107. package/dist/utils/predicate-fields.js +137 -12
  108. package/dist/utils/record-color.d.ts +75 -0
  109. package/dist/utils/record-color.js +65 -0
  110. package/dist/utils/record-source.d.ts +298 -0
  111. package/dist/utils/record-source.js +331 -0
  112. package/dist/utils/record-title.d.ts +148 -8
  113. package/dist/utils/record-title.js +220 -26
  114. package/dist/utils/reference-keys.d.ts +21 -22
  115. package/dist/utils/reference-keys.js +532 -29
  116. package/dist/utils/sort-query.d.ts +115 -15
  117. package/dist/utils/sort-query.js +240 -13
  118. package/dist/utils/text-comparand.d.ts +92 -0
  119. package/dist/utils/text-comparand.js +99 -0
  120. package/dist/utils/without-denied-fields.d.ts +49 -0
  121. package/dist/utils/without-denied-fields.js +52 -0
  122. package/dist/validation/index.d.ts +8 -4
  123. package/dist/validation/index.js +8 -4
  124. package/dist/validation/schema-validator.d.ts +1 -1
  125. package/dist/validation/schema-validator.js +200 -82
  126. package/dist/validation/validators/object-validation-engine.d.ts +1 -1
  127. package/dist/validation/validators/object-validation-engine.js +14 -3
  128. package/package.json +8 -9
  129. package/dist/data-scope/DataScopeManager.d.ts +0 -127
  130. package/dist/data-scope/DataScopeManager.js +0 -229
  131. package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
  132. package/dist/protocols/ResponsiveProtocol.js +0 -158
  133. package/dist/validation/validation-engine.d.ts +0 -88
  134. package/dist/validation/validation-engine.js +0 -434
@@ -21,6 +21,14 @@
21
21
  *
22
22
  * This is a single, reviewable source of truth for the public surface — prefer
23
23
  * editing this list over scattering `tier` flags across registration sites.
24
+ *
25
+ * ⛔ The html tier's intrinsic elements (`h1`, `p`, `a`, the sectioning tags, …)
26
+ * do NOT belong here. They reach the published manifest through the second
27
+ * roster beside this one, `HTML_TIER_INTRINSICS`, stamped `tier: 'html'`, so
28
+ * that the manifest's other reader — the objectstack gate that whitelists a
29
+ * `kind:'html'` page's tags — accepts them without this curated JSON-surface
30
+ * vocabulary growing (objectui#10735). Listing one here would widen the
31
+ * vocabulary silently; `html-tier-intrinsics.test.ts` pins the two disjoint.
24
32
  */
25
33
  export const PUBLIC_BLOCKS = [
26
34
  // ── Tier A — object-aware blocks (the contract core) ──────────────────────
@@ -34,6 +42,23 @@ export const PUBLIC_BLOCKS = [
34
42
  'object-gantt',
35
43
  'object-timeline',
36
44
  'object-map',
45
+ // Curated by objectui#10064, which is a NARROWING being undone rather than a
46
+ // widening: `@objectstack/spec` already declares `object-tree` on
47
+ // `ComponentPropsMap`, and @object-ui/plugin-tree has registered the renderer
48
+ // all along — this roster was the one face that withheld it, so the tier sat
49
+ // narrower than the declaration. Everything downstream reported that
50
+ // faithfully and could not fix it: `getPublicConfigs()` is what both manifest
51
+ // producers serialize, so the framework's tracked `sdui.manifest.json` could
52
+ // never carry a block the contract says exists, and moving the objectui pin
53
+ // could not help while the pinned source had nothing to carry
54
+ // (objectstack#18407, whose parity-gate half waits on this entry).
55
+ //
56
+ // The `tree` alias the same module registers is deliberately NOT here: it is
57
+ // a second spelling of this one block and @objectstack/spec declares no such
58
+ // key, so curating it would widen the vocabulary past the declaration — the
59
+ // opposite act — and leave an authoring model two names for one block, which
60
+ // is the ground `record:chatter` is held out on above.
61
+ 'object-tree',
37
62
  'object-metric',
38
63
  'object-chart',
39
64
  'dashboard',
@@ -102,6 +127,38 @@ export const PUBLIC_BLOCKS = [
102
127
  'tabs',
103
128
  'accordion',
104
129
  'container',
130
+ // The neutral block container, minted by the 2026-08-29 ruling as the JSON
131
+ // surface's replacement for the deprecated `div` (objectui#3965, PR #6878).
132
+ // It landed on every other declaration face — interface, zod mirror,
133
+ // `SchemaRegistry`, registration, docs page, and 27 catalog fixtures — and
134
+ // this list was the one face it missed, so the vocabulary taught a type the
135
+ // contract did not carry (`3619792bf`).
136
+ //
137
+ // MEASURED before it was added, because "add it and see" is how a gate
138
+ // discovers a new population at merge time. `registry-inputs-spec-parity`
139
+ // (apps/console) does NOT judge it, and not by tolerance: its coverage set is
140
+ // SPEC-shaped — `Object.keys(ComponentPropsMap)`, filtered to what this repo
141
+ // registers with inputs. It never reads this list, and membership here is
142
+ // neither necessary nor sufficient for being judged: `element:record_picker`
143
+ // is judged while absent from this list, and `flex` / `grid` / `stack` /
144
+ // `card` / `container` are all listed here and all unjudged, because
145
+ // `@objectstack/spec@17` carries no entry for ANY Tier B layout primitive. So
146
+ // `box` joins a spec-less population five types deep rather than opening a
147
+ // new one; it needs no spec entry, and no gate needed loosening to accept it.
148
+ //
149
+ // ⚠️ And "not on this list" never meant "not on an authoring surface". `box`
150
+ // has been validated all along through the OTHER consumer of the same
151
+ // `inputs`: `components/src/renderers/layout/page.tsx` builds the JSX-page
152
+ // compiler's manifest from `getKnownTypes()`, registry-wide. What this list
153
+ // gates is `getPublicConfigs()` — the curated AI-authoring vocabulary, and
154
+ // through `sdui-parser/scripts/gen-manifest.ts` the published
155
+ // `sdui.manifest.json` and `sdui-intrinsics.d.ts`. THAT is the surface `box`
156
+ // was missing from, which is a narrower and truer statement of the gap than
157
+ // "the contract does not know it".
158
+ //
159
+ // The measurement is pinned next to the roster census in `apps/console`, over
160
+ // the DERIVED container population rather than over this list.
161
+ 'box',
105
162
  'page:header',
106
163
  'text',
107
164
  'image',
@@ -124,7 +124,7 @@ export function generateColorVars(colors) {
124
124
  // (`fontSize` / `fontWeight` / `lineHeight` / `letterSpacing`, plus
125
125
  // `fontFamily.heading` / `fontFamily.mono`) became TOMBSTONES: the schema now
126
126
  // rejects them by name and their prescription points at `theme.customVars`, the
127
- // declared — and since #5021 the only — door for a custom property. A
127
+ // declared — and since objectstack-ai/objectstack#5021 the only — door for a custom property. A
128
128
  // `--z-modal` or a `--duration-fast` is authored there now, emitted verbatim as
129
129
  // `--<key>: <value>`.
130
130
  //
@@ -0,0 +1,82 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * chart-category-key — the ONE answer to "which result column carries the
10
+ * CATEGORY?" for an object-bound chart (objectui#8269).
11
+ *
12
+ * The mirror of `chart-measure-key` (objectui#8266) on the other axis. That
13
+ * card landed `chartMeasureKey` over the contract's `chartAggregateValueKey`;
14
+ * the contract publishes a CATEGORY sibling in the same module —
15
+ * `chartAggregateCategoryKey` — and nothing in this repository read it.
16
+ *
17
+ * ## The disagreement this replaces
18
+ *
19
+ * Both dashboard relays composed the category binding as a LITERAL FLOOR:
20
+ *
21
+ * const xAxisKey = options.xField || 'name';
22
+ *
23
+ * and handed it to the `object-chart` node without ever consulting the
24
+ * `aggregate` that decides it. An object-bound aggregate returns one row per
25
+ * group, keyed by the raw `groupBy` field, so a widget declaring
26
+ * `aggregate: { function: 'count', groupBy: 'status' }` and no `options.xField`
27
+ * bound `'name'` against rows keyed `'status'`.
28
+ *
29
+ * Unlike the measure half, this one is LOUD: `hasNoCategoryKey`
30
+ * (`plugin-charts/src/AdvancedChartImpl.tsx`, framework#4033) fires and the
31
+ * author reads "This chart cannot plot its category axis: no row has a `name`
32
+ * field." The diagnostic is wrong-CAUSE — it names a binding the author never
33
+ * wrote and never mentions the `groupBy` they did — so it points at the wrong
34
+ * layer. Being loud makes it a lesser harm than objectui#8266, not a
35
+ * non-defect: measured over the rows a fieldless count returns,
36
+ * `[{status:'open',count:2},{status:'paid',count:5}]`, an `xAxisKey` of
37
+ * `'name'` refuses under BOTH series bindings, i.e. objectui#8266's fix does
38
+ * not reach this authoring shape at all.
39
+ *
40
+ * ## Why the answer is delegated rather than restated
41
+ *
42
+ * Restating the rule here would make this file a second opinion of a question
43
+ * the contract already answers — the objectui#5042 / #7544 / #8193 / #8168
44
+ * drift shape that `chart-measure-key` exists to end. So the rule stays
45
+ * upstream in `@objectstack/spec/ui` and this is the seam objectui-side callers
46
+ * share.
47
+ *
48
+ * ## ⚠️ NOT the same function as `resolveChartCategoryField`
49
+ *
50
+ * `plugin-charts`' own `resolveChartCategoryField` (objectui#8168) reads
51
+ * `aggregate.groupBy` first too — which is exactly why `ObjectChart` does NOT
52
+ * refuse this shape at its own level, and then forwards `schema.xAxisKey`
53
+ * verbatim as the render binding anyway. But the two answer DIFFERENT
54
+ * questions and must not be collapsed:
55
+ *
56
+ * - `resolveChartCategoryField` answers "which FIELD is the category?" — its
57
+ * structured-`groupBy` leg returns `node.field`, because its two readers
58
+ * are the refusal (does the author name a category at all?) and the
59
+ * field-metadata probe that loads that field's option labels and colours.
60
+ * - this function answers "which COLUMN do the returned rows carry it
61
+ * under?" — `groupBy.alias ?? groupBy.field` per the contract, because an
62
+ * `alias` (admitted by `ChartGroupBySchema`) renames the projected column.
63
+ *
64
+ * They coincide whenever no alias is written, which is why the distinction is
65
+ * easy to miss; conflating them would either bind an axis to a column the rows
66
+ * do not carry, or probe field metadata for a field that does not exist.
67
+ */
68
+ import { type ChartAggregateLike } from '@objectstack/spec/ui';
69
+ /**
70
+ * The result column a chart's category axis / x-axis binding must name.
71
+ *
72
+ * `fallback` is the caller's own floor, used ONLY when the contract has no
73
+ * answer — a chart that declares no `aggregate` at all (its rows are raw
74
+ * records or authored literals, where the author's `xField` is the right key),
75
+ * an UNGROUPED aggregate (one row, no category column), or a `groupBy` shape
76
+ * `ChartGroupBySchema` already rejects. It is never a second opinion about an
77
+ * aggregate the contract CAN answer for.
78
+ *
79
+ * @param aggregate the chart's inline aggregate, or `undefined`
80
+ * @param fallback the key to bind when the contract has no answer
81
+ */
82
+ export declare function chartCategoryKey(aggregate: ChartAggregateLike | undefined, fallback: string): string;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * chart-category-key — the ONE answer to "which result column carries the
10
+ * CATEGORY?" for an object-bound chart (objectui#8269).
11
+ *
12
+ * The mirror of `chart-measure-key` (objectui#8266) on the other axis. That
13
+ * card landed `chartMeasureKey` over the contract's `chartAggregateValueKey`;
14
+ * the contract publishes a CATEGORY sibling in the same module —
15
+ * `chartAggregateCategoryKey` — and nothing in this repository read it.
16
+ *
17
+ * ## The disagreement this replaces
18
+ *
19
+ * Both dashboard relays composed the category binding as a LITERAL FLOOR:
20
+ *
21
+ * const xAxisKey = options.xField || 'name';
22
+ *
23
+ * and handed it to the `object-chart` node without ever consulting the
24
+ * `aggregate` that decides it. An object-bound aggregate returns one row per
25
+ * group, keyed by the raw `groupBy` field, so a widget declaring
26
+ * `aggregate: { function: 'count', groupBy: 'status' }` and no `options.xField`
27
+ * bound `'name'` against rows keyed `'status'`.
28
+ *
29
+ * Unlike the measure half, this one is LOUD: `hasNoCategoryKey`
30
+ * (`plugin-charts/src/AdvancedChartImpl.tsx`, framework#4033) fires and the
31
+ * author reads "This chart cannot plot its category axis: no row has a `name`
32
+ * field." The diagnostic is wrong-CAUSE — it names a binding the author never
33
+ * wrote and never mentions the `groupBy` they did — so it points at the wrong
34
+ * layer. Being loud makes it a lesser harm than objectui#8266, not a
35
+ * non-defect: measured over the rows a fieldless count returns,
36
+ * `[{status:'open',count:2},{status:'paid',count:5}]`, an `xAxisKey` of
37
+ * `'name'` refuses under BOTH series bindings, i.e. objectui#8266's fix does
38
+ * not reach this authoring shape at all.
39
+ *
40
+ * ## Why the answer is delegated rather than restated
41
+ *
42
+ * Restating the rule here would make this file a second opinion of a question
43
+ * the contract already answers — the objectui#5042 / #7544 / #8193 / #8168
44
+ * drift shape that `chart-measure-key` exists to end. So the rule stays
45
+ * upstream in `@objectstack/spec/ui` and this is the seam objectui-side callers
46
+ * share.
47
+ *
48
+ * ## ⚠️ NOT the same function as `resolveChartCategoryField`
49
+ *
50
+ * `plugin-charts`' own `resolveChartCategoryField` (objectui#8168) reads
51
+ * `aggregate.groupBy` first too — which is exactly why `ObjectChart` does NOT
52
+ * refuse this shape at its own level, and then forwards `schema.xAxisKey`
53
+ * verbatim as the render binding anyway. But the two answer DIFFERENT
54
+ * questions and must not be collapsed:
55
+ *
56
+ * - `resolveChartCategoryField` answers "which FIELD is the category?" — its
57
+ * structured-`groupBy` leg returns `node.field`, because its two readers
58
+ * are the refusal (does the author name a category at all?) and the
59
+ * field-metadata probe that loads that field's option labels and colours.
60
+ * - this function answers "which COLUMN do the returned rows carry it
61
+ * under?" — `groupBy.alias ?? groupBy.field` per the contract, because an
62
+ * `alias` (admitted by `ChartGroupBySchema`) renames the projected column.
63
+ *
64
+ * They coincide whenever no alias is written, which is why the distinction is
65
+ * easy to miss; conflating them would either bind an axis to a column the rows
66
+ * do not carry, or probe field metadata for a field that does not exist.
67
+ */
68
+ import { chartAggregateCategoryKey } from '@objectstack/spec/ui';
69
+ /**
70
+ * The result column a chart's category axis / x-axis binding must name.
71
+ *
72
+ * `fallback` is the caller's own floor, used ONLY when the contract has no
73
+ * answer — a chart that declares no `aggregate` at all (its rows are raw
74
+ * records or authored literals, where the author's `xField` is the right key),
75
+ * an UNGROUPED aggregate (one row, no category column), or a `groupBy` shape
76
+ * `ChartGroupBySchema` already rejects. It is never a second opinion about an
77
+ * aggregate the contract CAN answer for.
78
+ *
79
+ * @param aggregate the chart's inline aggregate, or `undefined`
80
+ * @param fallback the key to bind when the contract has no answer
81
+ */
82
+ export function chartCategoryKey(aggregate, fallback) {
83
+ return chartAggregateCategoryKey(aggregate) ?? fallback;
84
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * chart-measure-key — the ONE answer to "which result column carries the
10
+ * measure?" for an object-bound chart (objectui#8266).
11
+ *
12
+ * ## The disagreement this replaces
13
+ *
14
+ * The question was answered independently in three places, and for a FIELDLESS
15
+ * `count` — the normal way to author "how many records per status", and the
16
+ * most common dashboard chart there is — two of them answered differently:
17
+ *
18
+ * - the ROW PROJECTION (`aggregateValueKey` / `aggregateRecords` /
19
+ * `runAggregate`'s alias, all in `plugin-charts/src/ObjectChart.tsx`) keys
20
+ * the value column `'count'`, the alias the engine projects `COUNT(*)`
21
+ * under;
22
+ * - the SERIES BINDING, spelled twice in `plugin-dashboard`
23
+ * (`DashboardGridLayout.tsx` and `DashboardRenderer.tsx`), read
24
+ * `aggregate.field || (options.yField || 'value')` and so bound `'value'`.
25
+ *
26
+ * A `dataKey` naming a column no row carries plots nothing, and nothing says
27
+ * so: measured on `origin/main` `0fa7a9c83`, rows `[{status:'open',count:2},
28
+ * {status:'paid',count:5}]` with `series:[{dataKey:'value'}]` rendered a
29
+ * `.recharts-surface` with the category ticks "open"/"paid", **0 bars, 0
30
+ * rectangles**, no refusal and no empty state — the same rows under
31
+ * `dataKey:'count'` drew 2 rectangles. An empty chart reads as "no data yet",
32
+ * which is exactly what an author with a genuinely empty object also sees.
33
+ *
34
+ * ## Why the answer is delegated rather than restated
35
+ *
36
+ * `chartAggregateValueKey` in `@objectstack/spec/ui` is the CONTRACT's own
37
+ * derivation — "the VALUE column an object-bound aggregate produces … what a
38
+ * chart's series / y-axis binding must name". Producers and checkers already
39
+ * read it. Restating the rule here would make this file a fourth opinion of the
40
+ * same question, which is the shape (objectui#5042 / #7544 / #8193 / #8168)
41
+ * this module exists to end. So the rule lives upstream in the spec and this is
42
+ * the seam objectui-side callers share, exactly as `humanizeLabel` (objectui#5444)
43
+ * and `buildChartSeries` (ADR-0021) already are.
44
+ *
45
+ * ⛔ The inverse fix — making the row projection key a fieldless count
46
+ * `'value'` — is not available: it contradicts a pin the tree already carries
47
+ * (`ObjectChart.aggregateResultColumns.test.ts`) and renames the column every
48
+ * other consumer reads, including the engine's own `COUNT(*)` alias.
49
+ */
50
+ import { type ChartAggregateLike } from '@objectstack/spec/ui';
51
+ export type { ChartAggregateLike };
52
+ /**
53
+ * The result column a chart's series must bind to.
54
+ *
55
+ * `fallback` is the caller's own floor, used ONLY when the contract has no
56
+ * answer — a chart that declares no `aggregate` at all (its rows are raw
57
+ * records or authored literals, where the author's `yField` is the right key),
58
+ * or an aggregate shape `ChartAggregateSchema` already rejects. It is never a
59
+ * second opinion about an aggregate the contract CAN answer for.
60
+ *
61
+ * @param aggregate the chart's inline aggregate, or `undefined`
62
+ * @param fallback the key to bind when the contract has no answer
63
+ */
64
+ export declare function chartMeasureKey(aggregate: ChartAggregateLike | undefined, fallback: string): string;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * chart-measure-key — the ONE answer to "which result column carries the
10
+ * measure?" for an object-bound chart (objectui#8266).
11
+ *
12
+ * ## The disagreement this replaces
13
+ *
14
+ * The question was answered independently in three places, and for a FIELDLESS
15
+ * `count` — the normal way to author "how many records per status", and the
16
+ * most common dashboard chart there is — two of them answered differently:
17
+ *
18
+ * - the ROW PROJECTION (`aggregateValueKey` / `aggregateRecords` /
19
+ * `runAggregate`'s alias, all in `plugin-charts/src/ObjectChart.tsx`) keys
20
+ * the value column `'count'`, the alias the engine projects `COUNT(*)`
21
+ * under;
22
+ * - the SERIES BINDING, spelled twice in `plugin-dashboard`
23
+ * (`DashboardGridLayout.tsx` and `DashboardRenderer.tsx`), read
24
+ * `aggregate.field || (options.yField || 'value')` and so bound `'value'`.
25
+ *
26
+ * A `dataKey` naming a column no row carries plots nothing, and nothing says
27
+ * so: measured on `origin/main` `0fa7a9c83`, rows `[{status:'open',count:2},
28
+ * {status:'paid',count:5}]` with `series:[{dataKey:'value'}]` rendered a
29
+ * `.recharts-surface` with the category ticks "open"/"paid", **0 bars, 0
30
+ * rectangles**, no refusal and no empty state — the same rows under
31
+ * `dataKey:'count'` drew 2 rectangles. An empty chart reads as "no data yet",
32
+ * which is exactly what an author with a genuinely empty object also sees.
33
+ *
34
+ * ## Why the answer is delegated rather than restated
35
+ *
36
+ * `chartAggregateValueKey` in `@objectstack/spec/ui` is the CONTRACT's own
37
+ * derivation — "the VALUE column an object-bound aggregate produces … what a
38
+ * chart's series / y-axis binding must name". Producers and checkers already
39
+ * read it. Restating the rule here would make this file a fourth opinion of the
40
+ * same question, which is the shape (objectui#5042 / #7544 / #8193 / #8168)
41
+ * this module exists to end. So the rule lives upstream in the spec and this is
42
+ * the seam objectui-side callers share, exactly as `humanizeLabel` (objectui#5444)
43
+ * and `buildChartSeries` (ADR-0021) already are.
44
+ *
45
+ * ⛔ The inverse fix — making the row projection key a fieldless count
46
+ * `'value'` — is not available: it contradicts a pin the tree already carries
47
+ * (`ObjectChart.aggregateResultColumns.test.ts`) and renames the column every
48
+ * other consumer reads, including the engine's own `COUNT(*)` alias.
49
+ */
50
+ import { chartAggregateValueKey } from '@objectstack/spec/ui';
51
+ /**
52
+ * The result column a chart's series must bind to.
53
+ *
54
+ * `fallback` is the caller's own floor, used ONLY when the contract has no
55
+ * answer — a chart that declares no `aggregate` at all (its rows are raw
56
+ * records or authored literals, where the author's `yField` is the right key),
57
+ * or an aggregate shape `ChartAggregateSchema` already rejects. It is never a
58
+ * second opinion about an aggregate the contract CAN answer for.
59
+ *
60
+ * @param aggregate the chart's inline aggregate, or `undefined`
61
+ * @param fallback the key to bind when the contract has no answer
62
+ */
63
+ export function chartMeasureKey(aggregate, fallback) {
64
+ return chartAggregateValueKey(aggregate) ?? fallback;
65
+ }
@@ -5,58 +5,6 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
- /**
9
- * chart-presentation — the AUTHORED half of a dataset-bound chart, lowered onto
10
- * the bindings {@link buildChartSeries} derived from the dataset selection.
11
- *
12
- * `chart-series.ts` beside this file owns the DATA half: which columns become
13
- * series, which rows, which buckets. This file owns everything the author gets
14
- * to say about how that data LOOKS — the ruled data/presentation split of
15
- * objectui#4229, stated once:
16
- *
17
- * - **Data (derived, never forwarded)** — series MEMBERSHIP and the column each
18
- * binding reads. Concretely `buildChartSeries`' `dataKey`s, `xAxisKey`, and
19
- * the spec's two binding keys `ChartSeries.name` and `ChartAxis.field`.
20
- * - **Presentation (authored, merged forward)** — everything else on those same
21
- * objects: `series[].type` (the per-series mark), `series[].yAxis` (which
22
- * axis it binds to), `label`/`color`/`stack`/`variant`/`dashArray`/`opacity`,
23
- * the axis definitions' `title`/`format`/`min`/`max`/`stepSize`/
24
- * `showGridLines`/`position`/`logarithmic`, and the chart chrome
25
- * (`showLegend`, `showDataLabels`, `colors`, `annotations`, …).
26
- *
27
- * ## Why it lives in `@object-ui/core` rather than in one plugin
28
- *
29
- * It was written for `plugin-dashboard`'s `DatasetWidget` (#3135 → objectstack#7016
30
- * → #4229) and lifted here by objectui#4877, when `plugin-report`'s embedded
31
- * report chart turned out to need the SAME merge over the same spec shapes: the
32
- * report was forwarding six keys and dropping every authored chrome key the
33
- * schema declares, and re-deriving this beside the dashboard's copy is exactly
34
- * the duplication objectui#4389 filed as a defect (two longhand copies of the
35
- * analytics label net, one per plugin, drifting apart).
36
- *
37
- * Everything here is a pure data transform over plain records, so it sits below
38
- * both plugins with no React and no i18n — the same layering `chart-series.ts`
39
- * already has (see {@link OptionLabelTranslator} there for why an i18n-resolved
40
- * string always arrives as an ARGUMENT rather than being read here).
41
- *
42
- * ## Two entry points, because the two surfaces declare axes differently
43
- *
44
- * A dashboard widget's `chartConfig` spells its axes as spec `ChartAxis`
45
- * OBJECTS. A report's `chart.xAxis` / `chart.yAxis` are bare dimension/measure
46
- * NAME strings — on that surface the axes are pure DATA (they ARE the selection)
47
- * and carry no presentation at all. So:
48
- *
49
- * - {@link mergeAuthoredPresentation} is the object-axis entry point: series
50
- * merge + axis presentation, for a caller whose axes are `ChartAxis` objects.
51
- * - {@link mergeAuthoredSeries} is the series merge ALONE, for a caller whose
52
- * axes are names. That split is structural, not a guard: handing a bare
53
- * string to {@link mergeAuthoredPresentation} would run it through
54
- * `axisPresentation`, which reads nothing off a string and would synthesise a
55
- * `yAxis: [{}]` entry — a y-axis declaring nothing but its own existence,
56
- * which is precisely how the COUNT of entries turns on a secondary axis. A
57
- * surface that cannot author axis presentation should not be able to reach
58
- * the code that reads it.
59
- */
60
8
  import type { ChartSeriesBinding } from './chart-series.js';
61
9
  /** Authored spec `ChartSeries` presentation, in the renderer's internal spelling. */
62
10
  export interface AuthoredSeriesPresentation {
@@ -76,39 +24,25 @@ export type MergedChartSeries = ChartSeriesBinding & AuthoredSeriesPresentation;
76
24
  * One authored `ChartSeries`, minus its `name` — i.e. everything about it that
77
25
  * is presentation rather than membership.
78
26
  *
79
- * `type` is narrowed to the three families that COMPOSE on one cartesian plot,
80
- * because this array reaches the renderer already speaking the internal shape
81
- * (`ChartRenderer` forwards a `dataKey`-shaped array untouched, so
82
- * `normalizeChartSchema`'s own identical narrowing never sees it). Without the
83
- * narrowing a `type: 'pie'` would not merely be inert — it would count as a
84
- * family disagreement in `effectiveChartFamily`, flip the whole chart into a
85
- * combo, and then draw that series as a bar anyway.
27
+ * `type` is narrowed to the three families that COMPOSE on one cartesian plot.
28
+ * Without the narrowing a `type: 'pie'` would not merely be inert — it would
29
+ * count as a family disagreement in `effectiveChartFamily`, flip the whole
30
+ * chart into a combo, and then draw that series as a bar anyway.
31
+ *
32
+ * `ChartRenderer` now narrows the same way for every series it receives,
33
+ * `dataKey`-shaped or not (`01c27c431` fixed the fast path that used to
34
+ * forward a `dataKey`-shaped array straight through, so `normalizeChartSchema`'s
35
+ * own identical narrowing never saw it) — so this module is no longer that
36
+ * narrowing's only line of defense. It stays anyway:
37
+ * `AuthoredSeriesPresentation.chartType` is a typed, narrower field
38
+ * (`'bar' | 'line' | 'area'`) this module's own callers read directly, and the
39
+ * double narrowing downstream is idempotent.
86
40
  */
87
41
  export declare function seriesPresentation(raw: Record<string, unknown>): AuthoredSeriesPresentation;
88
- /**
89
- * One authored `ChartAxis`, minus its `field` — the axis's presentation.
90
- *
91
- * `field` is the one DATA key on an axis (it names the plotted column), and
92
- * dropping it here is what keeps membership with the dataset **structurally**
93
- * rather than by a guard: `normalizeChartSchema` synthesises series out of
94
- * `yAxis[].field` when a chart declares no series, so a forwarded `field`
95
- * would be a live membership channel on an empty selection. With it gone the
96
- * axis carries scale and chrome only, and the count of entries — which is what
97
- * turns on the secondary axis (`yAxes.length > 1`) — survives, including for
98
- * an entry that declares nothing but its own existence.
99
- *
100
- * Keys the renderer does not read on a given axis are dropped by
101
- * `normalizeChartSchema`, the ONE normalization layer (#2880 S1): today it
102
- * keeps `format`/`title`/`showGridLines` on the x-axis and the full set on the
103
- * y-axes. That narrowing is deliberately NOT mirrored here — a second copy
104
- * would drift from the renderer's real capability the moment it grew.
105
- */
106
- export declare function axisPresentation(raw: unknown): Record<string, unknown>;
107
42
  /**
108
43
  * Merge an authored `series[]` array's PRESENTATION onto the bindings the
109
- * dataset selection derived — the series half of #4229's split, and the whole
110
- * of it for a surface whose axes are names rather than objects (see the file
111
- * header).
44
+ * dataset selection derived — the series half of #4229's split, and the only
45
+ * half this module lowers (see the file header).
112
46
  *
113
47
  * The match rule is **by name/key**: an authored `series[].name` is paired with
114
48
  * the derived binding whose `dataKey` it equals, and the pairing decides
@@ -136,28 +70,10 @@ export declare function axisPresentation(raw: unknown): Record<string, unknown>;
136
70
  */
137
71
  export declare function mergeAuthoredSeries(derived: ChartSeriesBinding[], authoredSeries: unknown): MergedChartSeries[];
138
72
  /**
139
- * Merge an authored chart config's PRESENTATION onto the series and axes the
140
- * dataset selection derived — the one place that happens for a surface whose
141
- * axes are spec `ChartAxis` OBJECTS (#4229).
142
- *
143
- * The series half is {@link mergeAuthoredSeries}; see it for the match rule and
144
- * for why membership is safe. The axes half reads `xAxis` / `yAxis` through
145
- * {@link axisPresentation}, which is why a surface spelling those as bare NAME
146
- * strings must call `mergeAuthoredSeries` directly instead (file header).
147
- *
148
- * @param derived the bindings {@link buildChartSeries} produced from the selection
149
- * @param raw the authored chart config (anything, incl. absent)
150
- * @returns the merged series, plus the presentation-only axes to spread onto
151
- * the chart schema (absent when the author declared none)
152
- */
153
- export declare function mergeAuthoredPresentation(derived: ChartSeriesBinding[], raw: unknown): {
154
- series: MergedChartSeries[];
155
- axes: Record<string, unknown>;
156
- };
157
- /**
158
- * Lower an authored chart config's CHROME (spec `ChartConfigSchema` /
159
- * `ReportChartSchema` — the same keys on both) onto the chart schema a
160
- * dataset-bound surface hands the renderer.
73
+ * Lower an authored chart config's CHROME (spec
74
+ * `DashboardWidgetChartConfigSchema` / `ReportChartSchema`, both extensions of
75
+ * `ChartConfigSchema` that keep its chrome keys unchanged) onto the chart
76
+ * schema a dataset-bound surface hands the renderer.
161
77
  *
162
78
  * ## Why this is a whitelist and not a spread
163
79
  *
@@ -181,20 +97,48 @@ export declare function mergeAuthoredPresentation(derived: ChartSeriesBinding[],
181
97
  * exists to remove.
182
98
  * 2. **It does not fight the dataset derivation.** `type` stays out: the
183
99
  * calling surface's own type already picks the chart family, which is the
184
- * dataset path's chart-family channel.
185
- *
186
- * `aria` is the one declared key with **no reader at all**: `AdvancedChartImpl`
187
- * has no `aria` prop, and `SchemaRenderer`'s ARIA injection reads the FLAT
188
- * `ariaLabel`/`ariaDescribedBy`/`role`, never a nested `aria` object. It is
189
- * therefore left unforwarded on purpose (criterion 1) rather than papered over
190
- * with a caller-side flattening that would also collide with the accessible
191
- * name `description` already sets — reported back to objectstack#5175's
192
- * narrowing half instead.
100
+ * dataset path's chart-family channel. The spec agrees on the dashboard
101
+ * carrier: since 17.5.0 `DashboardWidgetChartConfigSchema` refuses
102
+ * `chartConfig.type` at parse, with `xAxis` / `yAxis` / `series`, none of
103
+ * which this whitelist ever lowered (objectui#11315).
104
+ *
105
+ * `aria` is not lowered either. No renderer reads it: `AdvancedChartImpl` has
106
+ * no `aria` prop, and `SchemaRenderer`'s ARIA injection reads the FLAT
107
+ * `ariaLabel`/`ariaDescribedBy`/`role`, never a nested `aria` object (criterion
108
+ * 1). And it is no longer declared: spec 17.5.0 retired `ChartConfig.aria` as
109
+ * a `retiredKey` tombstone (objectstack#17751, objectui#4044), so an authored
110
+ * one is refused at parse. The accessible name a chart applies is
111
+ * `description`, above.
193
112
  *
194
113
  * `title` IS emitted here. A caller that paints the title itself (the report
195
114
  * renderer's own `h3` above the chart) must drop it from the result, or the
196
115
  * chart draws a second one.
197
116
  *
117
+ * ## `title` / `subtitle` / `description` are `I18nLabel`, and travel as such
118
+ *
119
+ * `ChartConfigSchema` types all three as the spec's `I18nLabel` union — a plain
120
+ * string OR an inline locale map — so all three are lowered by
121
+ * {@link forwardedI18nLabel}, which hands the authored value on untouched for
122
+ * the renderer to resolve against the viewer's language. Read that helper for
123
+ * why resolving cannot happen in this package.
124
+ *
125
+ * ⚠️ LEDGERED, by name, as still unresolved after objectui#9038 — not in
126
+ * that card's scope and not reached by the change above:
127
+ *
128
+ * - **A series `label`** still goes through {@link labelText}'s
129
+ * first-string-wins pick, the design objectui#4020 ledgered and a caller
130
+ * can override. Not reopened here.
131
+ *
132
+ * ✓ CLEARED, objectui#9150 — **the report renderer's own heading.**
133
+ * `DatasetReportChart` (`@object-ui/plugin-report`) still drops `title` from
134
+ * this result and still paints its own `h3`, but that read site no longer
135
+ * narrows `chart.title` to a plain string: it resolves the union through
136
+ * `pickLocalized` against the viewer's language, like every other `I18nLabel`
137
+ * on that surface. Kept here rather than deleted because the ledger entry is
138
+ * what the fixing card was dispatched from, and because the FIRST half of it —
139
+ * that a caller painting the title itself must drop `title` from this result —
140
+ * is a live constraint on this whitelist, stated above.
141
+ *
198
142
  * @param raw the authored chart config (anything, incl. absent)
199
143
  * @param fieldCategoryColors per-category colours resolved from the category
200
144
  * dimension's own select/lookup option colours, merged UNDER an explicit
@@ -203,3 +147,42 @@ export declare function mergeAuthoredPresentation(derived: ChartSeriesBinding[],
203
147
  * derived chart schema and every undeclared key keeps the renderer's default
204
148
  */
205
149
  export declare function chartConfigPresentation(raw: unknown, fieldCategoryColors?: Record<string, string> | null): Record<string, unknown>;
150
+ /**
151
+ * Whether a chart of this family IGNORES `compareTo` — the one declaration of
152
+ * that rule (objectui#7495).
153
+ *
154
+ * Every surface that turns a `compareTo` into a comparison reads this; none
155
+ * keeps a list of its own. The inline chart path (`@object-ui/plugin-charts`'
156
+ * ObjectChart) skips the comparison fetch and synthesises no overlay series for
157
+ * these families, and the dataset widget path (`@object-ui/plugin-dashboard`'s
158
+ * DatasetWidget) forwards no `compareTo` to the executor for a chart of one of
159
+ * them, so the comparison pass never runs and no overlay series is appended.
160
+ *
161
+ * - `pie` / `donut` / `funnel` — single-distribution charts: a comparison
162
+ * overlay has no meaning on them, and the renderer's pie and funnel arms
163
+ * draw only `series[0]`.
164
+ * - `scatter` (objectui#7402) — a scatter binds ONE measure: the renderer reads
165
+ * y through the single `YAxis dataKey={series[0].dataKey}`, so an overlay was
166
+ * painted on the PRIMARY's y and "previous period" landed exactly on top of
167
+ * "current". Drawing it honestly needs the multi-measure projection declined
168
+ * as option A of objectui#7194; `compareTo` on a scatter returns WITH that
169
+ * projection.
170
+ *
171
+ * It reads the RENDERER's chart family (`bar`, `pie`, `scatter`, …), not a
172
+ * dashboard widget type: a caller whose widget types alias a family (the
173
+ * dashboard maps `bubble` to `scatter` and `pyramid` to `funnel`) maps first and
174
+ * asks about the family. An absent or unrecognised family does NOT ignore
175
+ * `compareTo` — the comparison runs.
176
+ *
177
+ * Why it lives here: the dashboard reaches `@object-ui/plugin-charts` only as a
178
+ * devDependency, so this package — a runtime dependency of both — is the one
179
+ * place both paths can read. Two copies were how the lists drifted apart: the
180
+ * dashboard's said "scatter only" while the charts package said all four, so a
181
+ * compare-to pie widget ran a comparison query whose overlay the renderer then
182
+ * dropped (objectui#7495; objectui#4389 is the precedent that a second copy of
183
+ * chart-presentation logic is a defect).
184
+ *
185
+ * @param chartType the chart family, or `undefined` when none is declared
186
+ * @returns `true` for `pie`, `donut`, `funnel` and `scatter`; `false` otherwise
187
+ */
188
+ export declare function chartTypeIgnoresCompareTo(chartType: string | undefined): boolean;