@object-ui/core 17.2.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 (79) hide show
  1. package/CHANGELOG.md +1436 -0
  2. package/README.md +31 -12
  3. package/dist/actions/ActionEngine.d.ts +53 -31
  4. package/dist/actions/ActionEngine.js +87 -66
  5. package/dist/actions/ActionRunner.d.ts +215 -24
  6. package/dist/actions/ActionRunner.js +334 -17
  7. package/dist/actions/actionErrorDetail.d.ts +26 -0
  8. package/dist/actions/actionErrorDetail.js +37 -0
  9. package/dist/actions/actionKeys.d.ts +96 -20
  10. package/dist/actions/actionKeys.js +168 -18
  11. package/dist/actions/actionResponse.d.ts +42 -0
  12. package/dist/actions/actionResponse.js +104 -0
  13. package/dist/actions/index.d.ts +3 -0
  14. package/dist/actions/index.js +3 -0
  15. package/dist/actions/serverActionHandler.d.ts +143 -0
  16. package/dist/actions/serverActionHandler.js +137 -0
  17. package/dist/data-scope/ViewDataProvider.d.ts +37 -12
  18. package/dist/data-scope/ViewDataProvider.js +83 -5
  19. package/dist/data-scope/element-data-source.d.ts +113 -0
  20. package/dist/data-scope/element-data-source.js +216 -0
  21. package/dist/data-scope/index.d.ts +2 -1
  22. package/dist/data-scope/index.js +1 -0
  23. package/dist/evaluator/ExpressionEvaluator.js +4 -2
  24. package/dist/evaluator/declaredPredicate.d.ts +119 -0
  25. package/dist/evaluator/declaredPredicate.js +152 -0
  26. package/dist/evaluator/fieldRules.d.ts +42 -2
  27. package/dist/evaluator/fieldRules.js +101 -11
  28. package/dist/evaluator/index.d.ts +2 -0
  29. package/dist/evaluator/index.js +2 -0
  30. package/dist/evaluator/listConditional.d.ts +18 -3
  31. package/dist/evaluator/listConditional.js +0 -0
  32. package/dist/evaluator/optionRules.js +5 -1
  33. package/dist/evaluator/predicateInput.d.ts +132 -0
  34. package/dist/evaluator/predicateInput.js +129 -0
  35. package/dist/index.d.ts +7 -1
  36. package/dist/index.js +26 -1
  37. package/dist/protocols/DndProtocol.d.ts +42 -1
  38. package/dist/protocols/KeyboardProtocol.d.ts +31 -1
  39. package/dist/protocols/SharingProtocol.d.ts +11 -1
  40. package/dist/protocols/index.d.ts +0 -1
  41. package/dist/protocols/index.js +8 -1
  42. package/dist/registry/Registry.d.ts +24 -0
  43. package/dist/theme/ThemeEngine.d.ts +0 -8
  44. package/dist/theme/ThemeEngine.js +22 -103
  45. package/dist/theme/index.d.ts +1 -1
  46. package/dist/theme/index.js +7 -1
  47. package/dist/types/index.d.ts +42 -8
  48. package/dist/utils/chart-series.d.ts +258 -5
  49. package/dist/utils/chart-series.js +423 -20
  50. package/dist/utils/compare-to.d.ts +40 -13
  51. package/dist/utils/compare-to.js +4 -32
  52. package/dist/utils/dashboard-filters.d.ts +54 -8
  53. package/dist/utils/dashboard-filters.js +261 -12
  54. package/dist/utils/dataset-format.d.ts +79 -15
  55. package/dist/utils/dataset-format.js +130 -9
  56. package/dist/utils/dataset-pivot.d.ts +78 -0
  57. package/dist/utils/dataset-pivot.js +78 -0
  58. package/dist/utils/dom-props.d.ts +137 -0
  59. package/dist/utils/dom-props.js +153 -0
  60. package/dist/utils/filter-converter.d.ts +39 -11
  61. package/dist/utils/filter-converter.js +87 -7
  62. package/dist/utils/normalize-list-view.d.ts +26 -6
  63. package/dist/utils/normalize-list-view.js +30 -8
  64. package/dist/utils/number-display.d.ts +151 -0
  65. package/dist/utils/number-display.js +71 -0
  66. package/dist/utils/predicate-fields.d.ts +96 -0
  67. package/dist/utils/predicate-fields.js +163 -0
  68. package/dist/utils/predicate-record.d.ts +32 -0
  69. package/dist/utils/predicate-record.js +120 -0
  70. package/dist/utils/record-title.js +0 -0
  71. package/dist/utils/resolve-view-id.d.ts +31 -0
  72. package/dist/utils/resolve-view-id.js +45 -0
  73. package/dist/utils/sort-query.d.ts +55 -0
  74. package/dist/utils/sort-query.js +35 -0
  75. package/package.json +5 -5
  76. package/dist/protocols/NotificationProtocol.d.ts +0 -71
  77. package/dist/protocols/NotificationProtocol.js +0 -89
  78. package/dist/utils/merge-views-into-objects.d.ts +0 -19
  79. package/dist/utils/merge-views-into-objects.js +0 -39
@@ -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>>;