@object-ui/core 17.3.0 → 17.5.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 (58) hide show
  1. package/CHANGELOG.md +1224 -0
  2. package/README.md +2 -12
  3. package/dist/actions/ActionEngine.d.ts +42 -25
  4. package/dist/actions/ActionEngine.js +66 -52
  5. package/dist/actions/ActionRunner.d.ts +199 -21
  6. package/dist/actions/ActionRunner.js +332 -16
  7. package/dist/actions/actionKeys.d.ts +96 -20
  8. package/dist/actions/actionKeys.js +157 -18
  9. package/dist/data-scope/ViewDataProvider.d.ts +37 -12
  10. package/dist/data-scope/ViewDataProvider.js +83 -5
  11. package/dist/data-scope/element-data-source.d.ts +113 -0
  12. package/dist/data-scope/element-data-source.js +216 -0
  13. package/dist/data-scope/index.d.ts +2 -1
  14. package/dist/data-scope/index.js +1 -0
  15. package/dist/evaluator/declaredPredicate.d.ts +119 -0
  16. package/dist/evaluator/declaredPredicate.js +152 -0
  17. package/dist/evaluator/fieldRules.d.ts +16 -0
  18. package/dist/evaluator/fieldRules.js +51 -16
  19. package/dist/evaluator/index.d.ts +1 -0
  20. package/dist/evaluator/index.js +1 -0
  21. package/dist/evaluator/listConditional.d.ts +5 -2
  22. package/dist/evaluator/listConditional.js +58 -16
  23. package/dist/evaluator/predicateInput.d.ts +51 -3
  24. package/dist/evaluator/predicateInput.js +62 -5
  25. package/dist/index.d.ts +4 -1
  26. package/dist/index.js +14 -1
  27. package/dist/protocols/DndProtocol.d.ts +42 -1
  28. package/dist/protocols/KeyboardProtocol.d.ts +31 -1
  29. package/dist/protocols/SharingProtocol.d.ts +11 -1
  30. package/dist/registry/Registry.d.ts +24 -0
  31. package/dist/theme/ThemeEngine.d.ts +0 -8
  32. package/dist/theme/ThemeEngine.js +22 -103
  33. package/dist/theme/index.d.ts +1 -1
  34. package/dist/theme/index.js +7 -1
  35. package/dist/types/index.d.ts +42 -8
  36. package/dist/utils/chart-series.d.ts +258 -5
  37. package/dist/utils/chart-series.js +423 -20
  38. package/dist/utils/compare-to.d.ts +40 -13
  39. package/dist/utils/compare-to.js +4 -32
  40. package/dist/utils/dashboard-filters.d.ts +54 -8
  41. package/dist/utils/dashboard-filters.js +261 -12
  42. package/dist/utils/dataset-format.d.ts +73 -14
  43. package/dist/utils/dataset-format.js +130 -9
  44. package/dist/utils/dataset-pivot.d.ts +28 -7
  45. package/dist/utils/dataset-pivot.js +27 -6
  46. package/dist/utils/dom-props.d.ts +137 -0
  47. package/dist/utils/dom-props.js +153 -0
  48. package/dist/utils/normalize-list-view.d.ts +26 -6
  49. package/dist/utils/normalize-list-view.js +30 -8
  50. package/dist/utils/number-display.d.ts +151 -0
  51. package/dist/utils/number-display.js +71 -0
  52. package/dist/utils/resolve-view-id.d.ts +31 -0
  53. package/dist/utils/resolve-view-id.js +45 -0
  54. package/dist/utils/sort-query.d.ts +55 -0
  55. package/dist/utils/sort-query.js +35 -0
  56. package/package.json +5 -5
  57. package/dist/utils/merge-views-into-objects.d.ts +0 -19
  58. package/dist/utils/merge-views-into-objects.js +0 -39
@@ -116,41 +116,33 @@ export function generateColorVars(colors) {
116
116
  /**
117
117
  * Generate CSS custom properties from a Theme's typography config.
118
118
  */
119
+ // ----------------------------------------------------------------------------
120
+ // RETIRED THEME BLOCKS — @objectstack/spec 17.0.0-rc.3 (objectstack#5021 option
121
+ // 2, PR objectstack#5289), objectui#3361.
122
+ //
123
+ // `theme.animation`, `theme.zIndex` and five typography groups
124
+ // (`fontSize` / `fontWeight` / `lineHeight` / `letterSpacing`, plus
125
+ // `fontFamily.heading` / `fontFamily.mono`) became TOMBSTONES: the schema now
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
128
+ // `--z-modal` or a `--duration-fast` is authored there now, emitted verbatim as
129
+ // `--<key>: <value>`.
130
+ //
131
+ // The emission code below them was therefore structurally dead: no author can
132
+ // produce the input that would reach it. It is removed rather than left behind
133
+ // a cast, because the tombstoned keys type as `never` and any cast that made
134
+ // them compile would fossilize a shape the contract has withdrawn (AGENTS.md
135
+ // #0.1). LIVE emission — `colors`, `borderRadius`, `shadows`,
136
+ // `typography.fontFamily.base` (→ `--font-sans`) and `customVars` — is
137
+ // untouched, byte for byte.
138
+ // ----------------------------------------------------------------------------
119
139
  export function generateTypographyVars(typography) {
120
140
  const vars = {};
141
+ // `fontFamily.base` is the only surviving typography input — see the RETIRED
142
+ // THEME BLOCKS note above for where the other five groups went.
121
143
  if (typography.fontFamily?.base) {
122
144
  vars['--font-sans'] = typography.fontFamily.base;
123
145
  }
124
- if (typography.fontFamily?.heading) {
125
- vars['--font-heading'] = typography.fontFamily.heading;
126
- }
127
- if (typography.fontFamily?.mono) {
128
- vars['--font-mono'] = typography.fontFamily.mono;
129
- }
130
- if (typography.fontSize) {
131
- for (const [key, value] of Object.entries(typography.fontSize)) {
132
- if (value)
133
- vars[`--font-size-${key}`] = value;
134
- }
135
- }
136
- if (typography.fontWeight) {
137
- for (const [key, value] of Object.entries(typography.fontWeight)) {
138
- if (value != null)
139
- vars[`--font-weight-${key}`] = String(value);
140
- }
141
- }
142
- if (typography.lineHeight) {
143
- for (const [key, value] of Object.entries(typography.lineHeight)) {
144
- if (value)
145
- vars[`--line-height-${key}`] = value;
146
- }
147
- }
148
- if (typography.letterSpacing) {
149
- for (const [key, value] of Object.entries(typography.letterSpacing)) {
150
- if (value)
151
- vars[`--letter-spacing-${key}`] = value;
152
- }
153
- }
154
146
  return vars;
155
147
  }
156
148
  /**
@@ -197,36 +189,6 @@ export function generateShadowVars(shadows) {
197
189
  }
198
190
  return vars;
199
191
  }
200
- /**
201
- * Generate CSS custom properties from a Theme's animation config.
202
- */
203
- export function generateAnimationVars(animation) {
204
- const vars = {};
205
- if (animation.duration) {
206
- for (const [key, value] of Object.entries(animation.duration)) {
207
- if (value)
208
- vars[`--duration-${key}`] = value;
209
- }
210
- }
211
- if (animation.timing) {
212
- for (const [key, value] of Object.entries(animation.timing)) {
213
- if (value)
214
- vars[`--timing-${key}`] = value;
215
- }
216
- }
217
- return vars;
218
- }
219
- /**
220
- * Generate CSS custom properties from a Theme's z-index config.
221
- */
222
- export function generateZIndexVars(zIndex) {
223
- const vars = {};
224
- for (const [key, value] of Object.entries(zIndex)) {
225
- if (value != null)
226
- vars[`--z-${key}`] = String(value);
227
- }
228
- return vars;
229
- }
230
192
  /**
231
193
  * Generate ALL CSS custom properties from a complete Theme.
232
194
  * This is the main entry point for theme → CSS conversion.
@@ -247,14 +209,6 @@ export function generateThemeVars(theme) {
247
209
  if (theme.shadows) {
248
210
  Object.assign(vars, generateShadowVars(theme.shadows));
249
211
  }
250
- // Animation
251
- if (theme.animation) {
252
- Object.assign(vars, generateAnimationVars(theme.animation));
253
- }
254
- // Z-Index
255
- if (theme.zIndex) {
256
- Object.assign(vars, generateZIndexVars(theme.zIndex));
257
- }
258
212
  // Custom CSS variables (passthrough)
259
213
  if (theme.customVars) {
260
214
  for (const [key, value] of Object.entries(theme.customVars)) {
@@ -290,22 +244,6 @@ export function mergeThemes(parent, child) {
290
244
  ...parent.typography?.fontFamily,
291
245
  ...child.typography?.fontFamily,
292
246
  },
293
- fontSize: {
294
- ...parent.typography?.fontSize,
295
- ...child.typography?.fontSize,
296
- },
297
- fontWeight: {
298
- ...parent.typography?.fontWeight,
299
- ...child.typography?.fontWeight,
300
- },
301
- lineHeight: {
302
- ...parent.typography?.lineHeight,
303
- ...child.typography?.lineHeight,
304
- },
305
- letterSpacing: {
306
- ...parent.typography?.letterSpacing,
307
- ...child.typography?.letterSpacing,
308
- },
309
247
  }
310
248
  : undefined,
311
249
  // Deep-merge border radius
@@ -316,25 +254,6 @@ export function mergeThemes(parent, child) {
316
254
  shadows: child.shadows || parent.shadows
317
255
  ? { ...parent.shadows, ...child.shadows }
318
256
  : undefined,
319
- // Deep-merge animation
320
- animation: child.animation || parent.animation
321
- ? {
322
- ...parent.animation,
323
- ...child.animation,
324
- duration: {
325
- ...parent.animation?.duration,
326
- ...child.animation?.duration,
327
- },
328
- timing: {
329
- ...parent.animation?.timing,
330
- ...child.animation?.timing,
331
- },
332
- }
333
- : undefined,
334
- // Deep-merge zIndex
335
- zIndex: child.zIndex || parent.zIndex
336
- ? { ...parent.zIndex, ...child.zIndex }
337
- : undefined,
338
257
  // Deep-merge customVars
339
258
  customVars: child.customVars || parent.customVars
340
259
  ? { ...parent.customVars, ...child.customVars }
@@ -5,4 +5,4 @@
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
- export { hexToHSL, toCSSColor, generateColorVars, generateTypographyVars, generateBorderRadiusVars, generateShadowVars, generateAnimationVars, generateZIndexVars, generateThemeVars, mergeThemes, resolveThemeInheritance, resolveMode, contrastRatio, meetsContrastLevel, } from './ThemeEngine';
8
+ export { hexToHSL, toCSSColor, generateColorVars, generateTypographyVars, generateBorderRadiusVars, generateShadowVars, generateThemeVars, mergeThemes, resolveThemeInheritance, resolveMode, contrastRatio, meetsContrastLevel, } from './ThemeEngine';
@@ -5,4 +5,10 @@
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
- export { hexToHSL, toCSSColor, generateColorVars, generateTypographyVars, generateBorderRadiusVars, generateShadowVars, generateAnimationVars, generateZIndexVars, generateThemeVars, mergeThemes, resolveThemeInheritance, resolveMode, contrastRatio, meetsContrastLevel, } from './ThemeEngine';
8
+ export { hexToHSL, toCSSColor, generateColorVars, generateTypographyVars, generateBorderRadiusVars, generateShadowVars,
9
+ // `generateAnimationVars` / `generateZIndexVars` removed with the
10
+ // `theme.animation` / `theme.zIndex` tombstones in @objectstack/spec
11
+ // 17.0.0-rc.3 (objectstack#5021) — see ThemeEngine's RETIRED THEME BLOCKS
12
+ // note. `theme.customVars` is the declared door for a `--duration-*` or
13
+ // `--z-*` now.
14
+ generateThemeVars, mergeThemes, resolveThemeInheritance, resolveMode, contrastRatio, meetsContrastLevel, } from './ThemeEngine';
@@ -5,14 +5,48 @@
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
- export interface SchemaNode {
9
- type: string;
10
- id?: string;
11
- className?: string;
12
- data?: any;
13
- body?: SchemaNode | SchemaNode[];
14
- [key: string]: any;
15
- }
8
+ /**
9
+ * One `SchemaNode` (objectui#4580).
10
+ *
11
+ * This package used to hand-declare `interface SchemaNode { type: string; …
12
+ * [key: string]: any }` here, while `@object-ui/types` exported
13
+ * `type SchemaNode = BaseSchema | string | number | boolean | null | undefined`
14
+ * — two different types under one name, from two packages the same consumers
15
+ * import together. Which declaration a call site got depended on which package
16
+ * it happened to import from, and #4548's option-A canary measured **19 of 35**
17
+ * errors as exactly that collision:
18
+ *
19
+ * ```
20
+ * Type 'import(".../packages/types/dist/base").SchemaNode' is not assignable to
21
+ * type 'import(".../packages/core/dist/types/index").SchemaNode'
22
+ * ```
23
+ *
24
+ * PR #4578 sidestepped it — `SchemaRenderer` states its own component-level
25
+ * union rather than picking a side — and left the reconciliation to this card.
26
+ *
27
+ * **`@object-ui/types`' union wins**, per #4580's ruling 1: it is the spec of
28
+ * record (its own doc comment names `'Plain string'` a valid node), and it is
29
+ * the side the measured collisions resolve toward. The declaration is RE-EXPORTED
30
+ * rather than restated, so there is exactly one declaration left to disagree
31
+ * with — a structural copy would reproduce the defect the moment either side
32
+ * moved. This package already depends on `@object-ui/types`, so the edge exists
33
+ * and adds no cycle, and core's own entry surface is unchanged (`dist/index.d.ts`
34
+ * is byte-identical across the change — measured, both rounds).
35
+ *
36
+ * The collision is only observable from a package that resolves BOTH through
37
+ * `node_modules`; the pin therefore lives in `@object-ui/react`
38
+ * (`src/__tests__/SchemaNode.reconciliation.test.ts`), not here.
39
+ */
40
+ export type { SchemaNode } from '@object-ui/types';
41
+ import type { SchemaNode } from '@object-ui/types';
42
+ /**
43
+ * ⛔ Deliberately NOT reconciled with `@object-ui/types`' `ComponentRendererProps`
44
+ * (objectui#4594). The two declarations differ — types' is generic
45
+ * (`< TSchema extends BaseSchema = BaseSchema >`), this one is not — but that
46
+ * card measured **zero consumers** of this declaration, so reconciling it here
47
+ * would be an unmeasured change riding a card that was scoped to `SchemaNode`.
48
+ * It stays dual-declared until #4594 is dispatched on its own evidence.
49
+ */
16
50
  export interface ComponentRendererProps {
17
51
  schema: SchemaNode;
18
52
  [key: string]: any;
@@ -20,6 +20,11 @@
20
20
  * visible instead of just repeating the x-axis label.
21
21
  * - **otherwise** (single dimension, or multiple measures) → first dimension is
22
22
  * the x-axis and each measure is its own series (long format passes through).
23
+ *
24
+ * BOTH branches map a NULL first-dimension VALUE to an explicit bucket label
25
+ * ({@link NULL_CATEGORY_LABEL}) so the group renders instead of vanishing —
26
+ * objectui#4466 for the single-dimension branch, objectui#4497 for the pivot.
27
+ * One doctrine, one predicate (`isNullCategory`), two call sites.
23
28
  */
24
29
  export interface ChartResultField {
25
30
  name: string;
@@ -47,7 +52,36 @@ export interface ChartSeriesResult {
47
52
  xAxisKey: string | undefined;
48
53
  series: ChartSeriesBinding[];
49
54
  }
50
- export declare function buildChartSeries(rows: Array<Record<string, unknown>> | null | undefined, dimensions: string[] | null | undefined, values: string[] | null | undefined, fields?: ChartResultField[] | null): ChartSeriesResult;
55
+ /**
56
+ * The bucket a NULL/undefined category VALUE renders under, when the caller
57
+ * supplies no localized label of its own (objectui#4466).
58
+ *
59
+ * English by construction and deliberately so: this package is React-free and
60
+ * i18n-free (see {@link OptionLabelTranslator} for the same reason stated one
61
+ * layer down), so the locale bundle cannot be read here. Every renderer that
62
+ * has an i18n provider passes its own resolved label through
63
+ * {@link ChartSeriesOptions.nullCategoryLabel} — this constant is the floor
64
+ * that keeps a provider-less host (or a caller that has not been wired yet)
65
+ * drawing the group rather than dropping it.
66
+ *
67
+ * Exported so a consumer can recognise the bucket it will be handed —
68
+ * {@link findChartSeriesRow} uses the same default, which is what makes a
69
+ * clicked bucket bar resolve back to its (raw null) dataset row.
70
+ */
71
+ export declare const NULL_CATEGORY_LABEL = "(None)";
72
+ /** Per-call knobs shared by {@link buildChartSeries} / {@link findChartSeriesRow}. */
73
+ export interface ChartSeriesOptions {
74
+ /**
75
+ * Display label for a category whose value is `null`/`undefined`. Defaults to
76
+ * {@link NULL_CATEGORY_LABEL}.
77
+ *
78
+ * Pass the SAME value to both helpers: `buildChartSeries` writes it into the
79
+ * rendered rows and `findChartSeriesRow` matches a clicked category against
80
+ * it, so a mismatch costs the null bucket its drill-through.
81
+ */
82
+ nullCategoryLabel?: string;
83
+ }
84
+ export declare function buildChartSeries(rows: Array<Record<string, unknown>> | null | undefined, dimensions: string[] | null | undefined, values: string[] | null | undefined, fields?: ChartResultField[] | null, options?: ChartSeriesOptions): ChartSeriesResult;
51
85
  /**
52
86
  * Resolve select/enum dimension VALUES to display LABELS in chart rows.
53
87
  *
@@ -85,8 +119,28 @@ export declare function relabelDimensions(rows: Array<Record<string, unknown>> |
85
119
  *
86
120
  * Comparison is string-wise on the rows' display values (which is what the chart
87
121
  * surfaces as `category` / series key). Returns `-1` when nothing matches.
122
+ *
123
+ * The NULL-category bucket {@link buildChartSeries} renders (objectui#4466) is
124
+ * matched back to its row here, and it has to be: the caller passes the rows it
125
+ * charted FROM (still carrying the raw null) while the click event carries the
126
+ * bucket LABEL, so without this the one bar the fix made visible would resolve
127
+ * to `-1` and its drill-through would silently no-op. Pass the same
128
+ * `nullCategoryLabel` both helpers were given. The pre-existing `''` spelling
129
+ * of "no group value" still matches too — `computeDrillFilter` writes that one.
130
+ *
131
+ * **`xOf` covers BOTH arms, which is what let objectui#4497 bucket the pivot
132
+ * branch without touching this function.** Worth stating because the reverse
133
+ * would be silent: the pivot's emitted rows are AGGREGATED (`byX` collapses N
134
+ * raw rows into one bucket per first-dimension value), so they are NOT
135
+ * index-aligned with anything, and a caller cannot drill by the emitted row's
136
+ * position. It drills by SEARCHING the raw rows here and indexing
137
+ * `drillRawRows` with what this returns — see `DatasetWidget.handleChartDrill`,
138
+ * the one production caller. So the pivot's map key and its emitted display
139
+ * value being two different things never reached the drill: the raw rows this
140
+ * searches still carry their null either way, and `xOf` reads the label back to
141
+ * them in the multi-dimension arm exactly as in the single-dimension one.
88
142
  */
89
- export declare function findChartSeriesRow(rows: Array<Record<string, unknown>> | null | undefined, dimensions: string[] | null | undefined, values: string[] | null | undefined, category: string | undefined, seriesKey?: string): number;
143
+ export declare function findChartSeriesRow(rows: Array<Record<string, unknown>> | null | undefined, dimensions: string[] | null | undefined, values: string[] | null | undefined, category: string | undefined, seriesKey?: string, options?: ChartSeriesOptions): number;
90
144
  /**
91
145
  * Build a per-category colour map from a select/lookup field's `options`.
92
146
  *
@@ -129,14 +183,213 @@ export declare function buildCategoryOrder(options: unknown): string[] | null;
129
183
  * after the declared ones rather than dropping them).
130
184
  */
131
185
  export declare function buildCategoryRank(order: string[] | null | undefined): Map<string, number> | null;
186
+ /**
187
+ * The i18n seam of the analytics label net (objectui#4030).
188
+ *
189
+ * `(storedValue, authoredLabel) => displayLabel` — the SAME signature
190
+ * `resolveGroupByLabels` (plugin-charts) already takes for the legacy aggregate
191
+ * path, which callers bind to `useSafeFieldLabel().fieldOptionLabel(object,
192
+ * field, …)`. That resolver reads `{ns}.fieldOptions.<object>.<field>.<value>`,
193
+ * the one convention list and form surfaces already translate select options
194
+ * through (`useObjectLabel.translateOptions`, and `@objectstack/spec` names
195
+ * objectui as its reader). Analytics reuses that channel rather than growing a
196
+ * chart-side per-locale dialect.
197
+ *
198
+ * Omitted everywhere it isn't available (no i18n provider, an unresolvable
199
+ * owning object): every helper below then behaves exactly as it did before this
200
+ * seam existed.
201
+ */
202
+ export type OptionLabelTranslator = (value: string, authoredLabel: string) => string;
203
+ /**
204
+ * Run a select field's `options` through the locale bundle, returning options
205
+ * whose `label` is the translated one (objectui#4030).
206
+ *
207
+ * The mirror of `useObjectLabel().translateOptions` — the channel list and form
208
+ * surfaces localize select options through — kept pure and shape-tolerant so
209
+ * the analytics net can apply it at the point the resolved options are read.
210
+ * Everything downstream ({@link buildOptionColorMap},
211
+ * {@link buildCategoryOrder}) keeps reading `option.label` and needs no
212
+ * knowledge of i18n, exactly like `SelectCellRenderer` on the list side.
213
+ *
214
+ * Colour, order and every other option key survive untouched — only `label`
215
+ * changes. Returns the input ARRAY ITSELF when there is no translator or
216
+ * nothing translated, so an untranslated app keeps the identities (and the
217
+ * memo/render behaviour) it had before.
218
+ */
219
+ export declare function localizeFieldOptions(options: unknown, translate?: OptionLabelTranslator): unknown;
132
220
  /**
133
221
  * Build a `{ value → label }` map from a select/enum field's `options`, for
134
222
  * resolving a grouped dimension's stored value to its display label (fed to
135
223
  * {@link relabelDimensions}). Mirrors {@link buildOptionColorMap}.
136
224
  *
137
225
  * Options may be `{ value, label }` objects or bare strings (value == label —
138
- * nothing to relabel). Only entries whose `label` actually differs from the
139
- * `value` are kept, so the map is empty (→ `null`) when relabeling would be a
226
+ * nothing to relabel). Only entries whose display label actually differs from
227
+ * the key are kept, so the map is empty (→ `null`) when relabeling would be a
140
228
  * no-op and the caller can skip it entirely.
229
+ *
230
+ * **With a `translate` seam (objectui#4030) the map gains a SECOND key per
231
+ * option: the AUTHORED label.** A dimension's rows reach this net keyed either
232
+ * way — value-keyed when the server did not resolve the dimension (the whole
233
+ * reason this net exists), already label-keyed when it did (ADR-0021) — and
234
+ * the reported symptom is the second case: a chart legend showing the object's
235
+ * English `label` verbatim beside a related list showing the translation. One
236
+ * key resolves `orion`, the other re-translates `Orion Engineered Carbons`;
237
+ * `relabelDimensions` is value-wise and idempotent, so whichever the row
238
+ * carries lands on the same translated display.
239
+ *
240
+ * Value keys win over authored-label keys: a stored value that happens to
241
+ * equal some other option's label is still that option's value.
242
+ *
243
+ * Without a translator this is byte-for-byte the pre-#4030 map — the authored
244
+ * label then IS the display, so no second key is ever emitted.
245
+ */
246
+ export declare function buildDimensionLabelMap(options: unknown, translate?: OptionLabelTranslator): Record<string, string> | null;
247
+ /**
248
+ * The object a relationship field points at, or `undefined` when the field is
249
+ * not a relationship (or names no target).
250
+ *
251
+ * The target lives under `reference` on framework-served field defs; older /
252
+ * spec shapes spell it `reference_to` / `referenceTo` / `reference_to_object`,
253
+ * and any of them may carry a bare name, a one-element array, or `{ object }`.
254
+ * Same canonicalization as the dataset designer's `resolveReferenceTo`.
255
+ *
256
+ * The **type gate is deliberate**: only a declared relationship is walked, so a
257
+ * path segment naming a plain field can never be turned into an object name and
258
+ * fetched speculatively.
259
+ */
260
+ export declare function resolveRelationshipTarget(fieldDef: unknown): string | undefined;
261
+ /**
262
+ * Resolve each dataset dimension's underlying `field` to that field's select
263
+ * `options`, following DOTTED relationship paths to the object that actually
264
+ * owns the terminal field (objectui#4053).
265
+ *
266
+ * A dimension's `field` is either a local field name (`industry`) or a
267
+ * relationship path (`crm_account.industry`, and — per ADR-0071, which the
268
+ * dataset designer emits — `crm_account.owner.department`). Reading the options
269
+ * as `baseSchema.fields[path]` only ever works for the local spelling: for a
270
+ * dotted path the options live on the RELATED object, the lookup missed
271
+ * silently, and the chart fell through to the raw stored enum while the same
272
+ * field as a local dimension rendered its labels beside it.
273
+ *
274
+ * This is the object-resolution step of that ONE lookup, not a dotted-path
275
+ * variant beside it: a single-segment path never enters the walk and resolves
276
+ * exactly as before, so the local path cannot drift away from the joined one.
277
+ *
278
+ * `loadObjectSchema` supplies each hop's object metadata doc — the caller's
279
+ * existing channel (the same `GET /meta/object/:name` read that produced
280
+ * `baseSchema`), so no new fetch layer is introduced. It is memoized per call,
281
+ * because sibling dimensions routinely share a prefix (`crm_account.industry`
282
+ * alongside `crm_account.type` fetches `crm_account` once), and seeded with
283
+ * `baseSchema` under its own `name` so the base is never re-fetched.
284
+ *
285
+ * Best-effort by construction: a hop that is not a relationship, a target that
286
+ * cannot be loaded, or a terminal field that carries no `options` simply yields
287
+ * no entry, and the caller keeps the raw value. Returns `{ fieldPath → options }`
288
+ * for the paths that did resolve.
289
+ *
290
+ * Thin wrapper over {@link resolveDimensionFieldMeta}, which is the same ONE
291
+ * walk keeping the identity of what it found. Callers that translate option
292
+ * labels need that identity (the i18n key is
293
+ * `fieldOptions.<owningObject>.<terminalField>.<value>`, and for a dotted path
294
+ * the owner is the RELATIONSHIP TARGET, not the dataset's base object) — see
295
+ * objectui#4030.
296
+ */
297
+ export declare function resolveDimensionFieldOptions(baseSchema: unknown, fieldPaths: Array<string | undefined | null>, loadObjectSchema: (objectName: string) => Promise<unknown>): Promise<Record<string, unknown>>;
298
+ /**
299
+ * What {@link resolveDimensionFieldOptions} found for ONE dimension field path
300
+ * — the options AND the identity of the field they belong to.
301
+ */
302
+ export interface DimensionFieldMeta {
303
+ /**
304
+ * The object that OWNS the terminal field: the dataset's base object for a
305
+ * local path, the last relationship's TARGET for a dotted one. `undefined`
306
+ * only when the base schema carries no `name` and the path is local.
307
+ */
308
+ object: string | undefined;
309
+ /** The terminal field's own name — the LAST path segment, never the path. */
310
+ field: string;
311
+ /** The terminal field's `options`, exactly as the metadata doc carries them. */
312
+ options: unknown;
313
+ }
314
+ /**
315
+ * {@link resolveDimensionFieldOptions} keeping what it walked THROUGH.
316
+ *
317
+ * Same single walk, same best-effort tolerance, same memoized loader — it just
318
+ * returns `{ object, field, options }` per resolved path instead of the options
319
+ * alone. Split out for objectui#4030: applying the locale bundle to a resolved
320
+ * option label needs the key the bundle is written under
321
+ * (`fieldOptions.<object>.<field>.<value>`), and for `crm_account.industry`
322
+ * that object is `crm_account` — the walk already knows it and used to drop it
323
+ * on the floor. Deriving it at the call site would mean re-walking the
324
+ * relationship chain a second time, i.e. two derivations of one fact.
325
+ */
326
+ export declare function resolveDimensionFieldMeta(baseSchema: unknown, fieldPaths: Array<string | undefined | null>, loadObjectSchema: (objectName: string) => Promise<unknown>): Promise<Record<string, DimensionFieldMeta>>;
327
+ /**
328
+ * The locale-bundle resolver a caller supplies to translate one option label —
329
+ * `useSafeFieldLabel().fieldOptionLabel` in every current caller, i.e. the
330
+ * `{ns}.fieldOptions.<object>.<field>.<value>` convention list and form
331
+ * surfaces already translate select options through (objectui#4030).
332
+ *
333
+ * Taken as a plain function so this package needs no knowledge of i18n, React,
334
+ * or which provider is mounted — the same reason {@link OptionLabelTranslator}
335
+ * is a bare function type.
336
+ */
337
+ export type FieldOptionLabelResolver = (object: string, field: string, value: string, authoredLabel: string) => string;
338
+ /**
339
+ * Bind a {@link FieldOptionLabelResolver} to ONE resolved dimension field,
340
+ * yielding the {@link OptionLabelTranslator} the option helpers take.
341
+ *
342
+ * The binding is the part that is easy to get wrong, which is why it is stated
343
+ * once here rather than at each call site: the translator must be bound to the
344
+ * object that OWNS the terminal field — `crm_account` for `crm_account.industry`,
345
+ * NOT the dataset's base object — because that owner is the key the locale
346
+ * bundle is written under. {@link resolveDimensionFieldMeta} already walked the
347
+ * relationship chain and kept that owner; this just reads it.
348
+ *
349
+ * Returns `undefined` — i.e. "no translation, keep the authored labels" — when
350
+ * the owner could not be resolved or no resolver was supplied. Every helper
351
+ * downstream then behaves exactly as it did before the #4030 seam existed.
352
+ */
353
+ export declare function dimensionOptionTranslator(meta: DimensionFieldMeta | undefined, fieldOptionLabel?: FieldOptionLabelResolver): OptionLabelTranslator | undefined;
354
+ /** One dimension paired with the field path its values resolve through. */
355
+ export interface DimensionRelabelTarget {
356
+ /** The dimension name as the surface renders it (the row key). */
357
+ dim: string;
358
+ /** That dimension's underlying field path, as the dataset query reported it. */
359
+ path: string;
360
+ }
361
+ /**
362
+ * Derive the `{ dimension → { value|authoredLabel → displayLabel } }` maps that
363
+ * {@link relabelDimensions} consumes, from metadata {@link loadDimensionFieldMeta}
364
+ * (or {@link resolveDimensionFieldMeta}) already resolved.
365
+ *
366
+ * This is the LOCALE-APPLYING step, and keeping it separate from the load is
367
+ * the whole point of the split (objectui#4030 / PR #4324): the fetched metadata
368
+ * stays locale-free, so switching language re-derives these maps in a render
369
+ * memo instead of re-issuing the metadata read. A caller that folds the two
370
+ * together reintroduces a refetch on every language switch — the defect #4324
371
+ * fixed, and the property objectui#4389 lifted here so it is stated once.
372
+ *
373
+ * Best-effort per dimension, exactly as the longhand copies were: a path that
374
+ * resolved no metadata, or whose field carries no `options`, contributes no
375
+ * entry, and `relabelDimensions` then returns the caller's rows by identity.
376
+ * Returns `null` — never an empty object — when nothing resolved, so callers
377
+ * can skip the relabel entirely.
378
+ */
379
+ export declare function deriveDimensionLabelMaps(metaByPath: Record<string, DimensionFieldMeta> | null | undefined, relabel: readonly DimensionRelabelTarget[] | null | undefined, fieldOptionLabel?: FieldOptionLabelResolver): Record<string, Record<string, string>> | null;
380
+ /**
381
+ * Load one dataset query's dimension field metadata: fetch the base object's
382
+ * schema, then resolve every dimension's field path against it.
383
+ *
384
+ * The two-step composition every caller wrote out by hand. `loadObjectSchema`
385
+ * stays the CALLER's channel — this package issues no reads of its own and has
386
+ * no opinion about transport, which is what keeps it free of React, of
387
+ * `SchemaRendererContext`, and of the authenticated-`apiFetch` concern
388
+ * (objectui#4121) that belongs to the layer holding the host context.
389
+ *
390
+ * The same loader serves both steps by design: {@link resolveDimensionFieldMeta}
391
+ * memoizes it per call and seeds the cache with the base schema under its own
392
+ * `name`, so the base object is read ONCE even when several dotted dimensions
393
+ * walk back through it. That read count is pinned on both consuming surfaces.
141
394
  */
142
- export declare function buildDimensionLabelMap(options: unknown): Record<string, string> | null;
395
+ export declare function loadDimensionFieldMeta(loadObjectSchema: (objectName: string) => Promise<unknown>, object: string, fieldPaths: Array<string | undefined | null>): Promise<Record<string, DimensionFieldMeta>>;