@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.
- package/CHANGELOG.md +6610 -0
- package/README.md +101 -14
- package/dist/actions/ActionEngine.d.ts +4 -2
- package/dist/actions/ActionEngine.js +13 -8
- package/dist/actions/ActionRunner.d.ts +288 -27
- package/dist/actions/ActionRunner.js +443 -63
- package/dist/actions/TransactionManager.d.ts +7 -2
- package/dist/actions/actionKeys.d.ts +9 -7
- package/dist/actions/actionKeys.js +57 -14
- package/dist/actions/actionResponse.js +2 -2
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/actions/recordDelete.d.ts +118 -0
- package/dist/actions/recordDelete.js +112 -0
- package/dist/adapters/ValueDataSource.d.ts +6 -1
- package/dist/adapters/ValueDataSource.js +1090 -98
- package/dist/adapters/batchTransaction.d.ts +1 -1
- package/dist/adapters/batchTransaction.js +1 -1
- package/dist/builder/schema-builder.d.ts +38 -81
- package/dist/builder/schema-builder.js +33 -152
- package/dist/data-scope/ViewDataProvider.d.ts +1 -1
- package/dist/data-scope/ViewDataProvider.js +21 -1
- package/dist/data-scope/element-data-source.d.ts +261 -7
- package/dist/data-scope/element-data-source.js +342 -52
- package/dist/data-scope/index.d.ts +9 -4
- package/dist/data-scope/index.js +9 -4
- package/dist/evaluator/ExpressionEvaluator.d.ts +86 -0
- package/dist/evaluator/ExpressionEvaluator.js +184 -12
- package/dist/evaluator/FormulaFunctions.d.ts +0 -10
- package/dist/evaluator/FormulaFunctions.js +166 -27
- package/dist/evaluator/declaredPredicate.d.ts +93 -9
- package/dist/evaluator/declaredPredicate.js +117 -16
- package/dist/evaluator/fieldRules.d.ts +88 -4
- package/dist/evaluator/fieldRules.js +203 -31
- package/dist/evaluator/index.d.ts +1 -0
- package/dist/evaluator/index.js +1 -0
- package/dist/evaluator/listConditional.d.ts +103 -34
- package/dist/evaluator/listConditional.js +127 -50
- package/dist/evaluator/optionRules.d.ts +77 -6
- package/dist/evaluator/optionRules.js +87 -10
- package/dist/evaluator/predicateInput.d.ts +15 -4
- package/dist/evaluator/predicateInput.js +35 -2
- package/dist/evaluator/rowPredicateCanon.d.ts +7 -18
- package/dist/evaluator/rowPredicateCanon.js +65 -115
- package/dist/evaluator/subjectPermissions.d.ts +76 -0
- package/dist/evaluator/subjectPermissions.js +44 -0
- package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
- package/dist/evaluator/unevaluablePredicate.js +70 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +80 -2
- package/dist/protocols/index.d.ts +0 -1
- package/dist/protocols/index.js +9 -1
- package/dist/registry/Registry.d.ts +358 -31
- package/dist/registry/Registry.js +263 -11
- package/dist/registry/WidgetRegistry.js +7 -3
- package/dist/registry/html-tier-intrinsics.d.ts +90 -0
- package/dist/registry/html-tier-intrinsics.js +110 -0
- package/dist/registry/public-blocks.d.ts +8 -0
- package/dist/registry/public-blocks.js +57 -0
- package/dist/theme/ThemeEngine.js +1 -1
- package/dist/utils/chart-category-key.d.ts +82 -0
- package/dist/utils/chart-category-key.js +84 -0
- package/dist/utils/chart-measure-key.d.ts +64 -0
- package/dist/utils/chart-measure-key.js +65 -0
- package/dist/utils/chart-presentation.d.ts +95 -112
- package/dist/utils/chart-presentation.js +169 -104
- package/dist/utils/chart-series.d.ts +131 -5
- package/dist/utils/chart-series.js +98 -16
- package/dist/utils/column-sortability.d.ts +157 -0
- package/dist/utils/column-sortability.js +217 -0
- package/dist/utils/dashboard-filters.d.ts +38 -6
- package/dist/utils/dashboard-filters.js +107 -61
- package/dist/utils/dataset-format.d.ts +211 -30
- package/dist/utils/dataset-format.js +389 -46
- package/dist/utils/date-display.d.ts +354 -0
- package/dist/utils/date-display.js +463 -0
- package/dist/utils/date-macros.js +7 -0
- package/dist/utils/drill-down.d.ts +78 -0
- package/dist/utils/drill-down.js +93 -0
- package/dist/utils/emptiness.d.ts +96 -0
- package/dist/utils/emptiness.js +101 -0
- package/dist/utils/expand-fields.d.ts +76 -6
- package/dist/utils/expand-fields.js +76 -6
- package/dist/utils/extract-records.d.ts +65 -5
- package/dist/utils/extract-records.js +65 -8
- package/dist/utils/filter-converter.d.ts +152 -4
- package/dist/utils/filter-converter.js +1092 -22
- package/dist/utils/filter-tokens.d.ts +49 -5
- package/dist/utils/filter-tokens.js +105 -33
- package/dist/utils/grouping-fields.d.ts +61 -0
- package/dist/utils/grouping-fields.js +84 -0
- package/dist/utils/managedBy.d.ts +3 -3
- package/dist/utils/managedBy.js +5 -5
- package/dist/utils/native-date-value.d.ts +48 -0
- package/dist/utils/native-date-value.js +167 -0
- package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
- package/dist/utils/non-grid-row-ceiling.js +141 -0
- package/dist/utils/normalize-list-view.d.ts +121 -1
- package/dist/utils/normalize-list-view.js +186 -6
- package/dist/utils/number-display.d.ts +63 -14
- package/dist/utils/number-display.js +34 -12
- package/dist/utils/object-aggregate-query.d.ts +113 -0
- package/dist/utils/object-aggregate-query.js +50 -0
- package/dist/utils/parent-scope.d.ts +123 -0
- package/dist/utils/parent-scope.js +140 -0
- package/dist/utils/predicate-fields.d.ts +45 -12
- package/dist/utils/predicate-fields.js +137 -12
- package/dist/utils/record-color.d.ts +75 -0
- package/dist/utils/record-color.js +65 -0
- package/dist/utils/record-source.d.ts +298 -0
- package/dist/utils/record-source.js +331 -0
- package/dist/utils/record-title.d.ts +148 -8
- package/dist/utils/record-title.js +220 -26
- package/dist/utils/reference-keys.d.ts +21 -22
- package/dist/utils/reference-keys.js +532 -29
- package/dist/utils/sort-query.d.ts +115 -15
- package/dist/utils/sort-query.js +240 -13
- package/dist/utils/text-comparand.d.ts +92 -0
- package/dist/utils/text-comparand.js +99 -0
- package/dist/utils/without-denied-fields.d.ts +49 -0
- package/dist/utils/without-denied-fields.js +52 -0
- package/dist/validation/index.d.ts +8 -4
- package/dist/validation/index.js +8 -4
- package/dist/validation/schema-validator.d.ts +1 -1
- package/dist/validation/schema-validator.js +200 -82
- package/dist/validation/validators/object-validation-engine.d.ts +1 -1
- package/dist/validation/validators/object-validation-engine.js +14 -3
- package/package.json +8 -9
- package/dist/data-scope/DataScopeManager.d.ts +0 -127
- package/dist/data-scope/DataScopeManager.js +0 -229
- package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
- package/dist/protocols/ResponsiveProtocol.js +0 -158
- package/dist/validation/validation-engine.d.ts +0 -88
- package/dist/validation/validation-engine.js +0 -434
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
const isRecord = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
9
9
|
/**
|
|
10
10
|
* An i18n label is a plain string or a `{ en, zh-CN, … }` record; charts render
|
|
11
|
-
* a string.
|
|
12
|
-
*
|
|
11
|
+
* a string.
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ Its one caller is {@link seriesPresentation}'s `label`. An axis `title`
|
|
14
|
+
* read this too until `061f5e829` moved it off the pick, and the axis lowering
|
|
15
|
+
* itself is gone since objectui#11372 (file header).
|
|
13
16
|
*
|
|
14
17
|
* **First-string-wins, deliberately, and deliberately NOT locale-aware** — this
|
|
15
18
|
* package is React-free and holds no i18n provider, so it cannot know which
|
|
@@ -27,17 +30,83 @@ function labelText(v) {
|
|
|
27
30
|
}
|
|
28
31
|
return undefined;
|
|
29
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* An authored `I18nLabel` that travels to the renderer **unresolved** — the
|
|
35
|
+
* chart's own `title` / `subtitle` / `description` (objectui#9038).
|
|
36
|
+
*
|
|
37
|
+
* ## Why this does not resolve, and why that is not {@link labelText}'s answer
|
|
38
|
+
*
|
|
39
|
+
* These three keys are lowered onto a chart SCHEMA slot that already declares
|
|
40
|
+
* the union: `normalizeChartSchema` (`@object-ui/plugin-charts`) resolves
|
|
41
|
+
* `title`/`subtitle`/`description` through `pickLocalized` with the viewer's
|
|
42
|
+
* language, which `ChartRenderer` reads from `useObjectTranslation` and hands
|
|
43
|
+
* down (objectui#8943), and `ObjectChart` reads `schema.title` through the same
|
|
44
|
+
* resolver for its drill heading. So the value's one resolution point is
|
|
45
|
+
* already downstream of here, holding the one thing this package does not have
|
|
46
|
+
* — the viewer. Forwarding is what lets that resolver see the value at all.
|
|
47
|
+
*
|
|
48
|
+
* Resolving here is not merely unnecessary, it is unavailable: `pickLocalized`
|
|
49
|
+
* lives in `@object-ui/i18n`, which DEPENDS on this package. Declaring the
|
|
50
|
+
* reverse edge makes the build graph cyclic (measured: `turbo run build
|
|
51
|
+
* --filter=@object-ui/core` refuses with `Cyclic dependency detected`), and
|
|
52
|
+
* that package's entry point is a React provider plus hooks — the layering
|
|
53
|
+
* this file's header states, arrived at from the other side.
|
|
54
|
+
*
|
|
55
|
+
* ⛔ Do NOT "fix" this by reusing {@link labelText}. The two answer different
|
|
56
|
+
* questions and only one of them is a choice:
|
|
57
|
+
*
|
|
58
|
+
* - `labelText` is a locale-unaware PICK for a slot whose consumer takes a
|
|
59
|
+
* plain string. It is wrong for a zh console and is ledgered as such
|
|
60
|
+
* (objectui#4020) because a caller that CAN resolve the language overrides
|
|
61
|
+
* the merged label — the mitigation depends on the value still existing.
|
|
62
|
+
* - The guard this replaced (`typeof v === 'string' && v ? v : undefined`)
|
|
63
|
+
* offered no such route: it ERASED the map arm, the `if (title)` guard then
|
|
64
|
+
* skipped the assignment, and the chart drew **no heading at all**, in every
|
|
65
|
+
* language, with no diagnostic. Nothing downstream could override a key that
|
|
66
|
+
* never arrived.
|
|
67
|
+
*
|
|
68
|
+
* ## The admission test, and what it deliberately does not decide
|
|
69
|
+
*
|
|
70
|
+
* A plain string, or a record carrying at least one usable string entry. That
|
|
71
|
+
* is the same "is there anything here" question the `if (title)` truthiness
|
|
72
|
+
* guard always asked, extended to the map arm — it does NOT decide which limb
|
|
73
|
+
* wins, which stays `pickLocalized`'s alone. Everything else (a number, a
|
|
74
|
+
* boolean, an array, `''`, `{}`, a record with no string value) is refused
|
|
75
|
+
* exactly as before, so no key reaches the caller that cannot render, and the
|
|
76
|
+
* `@returns` contract below — only keys that resolved — still holds.
|
|
77
|
+
*
|
|
78
|
+
* The map is forwarded VERBATIM: `InlineLocaleMapSchema` is
|
|
79
|
+
* `z.record(<tag>, z.string())` and enforcing that belongs at the parse, not in
|
|
80
|
+
* a renderer-side coercion (AGENTS.md #0.1). A non-string entry falls through
|
|
81
|
+
* `pickLocalized`'s limbs exactly as an absent one does, which is why admitting
|
|
82
|
+
* the record on the strength of one usable entry cannot paint `[object
|
|
83
|
+
* Object]`.
|
|
84
|
+
*/
|
|
85
|
+
function forwardedI18nLabel(v) {
|
|
86
|
+
if (typeof v === 'string')
|
|
87
|
+
return v || undefined;
|
|
88
|
+
if (isRecord(v) && Object.values(v).some((x) => typeof x === 'string' && x)) {
|
|
89
|
+
return v;
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
30
93
|
/**
|
|
31
94
|
* One authored `ChartSeries`, minus its `name` — i.e. everything about it that
|
|
32
95
|
* is presentation rather than membership.
|
|
33
96
|
*
|
|
34
|
-
* `type` is narrowed to the three families that COMPOSE on one cartesian plot
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
97
|
+
* `type` is narrowed to the three families that COMPOSE on one cartesian plot.
|
|
98
|
+
* Without the narrowing a `type: 'pie'` would not merely be inert — it would
|
|
99
|
+
* count as a family disagreement in `effectiveChartFamily`, flip the whole
|
|
100
|
+
* chart into a combo, and then draw that series as a bar anyway.
|
|
101
|
+
*
|
|
102
|
+
* `ChartRenderer` now narrows the same way for every series it receives,
|
|
103
|
+
* `dataKey`-shaped or not (`01c27c431` fixed the fast path that used to
|
|
104
|
+
* forward a `dataKey`-shaped array straight through, so `normalizeChartSchema`'s
|
|
105
|
+
* own identical narrowing never saw it) — so this module is no longer that
|
|
106
|
+
* narrowing's only line of defense. It stays anyway:
|
|
107
|
+
* `AuthoredSeriesPresentation.chartType` is a typed, narrower field
|
|
108
|
+
* (`'bar' | 'line' | 'area'`) this module's own callers read directly, and the
|
|
109
|
+
* double narrowing downstream is idempotent.
|
|
41
110
|
*/
|
|
42
111
|
export function seriesPresentation(raw) {
|
|
43
112
|
const out = {};
|
|
@@ -61,54 +130,10 @@ export function seriesPresentation(raw) {
|
|
|
61
130
|
out.opacity = raw.opacity;
|
|
62
131
|
return out;
|
|
63
132
|
}
|
|
64
|
-
/**
|
|
65
|
-
* One authored `ChartAxis`, minus its `field` — the axis's presentation.
|
|
66
|
-
*
|
|
67
|
-
* `field` is the one DATA key on an axis (it names the plotted column), and
|
|
68
|
-
* dropping it here is what keeps membership with the dataset **structurally**
|
|
69
|
-
* rather than by a guard: `normalizeChartSchema` synthesises series out of
|
|
70
|
-
* `yAxis[].field` when a chart declares no series, so a forwarded `field`
|
|
71
|
-
* would be a live membership channel on an empty selection. With it gone the
|
|
72
|
-
* axis carries scale and chrome only, and the count of entries — which is what
|
|
73
|
-
* turns on the secondary axis (`yAxes.length > 1`) — survives, including for
|
|
74
|
-
* an entry that declares nothing but its own existence.
|
|
75
|
-
*
|
|
76
|
-
* Keys the renderer does not read on a given axis are dropped by
|
|
77
|
-
* `normalizeChartSchema`, the ONE normalization layer (#2880 S1): today it
|
|
78
|
-
* keeps `format`/`title`/`showGridLines` on the x-axis and the full set on the
|
|
79
|
-
* y-axes. That narrowing is deliberately NOT mirrored here — a second copy
|
|
80
|
-
* would drift from the renderer's real capability the moment it grew.
|
|
81
|
-
*/
|
|
82
|
-
export function axisPresentation(raw) {
|
|
83
|
-
const out = {};
|
|
84
|
-
if (!isRecord(raw))
|
|
85
|
-
return out;
|
|
86
|
-
const title = labelText(raw.title);
|
|
87
|
-
if (title)
|
|
88
|
-
out.title = title;
|
|
89
|
-
if (typeof raw.format === 'string' && raw.format)
|
|
90
|
-
out.format = raw.format;
|
|
91
|
-
if (typeof raw.min === 'number' && Number.isFinite(raw.min))
|
|
92
|
-
out.min = raw.min;
|
|
93
|
-
if (typeof raw.max === 'number' && Number.isFinite(raw.max))
|
|
94
|
-
out.max = raw.max;
|
|
95
|
-
if (typeof raw.stepSize === 'number' && Number.isFinite(raw.stepSize) && raw.stepSize > 0) {
|
|
96
|
-
out.stepSize = raw.stepSize;
|
|
97
|
-
}
|
|
98
|
-
if (typeof raw.showGridLines === 'boolean')
|
|
99
|
-
out.showGridLines = raw.showGridLines;
|
|
100
|
-
if (raw.position === 'left' || raw.position === 'right' || raw.position === 'top' || raw.position === 'bottom') {
|
|
101
|
-
out.position = raw.position;
|
|
102
|
-
}
|
|
103
|
-
if (typeof raw.logarithmic === 'boolean')
|
|
104
|
-
out.logarithmic = raw.logarithmic;
|
|
105
|
-
return out;
|
|
106
|
-
}
|
|
107
133
|
/**
|
|
108
134
|
* Merge an authored `series[]` array's PRESENTATION onto the bindings the
|
|
109
|
-
* dataset selection derived — the series half of #4229's split, and the
|
|
110
|
-
*
|
|
111
|
-
* header).
|
|
135
|
+
* dataset selection derived — the series half of #4229's split, and the only
|
|
136
|
+
* half this module lowers (see the file header).
|
|
112
137
|
*
|
|
113
138
|
* The match rule is **by name/key**: an authored `series[].name` is paired with
|
|
114
139
|
* the derived binding whose `dataKey` it equals, and the pairing decides
|
|
@@ -149,43 +174,10 @@ export function mergeAuthoredSeries(derived, authoredSeries) {
|
|
|
149
174
|
});
|
|
150
175
|
}
|
|
151
176
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* The series half is {@link mergeAuthoredSeries}; see it for the match rule and
|
|
157
|
-
* for why membership is safe. The axes half reads `xAxis` / `yAxis` through
|
|
158
|
-
* {@link axisPresentation}, which is why a surface spelling those as bare NAME
|
|
159
|
-
* strings must call `mergeAuthoredSeries` directly instead (file header).
|
|
160
|
-
*
|
|
161
|
-
* @param derived the bindings {@link buildChartSeries} produced from the selection
|
|
162
|
-
* @param raw the authored chart config (anything, incl. absent)
|
|
163
|
-
* @returns the merged series, plus the presentation-only axes to spread onto
|
|
164
|
-
* the chart schema (absent when the author declared none)
|
|
165
|
-
*/
|
|
166
|
-
export function mergeAuthoredPresentation(derived, raw) {
|
|
167
|
-
const config = isRecord(raw) ? raw : {};
|
|
168
|
-
const series = mergeAuthoredSeries(derived, config.series);
|
|
169
|
-
const axes = {};
|
|
170
|
-
const xAxis = axisPresentation(config.xAxis);
|
|
171
|
-
if (Object.keys(xAxis).length > 0)
|
|
172
|
-
axes.xAxis = xAxis;
|
|
173
|
-
// The COUNT of y-axis entries is itself presentation — it is what declares a
|
|
174
|
-
// secondary axis — so every declared entry keeps its slot even when it
|
|
175
|
-
// carries nothing but `field` (which is data and does not travel).
|
|
176
|
-
const yAxisRaw = Array.isArray(config.yAxis)
|
|
177
|
-
? config.yAxis
|
|
178
|
-
: config.yAxis !== undefined
|
|
179
|
-
? [config.yAxis]
|
|
180
|
-
: [];
|
|
181
|
-
if (yAxisRaw.length > 0)
|
|
182
|
-
axes.yAxis = yAxisRaw.map(axisPresentation);
|
|
183
|
-
return { series, axes };
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Lower an authored chart config's CHROME (spec `ChartConfigSchema` /
|
|
187
|
-
* `ReportChartSchema` — the same keys on both) onto the chart schema a
|
|
188
|
-
* dataset-bound surface hands the renderer.
|
|
177
|
+
* Lower an authored chart config's CHROME (spec
|
|
178
|
+
* `DashboardWidgetChartConfigSchema` / `ReportChartSchema`, both extensions of
|
|
179
|
+
* `ChartConfigSchema` that keep its chrome keys unchanged) onto the chart
|
|
180
|
+
* schema a dataset-bound surface hands the renderer.
|
|
189
181
|
*
|
|
190
182
|
* ## Why this is a whitelist and not a spread
|
|
191
183
|
*
|
|
@@ -209,20 +201,48 @@ export function mergeAuthoredPresentation(derived, raw) {
|
|
|
209
201
|
* exists to remove.
|
|
210
202
|
* 2. **It does not fight the dataset derivation.** `type` stays out: the
|
|
211
203
|
* calling surface's own type already picks the chart family, which is the
|
|
212
|
-
* dataset path's chart-family channel.
|
|
204
|
+
* dataset path's chart-family channel. The spec agrees on the dashboard
|
|
205
|
+
* carrier: since 17.5.0 `DashboardWidgetChartConfigSchema` refuses
|
|
206
|
+
* `chartConfig.type` at parse, with `xAxis` / `yAxis` / `series`, none of
|
|
207
|
+
* which this whitelist ever lowered (objectui#11315).
|
|
213
208
|
*
|
|
214
|
-
* `aria` is
|
|
215
|
-
*
|
|
216
|
-
* `ariaLabel`/`ariaDescribedBy`/`role`, never a nested `aria` object
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
209
|
+
* `aria` is not lowered either. No renderer reads it: `AdvancedChartImpl` has
|
|
210
|
+
* no `aria` prop, and `SchemaRenderer`'s ARIA injection reads the FLAT
|
|
211
|
+
* `ariaLabel`/`ariaDescribedBy`/`role`, never a nested `aria` object (criterion
|
|
212
|
+
* 1). And it is no longer declared: spec 17.5.0 retired `ChartConfig.aria` as
|
|
213
|
+
* a `retiredKey` tombstone (objectstack#17751, objectui#4044), so an authored
|
|
214
|
+
* one is refused at parse. The accessible name a chart applies is
|
|
215
|
+
* `description`, above.
|
|
221
216
|
*
|
|
222
217
|
* `title` IS emitted here. A caller that paints the title itself (the report
|
|
223
218
|
* renderer's own `h3` above the chart) must drop it from the result, or the
|
|
224
219
|
* chart draws a second one.
|
|
225
220
|
*
|
|
221
|
+
* ## `title` / `subtitle` / `description` are `I18nLabel`, and travel as such
|
|
222
|
+
*
|
|
223
|
+
* `ChartConfigSchema` types all three as the spec's `I18nLabel` union — a plain
|
|
224
|
+
* string OR an inline locale map — so all three are lowered by
|
|
225
|
+
* {@link forwardedI18nLabel}, which hands the authored value on untouched for
|
|
226
|
+
* the renderer to resolve against the viewer's language. Read that helper for
|
|
227
|
+
* why resolving cannot happen in this package.
|
|
228
|
+
*
|
|
229
|
+
* ⚠️ LEDGERED, by name, as still unresolved after objectui#9038 — not in
|
|
230
|
+
* that card's scope and not reached by the change above:
|
|
231
|
+
*
|
|
232
|
+
* - **A series `label`** still goes through {@link labelText}'s
|
|
233
|
+
* first-string-wins pick, the design objectui#4020 ledgered and a caller
|
|
234
|
+
* can override. Not reopened here.
|
|
235
|
+
*
|
|
236
|
+
* ✓ CLEARED, objectui#9150 — **the report renderer's own heading.**
|
|
237
|
+
* `DatasetReportChart` (`@object-ui/plugin-report`) still drops `title` from
|
|
238
|
+
* this result and still paints its own `h3`, but that read site no longer
|
|
239
|
+
* narrows `chart.title` to a plain string: it resolves the union through
|
|
240
|
+
* `pickLocalized` against the viewer's language, like every other `I18nLabel`
|
|
241
|
+
* on that surface. Kept here rather than deleted because the ledger entry is
|
|
242
|
+
* what the fixing card was dispatched from, and because the FIRST half of it —
|
|
243
|
+
* that a caller painting the title itself must drop `title` from this result —
|
|
244
|
+
* is a live constraint on this whitelist, stated above.
|
|
245
|
+
*
|
|
226
246
|
* @param raw the authored chart config (anything, incl. absent)
|
|
227
247
|
* @param fieldCategoryColors per-category colours resolved from the category
|
|
228
248
|
* dimension's own select/lookup option colours, merged UNDER an explicit
|
|
@@ -233,18 +253,20 @@ export function mergeAuthoredPresentation(derived, raw) {
|
|
|
233
253
|
export function chartConfigPresentation(raw, fieldCategoryColors) {
|
|
234
254
|
const config = isRecord(raw) ? raw : {};
|
|
235
255
|
const out = {};
|
|
236
|
-
const text = (v) => (typeof v === 'string' && v ? v : undefined);
|
|
237
256
|
if (typeof config.showLegend === 'boolean')
|
|
238
257
|
out.showLegend = config.showLegend;
|
|
239
258
|
if (typeof config.showDataLabels === 'boolean')
|
|
240
259
|
out.showDataLabels = config.showDataLabels;
|
|
241
|
-
|
|
260
|
+
// The three `I18nLabel` slots, carried through unresolved — see
|
|
261
|
+
// {@link forwardedI18nLabel} for why this package forwards rather than picks,
|
|
262
|
+
// and for what is still ledgered as unresolved on purpose.
|
|
263
|
+
const title = forwardedI18nLabel(config.title);
|
|
242
264
|
if (title)
|
|
243
265
|
out.title = title;
|
|
244
|
-
const subtitle =
|
|
266
|
+
const subtitle = forwardedI18nLabel(config.subtitle);
|
|
245
267
|
if (subtitle)
|
|
246
268
|
out.subtitle = subtitle;
|
|
247
|
-
const description =
|
|
269
|
+
const description = forwardedI18nLabel(config.description);
|
|
248
270
|
if (description)
|
|
249
271
|
out.description = description;
|
|
250
272
|
// A non-positive height would collapse the plot; the container default is the
|
|
@@ -278,3 +300,46 @@ export function chartConfigPresentation(raw, fieldCategoryColors) {
|
|
|
278
300
|
}
|
|
279
301
|
return out;
|
|
280
302
|
}
|
|
303
|
+
/** The families {@link chartTypeIgnoresCompareTo} answers `true` for. */
|
|
304
|
+
const CHART_TYPES_IGNORING_COMPARE_TO = new Set(['pie', 'donut', 'funnel', 'scatter']);
|
|
305
|
+
/**
|
|
306
|
+
* Whether a chart of this family IGNORES `compareTo` — the one declaration of
|
|
307
|
+
* that rule (objectui#7495).
|
|
308
|
+
*
|
|
309
|
+
* Every surface that turns a `compareTo` into a comparison reads this; none
|
|
310
|
+
* keeps a list of its own. The inline chart path (`@object-ui/plugin-charts`'
|
|
311
|
+
* ObjectChart) skips the comparison fetch and synthesises no overlay series for
|
|
312
|
+
* these families, and the dataset widget path (`@object-ui/plugin-dashboard`'s
|
|
313
|
+
* DatasetWidget) forwards no `compareTo` to the executor for a chart of one of
|
|
314
|
+
* them, so the comparison pass never runs and no overlay series is appended.
|
|
315
|
+
*
|
|
316
|
+
* - `pie` / `donut` / `funnel` — single-distribution charts: a comparison
|
|
317
|
+
* overlay has no meaning on them, and the renderer's pie and funnel arms
|
|
318
|
+
* draw only `series[0]`.
|
|
319
|
+
* - `scatter` (objectui#7402) — a scatter binds ONE measure: the renderer reads
|
|
320
|
+
* y through the single `YAxis dataKey={series[0].dataKey}`, so an overlay was
|
|
321
|
+
* painted on the PRIMARY's y and "previous period" landed exactly on top of
|
|
322
|
+
* "current". Drawing it honestly needs the multi-measure projection declined
|
|
323
|
+
* as option A of objectui#7194; `compareTo` on a scatter returns WITH that
|
|
324
|
+
* projection.
|
|
325
|
+
*
|
|
326
|
+
* It reads the RENDERER's chart family (`bar`, `pie`, `scatter`, …), not a
|
|
327
|
+
* dashboard widget type: a caller whose widget types alias a family (the
|
|
328
|
+
* dashboard maps `bubble` to `scatter` and `pyramid` to `funnel`) maps first and
|
|
329
|
+
* asks about the family. An absent or unrecognised family does NOT ignore
|
|
330
|
+
* `compareTo` — the comparison runs.
|
|
331
|
+
*
|
|
332
|
+
* Why it lives here: the dashboard reaches `@object-ui/plugin-charts` only as a
|
|
333
|
+
* devDependency, so this package — a runtime dependency of both — is the one
|
|
334
|
+
* place both paths can read. Two copies were how the lists drifted apart: the
|
|
335
|
+
* dashboard's said "scatter only" while the charts package said all four, so a
|
|
336
|
+
* compare-to pie widget ran a comparison query whose overlay the renderer then
|
|
337
|
+
* dropped (objectui#7495; objectui#4389 is the precedent that a second copy of
|
|
338
|
+
* chart-presentation logic is a defect).
|
|
339
|
+
*
|
|
340
|
+
* @param chartType the chart family, or `undefined` when none is declared
|
|
341
|
+
* @returns `true` for `pie`, `donut`, `funnel` and `scatter`; `false` otherwise
|
|
342
|
+
*/
|
|
343
|
+
export function chartTypeIgnoresCompareTo(chartType) {
|
|
344
|
+
return typeof chartType === 'string' && CHART_TYPES_IGNORING_COMPARE_TO.has(chartType);
|
|
345
|
+
}
|
|
@@ -10,6 +10,37 @@ export interface ChartResultField {
|
|
|
10
10
|
label?: string;
|
|
11
11
|
format?: string;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* The OPTIONAL wire discriminator naming which server-side BUILT-IN default
|
|
15
|
+
* measure a result column is (objectui#7258; producer half objectstack#14492).
|
|
16
|
+
* The analytics service sets it ONLY on the measures it minted itself with a
|
|
17
|
+
* hard-coded English default label (`count` → `'Count'`, …); an author-declared
|
|
18
|
+
* measure never carries it, so its `label` stays the author's verbatim text
|
|
19
|
+
* (objectui#4106). {@link resolveMeasureLabel} is the one reader.
|
|
20
|
+
*
|
|
21
|
+
* Declared BESIDE {@link ChartResultField} rather than on it, deliberately:
|
|
22
|
+
* `dataset-result-field-spec-parity` pins every key of `ChartResultField` as a
|
|
23
|
+
* key of the spec's `AnalyticsResult.fields[]`, and the `@objectstack/spec`
|
|
24
|
+
* this package is built against does not carry `builtinAggregate` yet. The
|
|
25
|
+
* intersection {@link ChartMeasureField} is what the helpers accept, so a
|
|
26
|
+
* spec-typed field flows in unchanged today and carries the key the moment
|
|
27
|
+
* the spec does — structurally, with no cast at the call sites.
|
|
28
|
+
*
|
|
29
|
+
* Typed as `string` rather than the closed {@link BuiltinAggregate} union on
|
|
30
|
+
* purpose: a JSON response is handed here as-is, so the value is validated at
|
|
31
|
+
* the lookup ({@link isBuiltinAggregate}) instead of trusted at the type.
|
|
32
|
+
*
|
|
33
|
+
* TODO(objectstack#14492): once the spec dependency is bumped past the release
|
|
34
|
+
* that adds `AnalyticsResult.fields[].builtinAggregate`, fold this key into
|
|
35
|
+
* `ChartResultField` as `DatasetResultField['builtinAggregate']` (pinning the
|
|
36
|
+
* two closed vocabularies to each other in the parity suite) and retire the
|
|
37
|
+
* carrier.
|
|
38
|
+
*/
|
|
39
|
+
export interface BuiltinAggregateCarrier {
|
|
40
|
+
builtinAggregate?: string;
|
|
41
|
+
}
|
|
42
|
+
/** A result field as the series helpers read it: the renderer shape plus the optional discriminator. */
|
|
43
|
+
export type ChartMeasureField = ChartResultField & BuiltinAggregateCarrier;
|
|
13
44
|
/**
|
|
14
45
|
* One renderer-internal series binding produced by {@link buildChartSeries}:
|
|
15
46
|
* WHICH result-set column to draw and what to call it.
|
|
@@ -31,6 +62,47 @@ export interface ChartSeriesResult {
|
|
|
31
62
|
xAxisKey: string | undefined;
|
|
32
63
|
series: ChartSeriesBinding[];
|
|
33
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* The closed vocabulary of server-side built-in default measures a result field
|
|
67
|
+
* can declare itself as (objectui#7258 / objectstack#14492) — the spec's own
|
|
68
|
+
* measure-aggregate enum spelling (`count_distinct`, not `countDistinct`), so
|
|
69
|
+
* there is exactly ONE spelling on the wire and this list never grows on its
|
|
70
|
+
* own: a value outside it is not "a new aggregate", it is an unrecognised one,
|
|
71
|
+
* and {@link resolveMeasureLabel} treats it as absent.
|
|
72
|
+
*/
|
|
73
|
+
export declare const BUILTIN_AGGREGATES: readonly ["count", "sum", "avg", "min", "max", "count_distinct"];
|
|
74
|
+
/** One member of {@link BUILTIN_AGGREGATES}. */
|
|
75
|
+
export type BuiltinAggregate = (typeof BUILTIN_AGGREGATES)[number];
|
|
76
|
+
/** Runtime membership test for {@link BUILTIN_AGGREGATES}, over an untrusted wire value. */
|
|
77
|
+
export declare function isBuiltinAggregate(value: unknown): value is BuiltinAggregate;
|
|
78
|
+
/**
|
|
79
|
+
* The LOCALIZED display label per built-in aggregate, as the layer that holds
|
|
80
|
+
* the i18n provider resolved them (`@object-ui/i18n`'s
|
|
81
|
+
* `builtinAggregateLabels(tt)`). Partial on purpose: an aggregate the caller
|
|
82
|
+
* did not resolve falls back to the wire `label`, exactly as if the field had
|
|
83
|
+
* carried no discriminator.
|
|
84
|
+
*/
|
|
85
|
+
export type BuiltinAggregateLabels = Partial<Record<BuiltinAggregate, string>>;
|
|
86
|
+
/**
|
|
87
|
+
* The display label of ONE result field, for a legend / axis / tooltip.
|
|
88
|
+
*
|
|
89
|
+
* Resolution order (objectui#7258, maintainer ruling B of 2026-09-02):
|
|
90
|
+
* 1. the field declares itself a built-in default measure
|
|
91
|
+
* (`builtinAggregate` ∈ {@link BUILTIN_AGGREGATES}) AND the caller resolved
|
|
92
|
+
* a locale label for it → that label. This is the only path that can
|
|
93
|
+
* replace a wire `label`, and it is keyed by a STRUCTURAL discriminator —
|
|
94
|
+
* never by matching the label's text or the field's name, both of which
|
|
95
|
+
* the ruling rejected (a real author measure literally named `count`, or
|
|
96
|
+
* labelled `Count`, must keep its own label);
|
|
97
|
+
* 2. otherwise the wire `label` verbatim — the objectui#4106 contract for an
|
|
98
|
+
* author-declared measure (`'Tasks'` reaches the legend untouched);
|
|
99
|
+
* 3. otherwise the field's `name`.
|
|
100
|
+
*
|
|
101
|
+
* So a discriminator the caller has no label for (a provider-less host, or a
|
|
102
|
+
* value outside the closed vocabulary) costs nothing: the field renders exactly
|
|
103
|
+
* what it rendered before the discriminator existed.
|
|
104
|
+
*/
|
|
105
|
+
export declare function resolveMeasureLabel(field: ChartMeasureField, labels?: BuiltinAggregateLabels): string;
|
|
34
106
|
/**
|
|
35
107
|
* The bucket a NULL/undefined category VALUE renders under, when the caller
|
|
36
108
|
* supplies no localized label of its own (objectui#4466).
|
|
@@ -110,6 +182,16 @@ export interface ChartSeriesOptions {
|
|
|
110
182
|
* it, so a mismatch costs the null bucket its drill-through.
|
|
111
183
|
*/
|
|
112
184
|
nullCategoryLabel?: string;
|
|
185
|
+
/**
|
|
186
|
+
* Locale labels for the server's BUILT-IN default measures, keyed by the
|
|
187
|
+
* wire discriminator a result field carries as `builtinAggregate`
|
|
188
|
+
* (objectui#7258). Same division of labour as `nullCategoryLabel`: this
|
|
189
|
+
* package is React-free and i18n-free, so the renderer that holds the
|
|
190
|
+
* provider resolves the strings (`builtinAggregateLabels(tt)` in
|
|
191
|
+
* `@object-ui/i18n`) and passes them in. Absent ⇒ every measure keeps its
|
|
192
|
+
* wire `label`, which for a built-in default is the server's English.
|
|
193
|
+
*/
|
|
194
|
+
builtinAggregateLabels?: BuiltinAggregateLabels;
|
|
113
195
|
}
|
|
114
196
|
/** Per-CLICK knobs for {@link findChartSeriesRow}, on top of the shared ones. */
|
|
115
197
|
export interface ChartDrillLookupOptions extends ChartSeriesOptions {
|
|
@@ -174,7 +256,7 @@ export interface ChartSegmentClickEvent {
|
|
|
174
256
|
/** The measure value at the click point. */
|
|
175
257
|
value?: number;
|
|
176
258
|
}
|
|
177
|
-
export declare function buildChartSeries(rows: Array<Record<string, unknown>> | null | undefined, dimensions: string[] | null | undefined, values: string[] | null | undefined, fields?:
|
|
259
|
+
export declare function buildChartSeries(rows: Array<Record<string, unknown>> | null | undefined, dimensions: string[] | null | undefined, values: string[] | null | undefined, fields?: ChartMeasureField[] | null, options?: ChartSeriesOptions): ChartSeriesResult;
|
|
178
260
|
/**
|
|
179
261
|
* Resolve select/enum dimension VALUES to display LABELS in chart rows.
|
|
180
262
|
*
|
|
@@ -362,10 +444,54 @@ export declare function buildDimensionLabelMap(options: unknown, translate?: Opt
|
|
|
362
444
|
* The object a relationship field points at, or `undefined` when the field is
|
|
363
445
|
* not a relationship (or names no target).
|
|
364
446
|
*
|
|
365
|
-
* The target lives under `reference`
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
447
|
+
* The target lives under `reference` — the ONLY spelling read here, and the same
|
|
448
|
+
* canonicalization as the dataset designer's `resolveReferenceTo`, which is
|
|
449
|
+
* narrowed to match in the same pass (objectui#6528). As of objectui#6648 the
|
|
450
|
+
* CARRIER matches too: a bare string and nothing else. The two helpers are one
|
|
451
|
+
* doctrine in two files and must never drift — a divergence recreates the
|
|
452
|
+
* defect one file over.
|
|
453
|
+
*
|
|
454
|
+
* The three legacy spellings this dropped (`reference_to` / `referenceTo` /
|
|
455
|
+
* `reference_to_object`) were censused against every producer, with `reference`
|
|
456
|
+
* itself as the positive control (445 of 565 lookup / master_detail defs in the
|
|
457
|
+
* framework tree; ACCEPTED by `ObjectSchema.safeParse` on spec 17.2.0, which
|
|
458
|
+
* REFUSES all three others BY NAME). No producer emits any of them onto an
|
|
459
|
+
* object metadata document: `reference_to` was then live only on ObjectUI's own
|
|
460
|
+
* view/field schema (a different contract, translated INTO from `reference`;
|
|
461
|
+
* objectui#11070 round 4 moved that contract to `reference` as well),
|
|
462
|
+
* `referenceTo`'s two producers were retired by objectui#6041, and
|
|
463
|
+
* `reference_to_object` occurs nowhere in either tree outside this chain and the
|
|
464
|
+
* test that called it.
|
|
465
|
+
*
|
|
466
|
+
* ⚠ Unlike `resolveReferenceTo`, this helper reads the `GET /meta/object/:name`
|
|
467
|
+
* document DIRECTLY — no `readFields` door strips retired keys on this path. So
|
|
468
|
+
* the narrowing is load-bearing rather than cosmetic: a stored pre-objectui#6041
|
|
469
|
+
* row spelling the target `referenceTo` now resolves to `undefined` here, the
|
|
470
|
+
* walk yields no entry, and the caller keeps the raw value (best-effort by
|
|
471
|
+
* construction — see {@link resolveDimensionFieldMeta}). That is the intended
|
|
472
|
+
* outcome: per AGENTS.md #0.1 such a row is a PRODUCER-side defect, and a
|
|
473
|
+
* lenient consumer here is exactly where it would have stayed hidden. Note
|
|
474
|
+
* `reference` was already HEAD of the old chain, so any doc carrying both is
|
|
475
|
+
* unaffected.
|
|
476
|
+
*
|
|
477
|
+
* The array and `{ object }` CARRIERS this used to accept were censused on the
|
|
478
|
+
* same standard and removed with the same reasoning (objectui#6648).
|
|
479
|
+
* `FieldSchema.reference` is a plain `z.string()`; `ObjectSchema.safeParse`
|
|
480
|
+
* (spec 17.2.0) ACCEPTS `reference: 'crm_account'` and REFUSES both
|
|
481
|
+
* `['crm_account']` and `{ object: 'crm_account' }` as `invalid_type`. A
|
|
482
|
+
* structure-walking, key-position-aware census of both trees found 587
|
|
483
|
+
* bare-string carriers at the field-def key position and ZERO array or
|
|
484
|
+
* `{ object }` carriers from any producer — the only ones in either tree were
|
|
485
|
+
* the green pins asserting this very tolerance. See `resolveReferenceTo` for
|
|
486
|
+
* the full census, including why a multi-target lookup is NOT what the array
|
|
487
|
+
* branch was serving: it returned `raw[0]` and discarded the rest, and
|
|
488
|
+
* polymorphic lookup remains an undeclared, open spec gap.
|
|
489
|
+
*
|
|
490
|
+
* This path matters MORE than the designer's for the same reason the spelling
|
|
491
|
+
* narrowing did: it reads the metadata document directly, with no `readFields`
|
|
492
|
+
* door in between, so a non-string carrier now degrades visibly (the walk
|
|
493
|
+
* yields no entry and the caller keeps the raw value) instead of being absorbed
|
|
494
|
+
* here.
|
|
369
495
|
*
|
|
370
496
|
* The **type gate is deliberate**: only a declared relationship is walked, so a
|
|
371
497
|
* path segment naming a plain field can never be turned into an object name and
|
|
@@ -44,6 +44,47 @@
|
|
|
44
44
|
* know" refuses — one doctrine, and now BOTH branches state it.
|
|
45
45
|
*/
|
|
46
46
|
import { pivotBucketId, pivotDimensionValue } from './dataset-pivot.js';
|
|
47
|
+
/**
|
|
48
|
+
* The closed vocabulary of server-side built-in default measures a result field
|
|
49
|
+
* can declare itself as (objectui#7258 / objectstack#14492) — the spec's own
|
|
50
|
+
* measure-aggregate enum spelling (`count_distinct`, not `countDistinct`), so
|
|
51
|
+
* there is exactly ONE spelling on the wire and this list never grows on its
|
|
52
|
+
* own: a value outside it is not "a new aggregate", it is an unrecognised one,
|
|
53
|
+
* and {@link resolveMeasureLabel} treats it as absent.
|
|
54
|
+
*/
|
|
55
|
+
export const BUILTIN_AGGREGATES = ['count', 'sum', 'avg', 'min', 'max', 'count_distinct'];
|
|
56
|
+
/** Runtime membership test for {@link BUILTIN_AGGREGATES}, over an untrusted wire value. */
|
|
57
|
+
export function isBuiltinAggregate(value) {
|
|
58
|
+
return typeof value === 'string' && BUILTIN_AGGREGATES.includes(value);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The display label of ONE result field, for a legend / axis / tooltip.
|
|
62
|
+
*
|
|
63
|
+
* Resolution order (objectui#7258, maintainer ruling B of 2026-09-02):
|
|
64
|
+
* 1. the field declares itself a built-in default measure
|
|
65
|
+
* (`builtinAggregate` ∈ {@link BUILTIN_AGGREGATES}) AND the caller resolved
|
|
66
|
+
* a locale label for it → that label. This is the only path that can
|
|
67
|
+
* replace a wire `label`, and it is keyed by a STRUCTURAL discriminator —
|
|
68
|
+
* never by matching the label's text or the field's name, both of which
|
|
69
|
+
* the ruling rejected (a real author measure literally named `count`, or
|
|
70
|
+
* labelled `Count`, must keep its own label);
|
|
71
|
+
* 2. otherwise the wire `label` verbatim — the objectui#4106 contract for an
|
|
72
|
+
* author-declared measure (`'Tasks'` reaches the legend untouched);
|
|
73
|
+
* 3. otherwise the field's `name`.
|
|
74
|
+
*
|
|
75
|
+
* So a discriminator the caller has no label for (a provider-less host, or a
|
|
76
|
+
* value outside the closed vocabulary) costs nothing: the field renders exactly
|
|
77
|
+
* what it rendered before the discriminator existed.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveMeasureLabel(field, labels) {
|
|
80
|
+
const aggregate = field.builtinAggregate;
|
|
81
|
+
if (isBuiltinAggregate(aggregate)) {
|
|
82
|
+
const localized = labels?.[aggregate];
|
|
83
|
+
if (typeof localized === 'string' && localized !== '')
|
|
84
|
+
return localized;
|
|
85
|
+
}
|
|
86
|
+
return field.label ?? field.name;
|
|
87
|
+
}
|
|
47
88
|
/**
|
|
48
89
|
* The bucket a NULL/undefined category VALUE renders under, when the caller
|
|
49
90
|
* supplies no localized label of its own (objectui#4466).
|
|
@@ -296,7 +337,13 @@ export function buildChartSeries(rows, dimensions, values, fields, options) {
|
|
|
296
337
|
const dims = (dimensions ?? []).filter(Boolean);
|
|
297
338
|
const vals = (values ?? []).filter(Boolean);
|
|
298
339
|
const safeRows = Array.isArray(rows) ? rows : [];
|
|
299
|
-
|
|
340
|
+
// A measure's series label: the locale label when the field declares itself
|
|
341
|
+
// a built-in default measure and the caller resolved one, else the wire
|
|
342
|
+
// `label` verbatim (objectui#4106), else the name — see `resolveMeasureLabel`.
|
|
343
|
+
const labelOf = (name) => {
|
|
344
|
+
const field = (fields ?? []).find((f) => f.name === name);
|
|
345
|
+
return field ? resolveMeasureLabel(field, options?.builtinAggregateLabels) : name;
|
|
346
|
+
};
|
|
300
347
|
// Multi-dimension, single-measure → pivot the second dimension into series.
|
|
301
348
|
if (dims.length >= 2 && vals.length === 1) {
|
|
302
349
|
const xKey = dims[0];
|
|
@@ -765,10 +812,54 @@ function fieldDefsOf(schema) {
|
|
|
765
812
|
* The object a relationship field points at, or `undefined` when the field is
|
|
766
813
|
* not a relationship (or names no target).
|
|
767
814
|
*
|
|
768
|
-
* The target lives under `reference`
|
|
769
|
-
*
|
|
770
|
-
*
|
|
771
|
-
*
|
|
815
|
+
* The target lives under `reference` — the ONLY spelling read here, and the same
|
|
816
|
+
* canonicalization as the dataset designer's `resolveReferenceTo`, which is
|
|
817
|
+
* narrowed to match in the same pass (objectui#6528). As of objectui#6648 the
|
|
818
|
+
* CARRIER matches too: a bare string and nothing else. The two helpers are one
|
|
819
|
+
* doctrine in two files and must never drift — a divergence recreates the
|
|
820
|
+
* defect one file over.
|
|
821
|
+
*
|
|
822
|
+
* The three legacy spellings this dropped (`reference_to` / `referenceTo` /
|
|
823
|
+
* `reference_to_object`) were censused against every producer, with `reference`
|
|
824
|
+
* itself as the positive control (445 of 565 lookup / master_detail defs in the
|
|
825
|
+
* framework tree; ACCEPTED by `ObjectSchema.safeParse` on spec 17.2.0, which
|
|
826
|
+
* REFUSES all three others BY NAME). No producer emits any of them onto an
|
|
827
|
+
* object metadata document: `reference_to` was then live only on ObjectUI's own
|
|
828
|
+
* view/field schema (a different contract, translated INTO from `reference`;
|
|
829
|
+
* objectui#11070 round 4 moved that contract to `reference` as well),
|
|
830
|
+
* `referenceTo`'s two producers were retired by objectui#6041, and
|
|
831
|
+
* `reference_to_object` occurs nowhere in either tree outside this chain and the
|
|
832
|
+
* test that called it.
|
|
833
|
+
*
|
|
834
|
+
* ⚠ Unlike `resolveReferenceTo`, this helper reads the `GET /meta/object/:name`
|
|
835
|
+
* document DIRECTLY — no `readFields` door strips retired keys on this path. So
|
|
836
|
+
* the narrowing is load-bearing rather than cosmetic: a stored pre-objectui#6041
|
|
837
|
+
* row spelling the target `referenceTo` now resolves to `undefined` here, the
|
|
838
|
+
* walk yields no entry, and the caller keeps the raw value (best-effort by
|
|
839
|
+
* construction — see {@link resolveDimensionFieldMeta}). That is the intended
|
|
840
|
+
* outcome: per AGENTS.md #0.1 such a row is a PRODUCER-side defect, and a
|
|
841
|
+
* lenient consumer here is exactly where it would have stayed hidden. Note
|
|
842
|
+
* `reference` was already HEAD of the old chain, so any doc carrying both is
|
|
843
|
+
* unaffected.
|
|
844
|
+
*
|
|
845
|
+
* The array and `{ object }` CARRIERS this used to accept were censused on the
|
|
846
|
+
* same standard and removed with the same reasoning (objectui#6648).
|
|
847
|
+
* `FieldSchema.reference` is a plain `z.string()`; `ObjectSchema.safeParse`
|
|
848
|
+
* (spec 17.2.0) ACCEPTS `reference: 'crm_account'` and REFUSES both
|
|
849
|
+
* `['crm_account']` and `{ object: 'crm_account' }` as `invalid_type`. A
|
|
850
|
+
* structure-walking, key-position-aware census of both trees found 587
|
|
851
|
+
* bare-string carriers at the field-def key position and ZERO array or
|
|
852
|
+
* `{ object }` carriers from any producer — the only ones in either tree were
|
|
853
|
+
* the green pins asserting this very tolerance. See `resolveReferenceTo` for
|
|
854
|
+
* the full census, including why a multi-target lookup is NOT what the array
|
|
855
|
+
* branch was serving: it returned `raw[0]` and discarded the rest, and
|
|
856
|
+
* polymorphic lookup remains an undeclared, open spec gap.
|
|
857
|
+
*
|
|
858
|
+
* This path matters MORE than the designer's for the same reason the spelling
|
|
859
|
+
* narrowing did: it reads the metadata document directly, with no `readFields`
|
|
860
|
+
* door in between, so a non-string carrier now degrades visibly (the walk
|
|
861
|
+
* yields no entry and the caller keeps the raw value) instead of being absorbed
|
|
862
|
+
* here.
|
|
772
863
|
*
|
|
773
864
|
* The **type gate is deliberate**: only a declared relationship is walked, so a
|
|
774
865
|
* path segment naming a plain field can never be turned into an object name and
|
|
@@ -781,17 +872,8 @@ export function resolveRelationshipTarget(fieldDef) {
|
|
|
781
872
|
const type = typeof def.type === 'string' ? def.type.toLowerCase() : '';
|
|
782
873
|
if (!RELATIONSHIP_FIELD_TYPES.has(type))
|
|
783
874
|
return undefined;
|
|
784
|
-
const raw = def.reference
|
|
785
|
-
|
|
786
|
-
return raw;
|
|
787
|
-
if (Array.isArray(raw) && typeof raw[0] === 'string' && raw[0])
|
|
788
|
-
return raw[0];
|
|
789
|
-
if (raw && typeof raw === 'object') {
|
|
790
|
-
const obj = raw.object;
|
|
791
|
-
if (typeof obj === 'string' && obj)
|
|
792
|
-
return obj;
|
|
793
|
-
}
|
|
794
|
-
return undefined;
|
|
875
|
+
const raw = def.reference;
|
|
876
|
+
return typeof raw === 'string' && raw ? raw : undefined;
|
|
795
877
|
}
|
|
796
878
|
/**
|
|
797
879
|
* Resolve each dataset dimension's underlying `field` to that field's select
|