@object-ui/core 17.6.0 → 18.0.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 +7260 -0
- package/README.md +228 -14
- package/dist/actions/ActionEngine.d.ts +4 -2
- package/dist/actions/ActionEngine.js +13 -8
- package/dist/actions/ActionRunner.d.ts +473 -69
- package/dist/actions/ActionRunner.js +792 -125
- package/dist/actions/TransactionManager.d.ts +7 -2
- package/dist/actions/UndoManager.d.ts +8 -13
- package/dist/actions/actionKeys.d.ts +9 -7
- package/dist/actions/actionKeys.js +67 -14
- package/dist/actions/actionResponse.js +2 -2
- package/dist/actions/index.d.ts +2 -0
- package/dist/actions/index.js +2 -0
- package/dist/actions/navigationTarget.d.ts +41 -0
- package/dist/actions/navigationTarget.js +20 -0
- package/dist/actions/recordDelete.d.ts +154 -0
- package/dist/actions/recordDelete.js +110 -0
- package/dist/adapters/ApiDataSource.d.ts +8 -4
- package/dist/adapters/ApiDataSource.js +19 -7
- 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/adapters/resolveDataSource.d.ts +9 -6
- package/dist/adapters/resolveDataSource.js +9 -12
- package/dist/builder/schema-builder.d.ts +38 -81
- package/dist/builder/schema-builder.js +33 -152
- package/dist/data-scope/ViewDataProvider.d.ts +8 -25
- package/dist/data-scope/ViewDataProvider.js +27 -54
- 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/deadlineSettled.d.ts +40 -0
- package/dist/evaluator/deadlineSettled.js +45 -0
- 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 +208 -31
- package/dist/evaluator/index.d.ts +2 -0
- package/dist/evaluator/index.js +2 -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 +105 -0
- package/dist/evaluator/subjectPermissions.js +86 -0
- package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
- package/dist/evaluator/unevaluablePredicate.js +70 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +83 -2
- package/dist/protocols/index.d.ts +0 -1
- package/dist/protocols/index.js +9 -1
- package/dist/registry/Registry.d.ts +382 -34
- package/dist/registry/Registry.js +247 -12
- 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/affordanceGrants.d.ts +371 -0
- package/dist/utils/affordanceGrants.js +252 -0
- 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 +165 -5
- package/dist/utils/chart-series.js +123 -18
- 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 +346 -0
- package/dist/utils/date-display.js +644 -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 +81 -9
- package/dist/utils/expand-fields.js +81 -9
- package/dist/utils/extract-records.d.ts +65 -5
- package/dist/utils/extract-records.js +65 -8
- package/dist/utils/filter-converter.d.ts +158 -6
- package/dist/utils/filter-converter.js +1098 -24
- 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 +135 -11
- package/dist/utils/normalize-list-view.js +243 -64
- 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 +141 -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 +225 -83
- package/dist/validation/server-owned-value.d.ts +5 -1
- package/dist/validation/server-owned-value.js +5 -1
- 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
|
@@ -9,7 +9,51 @@ export interface ChartResultField {
|
|
|
9
9
|
name: string;
|
|
10
10
|
label?: string;
|
|
11
11
|
format?: string;
|
|
12
|
+
/**
|
|
13
|
+
* The aggregate the measure column carries — the spec's
|
|
14
|
+
* `AnalyticsResult.fields[].aggregate`, which the dataset answer states on
|
|
15
|
+
* every measure column whose measure declares one, labelled or not
|
|
16
|
+
* (objectstack#21995). Absent on dimension columns and derived measures.
|
|
17
|
+
*
|
|
18
|
+
* Typed `string`, not the closed vocabulary, for the reason
|
|
19
|
+
* {@link BuiltinAggregateCarrier} gives: a JSON response is handed here
|
|
20
|
+
* as-is, so the value is judged at the read ({@link isIntegerAggregate}).
|
|
21
|
+
* {@link buildChartSeries} copies it onto each series it derives from the
|
|
22
|
+
* column, so a value axis can tell a count from a sum (objectui#11681).
|
|
23
|
+
*/
|
|
24
|
+
aggregate?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The OPTIONAL wire discriminator naming which server-side BUILT-IN default
|
|
28
|
+
* measure a result column is (objectui#7258; producer half objectstack#14492).
|
|
29
|
+
* The analytics service sets it ONLY on the measures it minted itself with a
|
|
30
|
+
* hard-coded English default label (`count` → `'Count'`, …); an author-declared
|
|
31
|
+
* measure never carries it, so its `label` stays the author's verbatim text
|
|
32
|
+
* (objectui#4106). {@link resolveMeasureLabel} is the one reader.
|
|
33
|
+
*
|
|
34
|
+
* Declared BESIDE {@link ChartResultField} rather than on it, deliberately:
|
|
35
|
+
* `dataset-result-field-spec-parity` pins every key of `ChartResultField` as a
|
|
36
|
+
* key of the spec's `AnalyticsResult.fields[]`, and the `@objectstack/spec`
|
|
37
|
+
* this package is built against does not carry `builtinAggregate` yet. The
|
|
38
|
+
* intersection {@link ChartMeasureField} is what the helpers accept, so a
|
|
39
|
+
* spec-typed field flows in unchanged today and carries the key the moment
|
|
40
|
+
* the spec does — structurally, with no cast at the call sites.
|
|
41
|
+
*
|
|
42
|
+
* Typed as `string` rather than the closed {@link BuiltinAggregate} union on
|
|
43
|
+
* purpose: a JSON response is handed here as-is, so the value is validated at
|
|
44
|
+
* the lookup ({@link isBuiltinAggregate}) instead of trusted at the type.
|
|
45
|
+
*
|
|
46
|
+
* TODO(objectstack#14492): once the spec dependency is bumped past the release
|
|
47
|
+
* that adds `AnalyticsResult.fields[].builtinAggregate`, fold this key into
|
|
48
|
+
* `ChartResultField` as `DatasetResultField['builtinAggregate']` (pinning the
|
|
49
|
+
* two closed vocabularies to each other in the parity suite) and retire the
|
|
50
|
+
* carrier.
|
|
51
|
+
*/
|
|
52
|
+
export interface BuiltinAggregateCarrier {
|
|
53
|
+
builtinAggregate?: string;
|
|
12
54
|
}
|
|
55
|
+
/** A result field as the series helpers read it: the renderer shape plus the optional discriminator. */
|
|
56
|
+
export type ChartMeasureField = ChartResultField & BuiltinAggregateCarrier;
|
|
13
57
|
/**
|
|
14
58
|
* One renderer-internal series binding produced by {@link buildChartSeries}:
|
|
15
59
|
* WHICH result-set column to draw and what to call it.
|
|
@@ -25,12 +69,74 @@ export interface ChartResultField {
|
|
|
25
69
|
export interface ChartSeriesBinding {
|
|
26
70
|
dataKey: string;
|
|
27
71
|
label: string;
|
|
72
|
+
/**
|
|
73
|
+
* The aggregate of the measure this series plots, copied from its result
|
|
74
|
+
* column's `aggregate` (objectui#11681). Renderer-internal: the chart's value
|
|
75
|
+
* axis draws whole-number ticks when every series on it plots an
|
|
76
|
+
* {@link isIntegerAggregate} measure. Absent when the column states none.
|
|
77
|
+
*/
|
|
78
|
+
aggregate?: string;
|
|
28
79
|
}
|
|
29
80
|
export interface ChartSeriesResult {
|
|
30
81
|
data: Array<Record<string, unknown>>;
|
|
31
82
|
xAxisKey: string | undefined;
|
|
32
83
|
series: ChartSeriesBinding[];
|
|
33
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* The closed vocabulary of server-side built-in default measures a result field
|
|
87
|
+
* can declare itself as (objectui#7258 / objectstack#14492) — the spec's own
|
|
88
|
+
* measure-aggregate enum spelling (`count_distinct`, not `countDistinct`), so
|
|
89
|
+
* there is exactly ONE spelling on the wire and this list never grows on its
|
|
90
|
+
* own: a value outside it is not "a new aggregate", it is an unrecognised one,
|
|
91
|
+
* and {@link resolveMeasureLabel} treats it as absent.
|
|
92
|
+
*/
|
|
93
|
+
export declare const BUILTIN_AGGREGATES: readonly ["count", "sum", "avg", "min", "max", "count_distinct"];
|
|
94
|
+
/** One member of {@link BUILTIN_AGGREGATES}. */
|
|
95
|
+
export type BuiltinAggregate = (typeof BUILTIN_AGGREGATES)[number];
|
|
96
|
+
/**
|
|
97
|
+
* The aggregates whose every value is a whole number: a count of rows, or of
|
|
98
|
+
* distinct values (objectui#11681, triage ruling Q3 A on that card). `sum` /
|
|
99
|
+
* `min` / `max` over an integer field would qualify too, but the answer does
|
|
100
|
+
* not state the source field's scale, so they are not judged integer here.
|
|
101
|
+
*/
|
|
102
|
+
export declare const INTEGER_AGGREGATES: readonly ["count", "count_distinct"];
|
|
103
|
+
/**
|
|
104
|
+
* Whether a measure column's `aggregate` makes every value a whole number — a
|
|
105
|
+
* value axis that plots only such measures takes integer ticks
|
|
106
|
+
* (objectui#11681). Judged over an untrusted wire value; anything else,
|
|
107
|
+
* absence included, answers `false`, so the axis keeps its default.
|
|
108
|
+
*/
|
|
109
|
+
export declare function isIntegerAggregate(value: unknown): boolean;
|
|
110
|
+
/** Runtime membership test for {@link BUILTIN_AGGREGATES}, over an untrusted wire value. */
|
|
111
|
+
export declare function isBuiltinAggregate(value: unknown): value is BuiltinAggregate;
|
|
112
|
+
/**
|
|
113
|
+
* The LOCALIZED display label per built-in aggregate, as the layer that holds
|
|
114
|
+
* the i18n provider resolved them (`@object-ui/i18n`'s
|
|
115
|
+
* `builtinAggregateLabels(tt)`). Partial on purpose: an aggregate the caller
|
|
116
|
+
* did not resolve falls back to the wire `label`, exactly as if the field had
|
|
117
|
+
* carried no discriminator.
|
|
118
|
+
*/
|
|
119
|
+
export type BuiltinAggregateLabels = Partial<Record<BuiltinAggregate, string>>;
|
|
120
|
+
/**
|
|
121
|
+
* The display label of ONE result field, for a legend / axis / tooltip.
|
|
122
|
+
*
|
|
123
|
+
* Resolution order (objectui#7258, maintainer ruling B of 2026-09-02):
|
|
124
|
+
* 1. the field declares itself a built-in default measure
|
|
125
|
+
* (`builtinAggregate` ∈ {@link BUILTIN_AGGREGATES}) AND the caller resolved
|
|
126
|
+
* a locale label for it → that label. This is the only path that can
|
|
127
|
+
* replace a wire `label`, and it is keyed by a STRUCTURAL discriminator —
|
|
128
|
+
* never by matching the label's text or the field's name, both of which
|
|
129
|
+
* the ruling rejected (a real author measure literally named `count`, or
|
|
130
|
+
* labelled `Count`, must keep its own label);
|
|
131
|
+
* 2. otherwise the wire `label` verbatim — the objectui#4106 contract for an
|
|
132
|
+
* author-declared measure (`'Tasks'` reaches the legend untouched);
|
|
133
|
+
* 3. otherwise the field's `name`.
|
|
134
|
+
*
|
|
135
|
+
* So a discriminator the caller has no label for (a provider-less host, or a
|
|
136
|
+
* value outside the closed vocabulary) costs nothing: the field renders exactly
|
|
137
|
+
* what it rendered before the discriminator existed.
|
|
138
|
+
*/
|
|
139
|
+
export declare function resolveMeasureLabel(field: ChartMeasureField, labels?: BuiltinAggregateLabels): string;
|
|
34
140
|
/**
|
|
35
141
|
* The bucket a NULL/undefined category VALUE renders under, when the caller
|
|
36
142
|
* supplies no localized label of its own (objectui#4466).
|
|
@@ -110,6 +216,16 @@ export interface ChartSeriesOptions {
|
|
|
110
216
|
* it, so a mismatch costs the null bucket its drill-through.
|
|
111
217
|
*/
|
|
112
218
|
nullCategoryLabel?: string;
|
|
219
|
+
/**
|
|
220
|
+
* Locale labels for the server's BUILT-IN default measures, keyed by the
|
|
221
|
+
* wire discriminator a result field carries as `builtinAggregate`
|
|
222
|
+
* (objectui#7258). Same division of labour as `nullCategoryLabel`: this
|
|
223
|
+
* package is React-free and i18n-free, so the renderer that holds the
|
|
224
|
+
* provider resolves the strings (`builtinAggregateLabels(tt)` in
|
|
225
|
+
* `@object-ui/i18n`) and passes them in. Absent ⇒ every measure keeps its
|
|
226
|
+
* wire `label`, which for a built-in default is the server's English.
|
|
227
|
+
*/
|
|
228
|
+
builtinAggregateLabels?: BuiltinAggregateLabels;
|
|
113
229
|
}
|
|
114
230
|
/** Per-CLICK knobs for {@link findChartSeriesRow}, on top of the shared ones. */
|
|
115
231
|
export interface ChartDrillLookupOptions extends ChartSeriesOptions {
|
|
@@ -174,7 +290,7 @@ export interface ChartSegmentClickEvent {
|
|
|
174
290
|
/** The measure value at the click point. */
|
|
175
291
|
value?: number;
|
|
176
292
|
}
|
|
177
|
-
export declare function buildChartSeries(rows: Array<Record<string, unknown>> | null | undefined, dimensions: string[] | null | undefined, values: string[] | null | undefined, fields?:
|
|
293
|
+
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
294
|
/**
|
|
179
295
|
* Resolve select/enum dimension VALUES to display LABELS in chart rows.
|
|
180
296
|
*
|
|
@@ -362,10 +478,54 @@ export declare function buildDimensionLabelMap(options: unknown, translate?: Opt
|
|
|
362
478
|
* The object a relationship field points at, or `undefined` when the field is
|
|
363
479
|
* not a relationship (or names no target).
|
|
364
480
|
*
|
|
365
|
-
* The target lives under `reference`
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
481
|
+
* The target lives under `reference` — the ONLY spelling read here, and the same
|
|
482
|
+
* canonicalization as the dataset designer's `resolveReferenceTo`, which is
|
|
483
|
+
* narrowed to match in the same pass (objectui#6528). As of objectui#6648 the
|
|
484
|
+
* CARRIER matches too: a bare string and nothing else. The two helpers are one
|
|
485
|
+
* doctrine in two files and must never drift — a divergence recreates the
|
|
486
|
+
* defect one file over.
|
|
487
|
+
*
|
|
488
|
+
* The three legacy spellings this dropped (`reference_to` / `referenceTo` /
|
|
489
|
+
* `reference_to_object`) were censused against every producer, with `reference`
|
|
490
|
+
* itself as the positive control (445 of 565 lookup / master_detail defs in the
|
|
491
|
+
* framework tree; ACCEPTED by `ObjectSchema.safeParse` on spec 17.2.0, which
|
|
492
|
+
* REFUSES all three others BY NAME). No producer emits any of them onto an
|
|
493
|
+
* object metadata document: `reference_to` was then live only on ObjectUI's own
|
|
494
|
+
* view/field schema (a different contract, translated INTO from `reference`;
|
|
495
|
+
* objectui#11070 round 4 moved that contract to `reference` as well),
|
|
496
|
+
* `referenceTo`'s two producers were retired by objectui#6041, and
|
|
497
|
+
* `reference_to_object` occurs nowhere in either tree outside this chain and the
|
|
498
|
+
* test that called it.
|
|
499
|
+
*
|
|
500
|
+
* ⚠ Unlike `resolveReferenceTo`, this helper reads the `GET /meta/object/:name`
|
|
501
|
+
* document DIRECTLY — no `readFields` door strips retired keys on this path. So
|
|
502
|
+
* the narrowing is load-bearing rather than cosmetic: a stored pre-objectui#6041
|
|
503
|
+
* row spelling the target `referenceTo` now resolves to `undefined` here, the
|
|
504
|
+
* walk yields no entry, and the caller keeps the raw value (best-effort by
|
|
505
|
+
* construction — see {@link resolveDimensionFieldMeta}). That is the intended
|
|
506
|
+
* outcome: per AGENTS.md #0.1 such a row is a PRODUCER-side defect, and a
|
|
507
|
+
* lenient consumer here is exactly where it would have stayed hidden. Note
|
|
508
|
+
* `reference` was already HEAD of the old chain, so any doc carrying both is
|
|
509
|
+
* unaffected.
|
|
510
|
+
*
|
|
511
|
+
* The array and `{ object }` CARRIERS this used to accept were censused on the
|
|
512
|
+
* same standard and removed with the same reasoning (objectui#6648).
|
|
513
|
+
* `FieldSchema.reference` is a plain `z.string()`; `ObjectSchema.safeParse`
|
|
514
|
+
* (spec 17.2.0) ACCEPTS `reference: 'crm_account'` and REFUSES both
|
|
515
|
+
* `['crm_account']` and `{ object: 'crm_account' }` as `invalid_type`. A
|
|
516
|
+
* structure-walking, key-position-aware census of both trees found 587
|
|
517
|
+
* bare-string carriers at the field-def key position and ZERO array or
|
|
518
|
+
* `{ object }` carriers from any producer — the only ones in either tree were
|
|
519
|
+
* the green pins asserting this very tolerance. See `resolveReferenceTo` for
|
|
520
|
+
* the full census, including why a multi-target lookup is NOT what the array
|
|
521
|
+
* branch was serving: it returned `raw[0]` and discarded the rest, and
|
|
522
|
+
* polymorphic lookup remains an undeclared, open spec gap.
|
|
523
|
+
*
|
|
524
|
+
* This path matters MORE than the designer's for the same reason the spelling
|
|
525
|
+
* narrowing did: it reads the metadata document directly, with no `readFields`
|
|
526
|
+
* door in between, so a non-string carrier now degrades visibly (the walk
|
|
527
|
+
* yields no entry and the caller keeps the raw value) instead of being absorbed
|
|
528
|
+
* here.
|
|
369
529
|
*
|
|
370
530
|
* The **type gate is deliberate**: only a declared relationship is walked, so a
|
|
371
531
|
* path segment naming a plain field can never be turned into an object name and
|
|
@@ -44,6 +44,63 @@
|
|
|
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
|
+
/**
|
|
57
|
+
* The aggregates whose every value is a whole number: a count of rows, or of
|
|
58
|
+
* distinct values (objectui#11681, triage ruling Q3 A on that card). `sum` /
|
|
59
|
+
* `min` / `max` over an integer field would qualify too, but the answer does
|
|
60
|
+
* not state the source field's scale, so they are not judged integer here.
|
|
61
|
+
*/
|
|
62
|
+
export const INTEGER_AGGREGATES = ['count', 'count_distinct'];
|
|
63
|
+
/**
|
|
64
|
+
* Whether a measure column's `aggregate` makes every value a whole number — a
|
|
65
|
+
* value axis that plots only such measures takes integer ticks
|
|
66
|
+
* (objectui#11681). Judged over an untrusted wire value; anything else,
|
|
67
|
+
* absence included, answers `false`, so the axis keeps its default.
|
|
68
|
+
*/
|
|
69
|
+
export function isIntegerAggregate(value) {
|
|
70
|
+
return typeof value === 'string' && INTEGER_AGGREGATES.includes(value);
|
|
71
|
+
}
|
|
72
|
+
/** Runtime membership test for {@link BUILTIN_AGGREGATES}, over an untrusted wire value. */
|
|
73
|
+
export function isBuiltinAggregate(value) {
|
|
74
|
+
return typeof value === 'string' && BUILTIN_AGGREGATES.includes(value);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The display label of ONE result field, for a legend / axis / tooltip.
|
|
78
|
+
*
|
|
79
|
+
* Resolution order (objectui#7258, maintainer ruling B of 2026-09-02):
|
|
80
|
+
* 1. the field declares itself a built-in default measure
|
|
81
|
+
* (`builtinAggregate` ∈ {@link BUILTIN_AGGREGATES}) AND the caller resolved
|
|
82
|
+
* a locale label for it → that label. This is the only path that can
|
|
83
|
+
* replace a wire `label`, and it is keyed by a STRUCTURAL discriminator —
|
|
84
|
+
* never by matching the label's text or the field's name, both of which
|
|
85
|
+
* the ruling rejected (a real author measure literally named `count`, or
|
|
86
|
+
* labelled `Count`, must keep its own label);
|
|
87
|
+
* 2. otherwise the wire `label` verbatim — the objectui#4106 contract for an
|
|
88
|
+
* author-declared measure (`'Tasks'` reaches the legend untouched);
|
|
89
|
+
* 3. otherwise the field's `name`.
|
|
90
|
+
*
|
|
91
|
+
* So a discriminator the caller has no label for (a provider-less host, or a
|
|
92
|
+
* value outside the closed vocabulary) costs nothing: the field renders exactly
|
|
93
|
+
* what it rendered before the discriminator existed.
|
|
94
|
+
*/
|
|
95
|
+
export function resolveMeasureLabel(field, labels) {
|
|
96
|
+
const aggregate = field.builtinAggregate;
|
|
97
|
+
if (isBuiltinAggregate(aggregate)) {
|
|
98
|
+
const localized = labels?.[aggregate];
|
|
99
|
+
if (typeof localized === 'string' && localized !== '')
|
|
100
|
+
return localized;
|
|
101
|
+
}
|
|
102
|
+
return field.label ?? field.name;
|
|
103
|
+
}
|
|
47
104
|
/**
|
|
48
105
|
* The bucket a NULL/undefined category VALUE renders under, when the caller
|
|
49
106
|
* supplies no localized label of its own (objectui#4466).
|
|
@@ -296,7 +353,19 @@ export function buildChartSeries(rows, dimensions, values, fields, options) {
|
|
|
296
353
|
const dims = (dimensions ?? []).filter(Boolean);
|
|
297
354
|
const vals = (values ?? []).filter(Boolean);
|
|
298
355
|
const safeRows = Array.isArray(rows) ? rows : [];
|
|
299
|
-
|
|
356
|
+
// A measure's series label: the locale label when the field declares itself
|
|
357
|
+
// a built-in default measure and the caller resolved one, else the wire
|
|
358
|
+
// `label` verbatim (objectui#4106), else the name — see `resolveMeasureLabel`.
|
|
359
|
+
const labelOf = (name) => {
|
|
360
|
+
const field = (fields ?? []).find((f) => f.name === name);
|
|
361
|
+
return field ? resolveMeasureLabel(field, options?.builtinAggregateLabels) : name;
|
|
362
|
+
};
|
|
363
|
+
// The measure column's own `aggregate`, spread onto each series that plots
|
|
364
|
+
// it (objectui#11681) — present only when the column states a string one.
|
|
365
|
+
const aggregateOf = (name) => {
|
|
366
|
+
const aggregate = (fields ?? []).find((f) => f.name === name)?.aggregate;
|
|
367
|
+
return typeof aggregate === 'string' ? { aggregate } : {};
|
|
368
|
+
};
|
|
300
369
|
// Multi-dimension, single-measure → pivot the second dimension into series.
|
|
301
370
|
if (dims.length >= 2 && vals.length === 1) {
|
|
302
371
|
const xKey = dims[0];
|
|
@@ -402,7 +471,8 @@ export function buildChartSeries(rows, dimensions, values, fields, options) {
|
|
|
402
471
|
// server-resolved to display labels by queryDataset), except the null
|
|
403
472
|
// group, which says so with the bucket label the renderer supplied. The
|
|
404
473
|
// key is a separate question from the text — see `pivotSeriesBuckets`.
|
|
405
|
-
|
|
474
|
+
// Every group plots the one measure, so each carries its aggregate.
|
|
475
|
+
series: groups.map(({ dataKey, label }) => ({ dataKey, label, ...aggregateOf(measure) })),
|
|
406
476
|
};
|
|
407
477
|
}
|
|
408
478
|
// Default: first dimension on the x-axis, one series per measure.
|
|
@@ -425,7 +495,7 @@ export function buildChartSeries(rows, dimensions, values, fields, options) {
|
|
|
425
495
|
? bucketNullCategories(safeRows, xKey, options?.nullCategoryLabel ?? NULL_CATEGORY_LABEL)
|
|
426
496
|
: safeRows,
|
|
427
497
|
xAxisKey: xKey,
|
|
428
|
-
series: vals.map((v) => ({ dataKey: v, label: labelOf(v) })),
|
|
498
|
+
series: vals.map((v) => ({ dataKey: v, label: labelOf(v), ...aggregateOf(v) })),
|
|
429
499
|
};
|
|
430
500
|
}
|
|
431
501
|
/**
|
|
@@ -765,10 +835,54 @@ function fieldDefsOf(schema) {
|
|
|
765
835
|
* The object a relationship field points at, or `undefined` when the field is
|
|
766
836
|
* not a relationship (or names no target).
|
|
767
837
|
*
|
|
768
|
-
* The target lives under `reference`
|
|
769
|
-
*
|
|
770
|
-
*
|
|
771
|
-
*
|
|
838
|
+
* The target lives under `reference` — the ONLY spelling read here, and the same
|
|
839
|
+
* canonicalization as the dataset designer's `resolveReferenceTo`, which is
|
|
840
|
+
* narrowed to match in the same pass (objectui#6528). As of objectui#6648 the
|
|
841
|
+
* CARRIER matches too: a bare string and nothing else. The two helpers are one
|
|
842
|
+
* doctrine in two files and must never drift — a divergence recreates the
|
|
843
|
+
* defect one file over.
|
|
844
|
+
*
|
|
845
|
+
* The three legacy spellings this dropped (`reference_to` / `referenceTo` /
|
|
846
|
+
* `reference_to_object`) were censused against every producer, with `reference`
|
|
847
|
+
* itself as the positive control (445 of 565 lookup / master_detail defs in the
|
|
848
|
+
* framework tree; ACCEPTED by `ObjectSchema.safeParse` on spec 17.2.0, which
|
|
849
|
+
* REFUSES all three others BY NAME). No producer emits any of them onto an
|
|
850
|
+
* object metadata document: `reference_to` was then live only on ObjectUI's own
|
|
851
|
+
* view/field schema (a different contract, translated INTO from `reference`;
|
|
852
|
+
* objectui#11070 round 4 moved that contract to `reference` as well),
|
|
853
|
+
* `referenceTo`'s two producers were retired by objectui#6041, and
|
|
854
|
+
* `reference_to_object` occurs nowhere in either tree outside this chain and the
|
|
855
|
+
* test that called it.
|
|
856
|
+
*
|
|
857
|
+
* ⚠ Unlike `resolveReferenceTo`, this helper reads the `GET /meta/object/:name`
|
|
858
|
+
* document DIRECTLY — no `readFields` door strips retired keys on this path. So
|
|
859
|
+
* the narrowing is load-bearing rather than cosmetic: a stored pre-objectui#6041
|
|
860
|
+
* row spelling the target `referenceTo` now resolves to `undefined` here, the
|
|
861
|
+
* walk yields no entry, and the caller keeps the raw value (best-effort by
|
|
862
|
+
* construction — see {@link resolveDimensionFieldMeta}). That is the intended
|
|
863
|
+
* outcome: per AGENTS.md #0.1 such a row is a PRODUCER-side defect, and a
|
|
864
|
+
* lenient consumer here is exactly where it would have stayed hidden. Note
|
|
865
|
+
* `reference` was already HEAD of the old chain, so any doc carrying both is
|
|
866
|
+
* unaffected.
|
|
867
|
+
*
|
|
868
|
+
* The array and `{ object }` CARRIERS this used to accept were censused on the
|
|
869
|
+
* same standard and removed with the same reasoning (objectui#6648).
|
|
870
|
+
* `FieldSchema.reference` is a plain `z.string()`; `ObjectSchema.safeParse`
|
|
871
|
+
* (spec 17.2.0) ACCEPTS `reference: 'crm_account'` and REFUSES both
|
|
872
|
+
* `['crm_account']` and `{ object: 'crm_account' }` as `invalid_type`. A
|
|
873
|
+
* structure-walking, key-position-aware census of both trees found 587
|
|
874
|
+
* bare-string carriers at the field-def key position and ZERO array or
|
|
875
|
+
* `{ object }` carriers from any producer — the only ones in either tree were
|
|
876
|
+
* the green pins asserting this very tolerance. See `resolveReferenceTo` for
|
|
877
|
+
* the full census, including why a multi-target lookup is NOT what the array
|
|
878
|
+
* branch was serving: it returned `raw[0]` and discarded the rest, and
|
|
879
|
+
* polymorphic lookup remains an undeclared, open spec gap.
|
|
880
|
+
*
|
|
881
|
+
* This path matters MORE than the designer's for the same reason the spelling
|
|
882
|
+
* narrowing did: it reads the metadata document directly, with no `readFields`
|
|
883
|
+
* door in between, so a non-string carrier now degrades visibly (the walk
|
|
884
|
+
* yields no entry and the caller keeps the raw value) instead of being absorbed
|
|
885
|
+
* here.
|
|
772
886
|
*
|
|
773
887
|
* The **type gate is deliberate**: only a declared relationship is walked, so a
|
|
774
888
|
* path segment naming a plain field can never be turned into an object name and
|
|
@@ -781,17 +895,8 @@ export function resolveRelationshipTarget(fieldDef) {
|
|
|
781
895
|
const type = typeof def.type === 'string' ? def.type.toLowerCase() : '';
|
|
782
896
|
if (!RELATIONSHIP_FIELD_TYPES.has(type))
|
|
783
897
|
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;
|
|
898
|
+
const raw = def.reference;
|
|
899
|
+
return typeof raw === 'string' && raw ? raw : undefined;
|
|
795
900
|
}
|
|
796
901
|
/**
|
|
797
902
|
* Resolve each dataset dimension's underlying `field` to that field's select
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — served per-column sortability projection (consumer side)
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The consumer half of objectstack#10235 (maintainer ruling A, 2026-08-23):
|
|
10
|
+
* the platform serves an explicit per-column sortability signal, and a grid
|
|
11
|
+
* reads THAT to decide whether a header offers a sort click — it never
|
|
12
|
+
* re-derives "virtual ⇒ unsortable" from the field's `type`.
|
|
13
|
+
*
|
|
14
|
+
* ## What the platform serves
|
|
15
|
+
*
|
|
16
|
+
* `GET /api/v1/meta/object/:name` answers `GetMetaItemResponseSchema`, whose
|
|
17
|
+
* ENVELOPE (never the document under `item` — `FieldSchema` is a
|
|
18
|
+
* `strictObject`, so the key must stay un-authorable) carries an optional
|
|
19
|
+
* `sortability` key when the served type is `object`, on every serving branch
|
|
20
|
+
* including the cached one:
|
|
21
|
+
*
|
|
22
|
+
* ```jsonc
|
|
23
|
+
* { "type": "object", "name": "crm_opportunity", "item": { … },
|
|
24
|
+
* "sortability": { "fields": {
|
|
25
|
+
* "amount": { "sortable": true },
|
|
26
|
+
* "expected_revenue": { "sortable": false, "reason": "virtual-type" },
|
|
27
|
+
* "remote_anchor": { "sortable": true, "caveat": "unprovisioned-anchor" }
|
|
28
|
+
* } } }
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* Shape verified against the MERGED upstream change, not against a description
|
|
32
|
+
* of it: `@objectstack/spec`'s `ObjectSortabilitySchema` /
|
|
33
|
+
* `FieldSortabilitySchema` and `GetMetaItemResponseSchema.sortability`.
|
|
34
|
+
*
|
|
35
|
+
* ## The contract, and the asymmetry that makes it easy to get backwards
|
|
36
|
+
*
|
|
37
|
+
* Offer a sort affordance on a column **iff the projection has an entry for
|
|
38
|
+
* the column's field name AND that entry says `sortable: true`**.
|
|
39
|
+
*
|
|
40
|
+
* ABSENCE OF AN ENTRY MEANS "no platform sort behind this name" — never
|
|
41
|
+
* "assume sortable". The projection's domain is exactly the served field map
|
|
42
|
+
* plus the always-provisioned `id`, so an unknown name, a dotted path
|
|
43
|
+
* (`account.name`) and an unprovisioned audit column all arrive as absence,
|
|
44
|
+
* and all three are refused by the runtime doors. A `!== false` test — the
|
|
45
|
+
* spelling every other optional flag in this repo uses — answers `true` for
|
|
46
|
+
* absence and so gets exactly that family backwards. {@link isPlatformSortableField}
|
|
47
|
+
* is the only spelling of this test; do not inline a comparison at a call site.
|
|
48
|
+
*
|
|
49
|
+
* ## Present-vs-absent PROJECTION is a different question
|
|
50
|
+
*
|
|
51
|
+
* `readObjectSortability` returns `undefined` when the served metadata carried
|
|
52
|
+
* no `sortability` key at all — a backend older than the upstream change, an
|
|
53
|
+
* inline/mock data source, a fixture. That is NOT "every column is unsortable":
|
|
54
|
+
* it is "no signal was served", and the caller decides what it did before the
|
|
55
|
+
* signal existed. The two are deliberately different types (`undefined` vs an
|
|
56
|
+
* empty `fields` map) so a caller cannot conflate them by accident.
|
|
57
|
+
*
|
|
58
|
+
* ## `caveat: 'unprovisioned-anchor'` is NOT a refusal
|
|
59
|
+
*
|
|
60
|
+
* Platform-injected anchors on ADR-0015 `external` objects come back
|
|
61
|
+
* `sortable: true, caveat: 'unprovisioned-anchor'`: the runtime ACCEPTS the
|
|
62
|
+
* sort, but silently drops it (`asc` === `desc` under a `200`) when the remote
|
|
63
|
+
* table carries no such column — a degradation the platform cannot prove
|
|
64
|
+
* either way at serve time. This module treats the caveat as advisory and the
|
|
65
|
+
* `sortable` verdict as the enforcement fact, so a caveated column keeps its
|
|
66
|
+
* click. Refusing what the platform does not refuse would recreate
|
|
67
|
+
* declared-≠-enforced drift in mirror image; whether to badge or withhold on
|
|
68
|
+
* the caveat is an open affordance question, not something to settle here.
|
|
69
|
+
*/
|
|
70
|
+
/**
|
|
71
|
+
* The served shape is the SPEC's, re-exported rather than restated.
|
|
72
|
+
*
|
|
73
|
+
* `FIELD_UNSORTABLE_VIRTUAL_TYPE` / `FIELD_SORTABLE_UNPROVISIONED_ANCHOR` are
|
|
74
|
+
* the two string literals the projection can carry, and `FieldSortability` /
|
|
75
|
+
* `ObjectSortability` are the wire types themselves (`z.input` of the
|
|
76
|
+
* schemas the REST layer answers with). Re-exporting is the point of a
|
|
77
|
+
* contract-first consumer: a local declaration under a spec export's name is
|
|
78
|
+
* read by the next agent as the spec's own definition, and then drifts from
|
|
79
|
+
* it — which is what `check:spec-symbols` guards, and what it caught here.
|
|
80
|
+
*/
|
|
81
|
+
export { FIELD_UNSORTABLE_VIRTUAL_TYPE, FIELD_SORTABLE_UNPROVISIONED_ANCHOR, } from '@objectstack/spec/api';
|
|
82
|
+
export type { FieldSortability, ObjectSortability } from '@objectstack/spec/api';
|
|
83
|
+
import type { ObjectSortability } from '@objectstack/spec/api';
|
|
84
|
+
/**
|
|
85
|
+
* Where the projection rides on the client-side object schema.
|
|
86
|
+
*
|
|
87
|
+
* A GLOBAL-REGISTRY SYMBOL, deliberately — the same carrier shape
|
|
88
|
+
* `INFLIGHT_GET_REGISTRY_KEY` uses in `@object-ui/types`. Three properties
|
|
89
|
+
* this signal needs and a string key would not give it:
|
|
90
|
+
*
|
|
91
|
+
* - `JSON.stringify` drops symbol-keyed properties, so a schema object that
|
|
92
|
+
* is ever round-tripped back at a metadata WRITE endpoint cannot carry the
|
|
93
|
+
* projection into a document the server parses `strict` and rejects by
|
|
94
|
+
* name. The upstream change kept the key off `item` for exactly that
|
|
95
|
+
* reason; stamping a string key here would undo it one repo away.
|
|
96
|
+
* - `Object.keys` / spread / `for…in` do not see it, so every existing reader
|
|
97
|
+
* that enumerates the schema (column generators, diffing, form builders)
|
|
98
|
+
* is unchanged by its presence.
|
|
99
|
+
* - `Symbol.for` is the cross-realm registry, so two copies of this module
|
|
100
|
+
* (a workspace build and a published one in the same graph) agree on the
|
|
101
|
+
* key rather than silently reading past each other.
|
|
102
|
+
*/
|
|
103
|
+
export declare const OBJECT_SORTABILITY_KEY: unique symbol;
|
|
104
|
+
/**
|
|
105
|
+
* Normalize a served `sortability` envelope value into the projection, or
|
|
106
|
+
* `undefined` when the envelope carried nothing usable.
|
|
107
|
+
*
|
|
108
|
+
* Tolerant on purpose (the same contract the upstream resolver carries): the
|
|
109
|
+
* adapter hands this whatever came back over the wire, unparsed.
|
|
110
|
+
*/
|
|
111
|
+
export declare function normalizeObjectSortability(served: unknown): ObjectSortability | undefined;
|
|
112
|
+
/**
|
|
113
|
+
* Stamp the served projection onto the client-side object schema, under
|
|
114
|
+
* {@link OBJECT_SORTABILITY_KEY}.
|
|
115
|
+
*
|
|
116
|
+
* Non-enumerable and non-writable-by-accident: the property is defined rather
|
|
117
|
+
* than assigned so it never shows up in a spread of the schema and cannot be
|
|
118
|
+
* clobbered by one. Returns the same object it was handed (the adapter caches
|
|
119
|
+
* that instance), and is a no-op when there is nothing usable to attach — so
|
|
120
|
+
* an older backend leaves the schema exactly as it found it rather than
|
|
121
|
+
* stamping an empty projection that would read as "nothing is sortable".
|
|
122
|
+
*/
|
|
123
|
+
export declare function attachObjectSortability<T>(schema: T, served: unknown): T;
|
|
124
|
+
/**
|
|
125
|
+
* Read the served projection off a client-side object schema.
|
|
126
|
+
*
|
|
127
|
+
* `undefined` means NO SIGNAL WAS SERVED — not "nothing is sortable". See the
|
|
128
|
+
* module docblock: callers must branch on this before applying the contract.
|
|
129
|
+
*/
|
|
130
|
+
export declare function readObjectSortability(schema: unknown): ObjectSortability | undefined;
|
|
131
|
+
/**
|
|
132
|
+
* THE contract test: does the platform offer a sort over this field name?
|
|
133
|
+
*
|
|
134
|
+
* `true` iff the projection has an entry for `fieldName` and that entry says
|
|
135
|
+
* `sortable: true`. Every other case — no entry, `sortable: false`, a
|
|
136
|
+
* malformed entry — answers `false`, because absence means "no platform sort
|
|
137
|
+
* behind this name".
|
|
138
|
+
*
|
|
139
|
+
* `sortability` is REQUIRED and non-optional on purpose: a caller holding
|
|
140
|
+
* `undefined` (no signal served) has a different question to answer and must
|
|
141
|
+
* not reach this function with it. See {@link readObjectSortability}.
|
|
142
|
+
*/
|
|
143
|
+
export declare function isPlatformSortableField(sortability: ObjectSortability, fieldName: string | undefined): boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Drop from a sort list every entry the platform will not order by.
|
|
146
|
+
*
|
|
147
|
+
* The RESTORE leg of the same contract: withholding the header click stops a
|
|
148
|
+
* fresh unsortable sort from being created, but a sort persisted BEFORE the
|
|
149
|
+
* signal existed is already in stored view state, and replaying it would keep
|
|
150
|
+
* a refused `$orderby` alive and re-persist it on the next unrelated edit.
|
|
151
|
+
* Filtering the list at the seam the grid both renders from and emits through
|
|
152
|
+
* means an already-stored entry is inert and is dropped the first time
|
|
153
|
+
* anything writes the sort back.
|
|
154
|
+
*/
|
|
155
|
+
export declare function filterPlatformSortableSort<T extends {
|
|
156
|
+
field?: string;
|
|
157
|
+
}>(sort: readonly T[] | undefined, sortability: ObjectSortability): T[];
|