@object-ui/core 17.5.0 → 17.6.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 +794 -0
- package/README.md +36 -22
- package/dist/actions/ActionEngine.d.ts +1 -1
- package/dist/actions/ActionEngine.js +1 -2
- package/dist/actions/ActionRunner.d.ts +82 -10
- package/dist/actions/ActionRunner.js +128 -22
- package/dist/actions/actionKeys.d.ts +68 -8
- package/dist/actions/actionKeys.js +81 -9
- package/dist/actions/actionResponse.js +1 -1
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/actions/recordIdParam.d.ts +77 -0
- package/dist/actions/recordIdParam.js +37 -0
- package/dist/actions/serverActionHandler.d.ts +1 -1
- package/dist/actions/serverActionHandler.js +1 -1
- package/dist/evaluator/ExpressionEvaluator.d.ts +42 -1
- package/dist/evaluator/ExpressionEvaluator.js +42 -1
- package/dist/evaluator/SafeExpressionParser.js +0 -4
- package/dist/evaluator/declaredPredicate.js +1 -1
- package/dist/evaluator/fieldRules.d.ts +19 -0
- package/dist/evaluator/fieldRules.js +30 -6
- package/dist/evaluator/index.d.ts +1 -0
- package/dist/evaluator/index.js +1 -0
- package/dist/evaluator/listConditional.d.ts +49 -2
- package/dist/evaluator/listConditional.js +70 -6
- package/dist/evaluator/optionRules.d.ts +55 -0
- package/dist/evaluator/optionRules.js +60 -0
- package/dist/evaluator/rowPredicateCanon.d.ts +67 -0
- package/dist/evaluator/rowPredicateCanon.js +208 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +17 -0
- package/dist/registry/Registry.d.ts +41 -19
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.js +1 -1
- package/dist/types/index.d.ts +92 -13
- package/dist/utils/chart-presentation.d.ts +205 -0
- package/dist/utils/chart-presentation.js +280 -0
- package/dist/utils/chart-series.d.ts +140 -26
- package/dist/utils/chart-series.js +380 -39
- package/dist/utils/column-identity.d.ts +43 -0
- package/dist/utils/column-identity.js +47 -1
- package/dist/utils/dataset-format.js +18 -6
- package/dist/utils/debug.js +7 -0
- package/dist/utils/expand-fields.d.ts +97 -7
- package/dist/utils/expand-fields.js +97 -7
- package/dist/utils/filter-tokens.js +0 -1
- package/dist/utils/freeze-schema.d.ts +38 -1
- package/dist/utils/freeze-schema.js +10 -0
- package/dist/utils/humanize-label.d.ts +60 -0
- package/dist/utils/humanize-label.js +62 -0
- package/dist/utils/managedBy.d.ts +27 -2
- package/dist/utils/normalize-list-view.d.ts +10 -0
- package/dist/utils/normalize-list-view.js +63 -1
- package/dist/utils/number-display.d.ts +7 -3
- package/dist/utils/predicate-fields.d.ts +29 -4
- package/dist/utils/predicate-fields.js +44 -4
- package/dist/utils/retired-field-types.d.ts +90 -0
- package/dist/utils/retired-field-types.js +124 -0
- package/dist/utils/sort-query.d.ts +12 -10
- package/dist/utils/unmaterialized-fields.d.ts +52 -0
- package/dist/utils/unmaterialized-fields.js +57 -0
- package/dist/validation/index.d.ts +1 -0
- package/dist/validation/index.js +1 -0
- package/dist/validation/schema-validator.js +105 -0
- package/dist/validation/server-owned-value.d.ts +62 -0
- package/dist/validation/server-owned-value.js +99 -0
- package/dist/validation/validators/object-validation-engine.js +0 -2
- package/package.json +4 -4
- package/dist/__benchmarks__/core.bench.d.ts +0 -8
- package/dist/__benchmarks__/core.bench.js +0 -53
package/dist/utils/debug.js
CHANGED
|
@@ -92,9 +92,15 @@ export function debugLog(category, message, data) {
|
|
|
92
92
|
if (!isDebugEnabled())
|
|
93
93
|
return;
|
|
94
94
|
if (data !== undefined) {
|
|
95
|
+
// objectui#4029 — this IS the repo's debug channel (opt-in via
|
|
96
|
+
// globalThis.OBJECTUI_DEBUG, gated by isDebugEnabled() above), not a
|
|
97
|
+
// leaked module-scope log; no-console's blanket ban would otherwise
|
|
98
|
+
// block the function whose entire job is to console.log.
|
|
99
|
+
// eslint-disable-next-line no-console
|
|
95
100
|
console.log(`[ObjectUI Debug][${category}] ${message}`, data);
|
|
96
101
|
}
|
|
97
102
|
else {
|
|
103
|
+
// eslint-disable-next-line no-console -- see comment above
|
|
98
104
|
console.log(`[ObjectUI Debug][${category}] ${message}`);
|
|
99
105
|
}
|
|
100
106
|
}
|
|
@@ -116,6 +122,7 @@ export function debugTimeEnd(label) {
|
|
|
116
122
|
const start = timers.get(label);
|
|
117
123
|
if (start !== undefined) {
|
|
118
124
|
const elapsed = (performance.now() - start).toFixed(2);
|
|
125
|
+
// eslint-disable-next-line no-console -- objectui#4029, see debugLog above
|
|
119
126
|
console.log(`[ObjectUI Debug][perf] ${label}: ${elapsed}ms`);
|
|
120
127
|
timers.delete(label);
|
|
121
128
|
}
|
|
@@ -11,17 +11,107 @@
|
|
|
11
11
|
* list / grid / detail cell can render the related record's display name
|
|
12
12
|
* instead of a bare id placeholder ("—").
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* lookup
|
|
17
|
-
*
|
|
18
|
-
* `reference` + id storage), so a `user` column that is NOT requested for
|
|
19
|
-
* expansion comes back as a raw user id and renders as "—". Keeping the set
|
|
20
|
-
* here as the single source of truth is what closes that gap.
|
|
14
|
+
* `user` is a lookup specialised to `sys_user`: it carries the same `reference`
|
|
15
|
+
* + id storage and the server resolves it through the same expand path as
|
|
16
|
+
* `lookup` / `master_detail`, so a `user` column that is NOT requested for
|
|
17
|
+
* expansion comes back as a raw user id and renders as "—" (objectui#2032).
|
|
21
18
|
*
|
|
22
19
|
* Note on `tree`: a self-referencing hierarchy field is a reference too, so it
|
|
23
20
|
* belongs in this set; whether the backend materialises the expanded object
|
|
24
21
|
* for it is a server concern — requesting it is harmless and forward-compatible.
|
|
22
|
+
*
|
|
23
|
+
* ## One family, many consumers — and NO reliable count of them
|
|
24
|
+
*
|
|
25
|
+
* This section used to open "One family, four consumers" and enumerate four.
|
|
26
|
+
* That count was hand-kept, and hand-kept counts of this table are exactly what
|
|
27
|
+
* keeps going wrong: it was already stale by two when objectui#5692 measured it
|
|
28
|
+
* (`paramToField` in `app-shell` had joined with objectui#5312, and
|
|
29
|
+
* `ListView`'s relational-sort rule reads the set directly), and objectui#5312's
|
|
30
|
+
* claim to have converted "the LAST private copy" was false by two MORE — see
|
|
31
|
+
* the falsification note at the end. Read the list below as the lineage of the
|
|
32
|
+
* conversions, not as a census; the mechanical fact is the identity pins.
|
|
33
|
+
*
|
|
34
|
+
* This is the reference-bearing FAMILY, not the `$expand` builder's private
|
|
35
|
+
* list, and these concerns read it under several different words:
|
|
36
|
+
*
|
|
37
|
+
* - `$expand` construction — `buildExpandFields` below ("expandable");
|
|
38
|
+
* - predicate-record projection — `predicate-record.ts` ("relational");
|
|
39
|
+
* - the object form's data-source wiring — `needsDataSourceWiring` in
|
|
40
|
+
* `packages/components/src/renderers/form/form.tsx`, which decides which
|
|
41
|
+
* registered widget gets `dataSource` / `dependentValues` /
|
|
42
|
+
* `dependsOnLabels` threaded to it;
|
|
43
|
+
* - the grid's bulk-action dialog — `widgetNeedsDataSource` in
|
|
44
|
+
* `packages/plugin-grid/src/components/bulkParamToField.ts`, which decides
|
|
45
|
+
* which param widget is handed the grid's `DataSource` and which param field
|
|
46
|
+
* shape carries `reference_to` / `display_field`;
|
|
47
|
+
* - the action-param dialog — `paramToField` in
|
|
48
|
+
* `packages/app-shell/src/utils/paramToField.ts`, which decides which param
|
|
49
|
+
* carries a reference target (objectui#5312);
|
|
50
|
+
* - the list view's relational-sort rule — `ListView.tsx`, which will not offer
|
|
51
|
+
* a server-side sort on a field whose stored value is a foreign key;
|
|
52
|
+
* - the dashboard table's `$expand` whitelist — `computeLookupExpand` in
|
|
53
|
+
* `packages/plugin-dashboard/src/ObjectDataTable.tsx` (objectui#5692);
|
|
54
|
+
* - the dashboard's relation/link test — `isLookupType` in
|
|
55
|
+
* `packages/plugin-dashboard/src/recordFields.tsx` (objectui#5692).
|
|
56
|
+
*
|
|
57
|
+
* The third one used to be a second hand-maintained copy, and this comment used
|
|
58
|
+
* to claim the set "mirrors the form layer's `DATA_SOURCE_FIELD_TYPES`
|
|
59
|
+
* (`lookup` / `master_detail` / `tree`)". That was true for 15 days. The form's
|
|
60
|
+
* copy then gained `capability-multiselect` (objectui#2403) and, the same day,
|
|
61
|
+
* `object-ref` / `filter-condition` / `recipient-picker` (objectui#2421), after
|
|
62
|
+
* which the two sets were not in a subset relation in EITHER direction — `user`
|
|
63
|
+
* only here, three picker names only there — and no gate could say so
|
|
64
|
+
* (objectui#4790). The form now DERIVES from this set instead of restating it:
|
|
65
|
+
*
|
|
66
|
+
* form's data-source rule == EXPANDABLE_FIELD_TYPES
|
|
67
|
+
* + three widget-hint-only picker names
|
|
68
|
+
*
|
|
69
|
+
* The extension is one-directional and cannot fold back into this set:
|
|
70
|
+
* `object-ref` / `filter-condition` / `recipient-picker` are widget HINTS, never
|
|
71
|
+
* declarable field types (`widgetHintOnly: true` in
|
|
72
|
+
* `app-shell/src/utils/paramValueShape.ts`), so no object schema can produce a
|
|
73
|
+
* field whose `type` is one of them.
|
|
74
|
+
*
|
|
75
|
+
* The fourth was a hand-maintained copy too, one member set removed from BOTH
|
|
76
|
+
* of the above (`lookup` / `master_detail` / `user` — no `tree`, no pickers, and
|
|
77
|
+
* for a while a fifth spelling, `owner`, that objectui#4814 retired). It now
|
|
78
|
+
* derives from this set with NO extension, because the bulk dialog's params
|
|
79
|
+
* cannot produce the three widget-hint names (objectui#4815):
|
|
80
|
+
*
|
|
81
|
+
* bulk dialog's data-source rule == EXPANDABLE_FIELD_TYPES
|
|
82
|
+
*
|
|
83
|
+
* Neither consumer copies the set — both call `.has()` on THIS object, and both
|
|
84
|
+
* carry an identity pin (a spy on this `has`) so a member-identical private copy
|
|
85
|
+
* fails rather than quietly re-forking the table.
|
|
86
|
+
*
|
|
87
|
+
* ## The "LAST private copy" claim was false — objectui#5692
|
|
88
|
+
*
|
|
89
|
+
* objectui#5312 converted `paramToField` (`app-shell`) and recorded it as the
|
|
90
|
+
* fourth and LAST private copy of this rule. `packages/plugin-dashboard` held
|
|
91
|
+
* two more the whole time — `LOOKUP_TYPES` in `recordFields.tsx` and an inline
|
|
92
|
+
* disjunction inside `computeLookupExpand` in `ObjectDataTable.tsx` — which
|
|
93
|
+
* predate that sweep and were outside its file surface, so nothing contradicted
|
|
94
|
+
* the claim. Both now derive from this set with NO extension and carry the same
|
|
95
|
+
* identity pin.
|
|
96
|
+
*
|
|
97
|
+
* Those two were NOT member-identical to this set, in either direction: they
|
|
98
|
+
* lacked `tree` and carried a fifth spelling, `reference`. Converging them was
|
|
99
|
+
* therefore a behaviour change in two directions, and the direction that could
|
|
100
|
+
* have widened THIS set was settled by measurement rather than by preference:
|
|
101
|
+
* `reference` is absent from `@objectstack/spec`'s closed `FieldType`
|
|
102
|
+
* vocabulary and is refused by `FieldSchema.safeParse` — measured with `lookup`
|
|
103
|
+
* / `master_detail` / `user` / `tree` as live controls and the retired `owner`
|
|
104
|
+
* plus a nonsense spelling as dead ones — so no spec-compliant object schema can
|
|
105
|
+
* declare a field whose stored type is `reference`, and the dashboard's copies
|
|
106
|
+
* were carrying a dead spelling, not a member this set was missing. This set is
|
|
107
|
+
* unchanged by objectui#5692; the two dashboard faces simply stopped answering
|
|
108
|
+
* for a type no producer can emit.
|
|
109
|
+
*
|
|
110
|
+
* Stated so it reads as a decision rather than a surprise: **adding a member
|
|
111
|
+
* here also grants that type the form's data-source wiring.** That is the
|
|
112
|
+
* intended coupling — a type whose stored value is a foreign key needs a
|
|
113
|
+
* `DataSource` to resolve that key on the read path (expand) and on the edit
|
|
114
|
+
* path (the picker) alike.
|
|
25
115
|
*/
|
|
26
116
|
export declare const EXPANDABLE_FIELD_TYPES: ReadonlySet<string>;
|
|
27
117
|
/**
|
|
@@ -12,17 +12,107 @@ import { columnIdentity } from './column-identity.js';
|
|
|
12
12
|
* list / grid / detail cell can render the related record's display name
|
|
13
13
|
* instead of a bare id placeholder ("—").
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* lookup
|
|
18
|
-
*
|
|
19
|
-
* `reference` + id storage), so a `user` column that is NOT requested for
|
|
20
|
-
* expansion comes back as a raw user id and renders as "—". Keeping the set
|
|
21
|
-
* here as the single source of truth is what closes that gap.
|
|
15
|
+
* `user` is a lookup specialised to `sys_user`: it carries the same `reference`
|
|
16
|
+
* + id storage and the server resolves it through the same expand path as
|
|
17
|
+
* `lookup` / `master_detail`, so a `user` column that is NOT requested for
|
|
18
|
+
* expansion comes back as a raw user id and renders as "—" (objectui#2032).
|
|
22
19
|
*
|
|
23
20
|
* Note on `tree`: a self-referencing hierarchy field is a reference too, so it
|
|
24
21
|
* belongs in this set; whether the backend materialises the expanded object
|
|
25
22
|
* for it is a server concern — requesting it is harmless and forward-compatible.
|
|
23
|
+
*
|
|
24
|
+
* ## One family, many consumers — and NO reliable count of them
|
|
25
|
+
*
|
|
26
|
+
* This section used to open "One family, four consumers" and enumerate four.
|
|
27
|
+
* That count was hand-kept, and hand-kept counts of this table are exactly what
|
|
28
|
+
* keeps going wrong: it was already stale by two when objectui#5692 measured it
|
|
29
|
+
* (`paramToField` in `app-shell` had joined with objectui#5312, and
|
|
30
|
+
* `ListView`'s relational-sort rule reads the set directly), and objectui#5312's
|
|
31
|
+
* claim to have converted "the LAST private copy" was false by two MORE — see
|
|
32
|
+
* the falsification note at the end. Read the list below as the lineage of the
|
|
33
|
+
* conversions, not as a census; the mechanical fact is the identity pins.
|
|
34
|
+
*
|
|
35
|
+
* This is the reference-bearing FAMILY, not the `$expand` builder's private
|
|
36
|
+
* list, and these concerns read it under several different words:
|
|
37
|
+
*
|
|
38
|
+
* - `$expand` construction — `buildExpandFields` below ("expandable");
|
|
39
|
+
* - predicate-record projection — `predicate-record.ts` ("relational");
|
|
40
|
+
* - the object form's data-source wiring — `needsDataSourceWiring` in
|
|
41
|
+
* `packages/components/src/renderers/form/form.tsx`, which decides which
|
|
42
|
+
* registered widget gets `dataSource` / `dependentValues` /
|
|
43
|
+
* `dependsOnLabels` threaded to it;
|
|
44
|
+
* - the grid's bulk-action dialog — `widgetNeedsDataSource` in
|
|
45
|
+
* `packages/plugin-grid/src/components/bulkParamToField.ts`, which decides
|
|
46
|
+
* which param widget is handed the grid's `DataSource` and which param field
|
|
47
|
+
* shape carries `reference_to` / `display_field`;
|
|
48
|
+
* - the action-param dialog — `paramToField` in
|
|
49
|
+
* `packages/app-shell/src/utils/paramToField.ts`, which decides which param
|
|
50
|
+
* carries a reference target (objectui#5312);
|
|
51
|
+
* - the list view's relational-sort rule — `ListView.tsx`, which will not offer
|
|
52
|
+
* a server-side sort on a field whose stored value is a foreign key;
|
|
53
|
+
* - the dashboard table's `$expand` whitelist — `computeLookupExpand` in
|
|
54
|
+
* `packages/plugin-dashboard/src/ObjectDataTable.tsx` (objectui#5692);
|
|
55
|
+
* - the dashboard's relation/link test — `isLookupType` in
|
|
56
|
+
* `packages/plugin-dashboard/src/recordFields.tsx` (objectui#5692).
|
|
57
|
+
*
|
|
58
|
+
* The third one used to be a second hand-maintained copy, and this comment used
|
|
59
|
+
* to claim the set "mirrors the form layer's `DATA_SOURCE_FIELD_TYPES`
|
|
60
|
+
* (`lookup` / `master_detail` / `tree`)". That was true for 15 days. The form's
|
|
61
|
+
* copy then gained `capability-multiselect` (objectui#2403) and, the same day,
|
|
62
|
+
* `object-ref` / `filter-condition` / `recipient-picker` (objectui#2421), after
|
|
63
|
+
* which the two sets were not in a subset relation in EITHER direction — `user`
|
|
64
|
+
* only here, three picker names only there — and no gate could say so
|
|
65
|
+
* (objectui#4790). The form now DERIVES from this set instead of restating it:
|
|
66
|
+
*
|
|
67
|
+
* form's data-source rule == EXPANDABLE_FIELD_TYPES
|
|
68
|
+
* + three widget-hint-only picker names
|
|
69
|
+
*
|
|
70
|
+
* The extension is one-directional and cannot fold back into this set:
|
|
71
|
+
* `object-ref` / `filter-condition` / `recipient-picker` are widget HINTS, never
|
|
72
|
+
* declarable field types (`widgetHintOnly: true` in
|
|
73
|
+
* `app-shell/src/utils/paramValueShape.ts`), so no object schema can produce a
|
|
74
|
+
* field whose `type` is one of them.
|
|
75
|
+
*
|
|
76
|
+
* The fourth was a hand-maintained copy too, one member set removed from BOTH
|
|
77
|
+
* of the above (`lookup` / `master_detail` / `user` — no `tree`, no pickers, and
|
|
78
|
+
* for a while a fifth spelling, `owner`, that objectui#4814 retired). It now
|
|
79
|
+
* derives from this set with NO extension, because the bulk dialog's params
|
|
80
|
+
* cannot produce the three widget-hint names (objectui#4815):
|
|
81
|
+
*
|
|
82
|
+
* bulk dialog's data-source rule == EXPANDABLE_FIELD_TYPES
|
|
83
|
+
*
|
|
84
|
+
* Neither consumer copies the set — both call `.has()` on THIS object, and both
|
|
85
|
+
* carry an identity pin (a spy on this `has`) so a member-identical private copy
|
|
86
|
+
* fails rather than quietly re-forking the table.
|
|
87
|
+
*
|
|
88
|
+
* ## The "LAST private copy" claim was false — objectui#5692
|
|
89
|
+
*
|
|
90
|
+
* objectui#5312 converted `paramToField` (`app-shell`) and recorded it as the
|
|
91
|
+
* fourth and LAST private copy of this rule. `packages/plugin-dashboard` held
|
|
92
|
+
* two more the whole time — `LOOKUP_TYPES` in `recordFields.tsx` and an inline
|
|
93
|
+
* disjunction inside `computeLookupExpand` in `ObjectDataTable.tsx` — which
|
|
94
|
+
* predate that sweep and were outside its file surface, so nothing contradicted
|
|
95
|
+
* the claim. Both now derive from this set with NO extension and carry the same
|
|
96
|
+
* identity pin.
|
|
97
|
+
*
|
|
98
|
+
* Those two were NOT member-identical to this set, in either direction: they
|
|
99
|
+
* lacked `tree` and carried a fifth spelling, `reference`. Converging them was
|
|
100
|
+
* therefore a behaviour change in two directions, and the direction that could
|
|
101
|
+
* have widened THIS set was settled by measurement rather than by preference:
|
|
102
|
+
* `reference` is absent from `@objectstack/spec`'s closed `FieldType`
|
|
103
|
+
* vocabulary and is refused by `FieldSchema.safeParse` — measured with `lookup`
|
|
104
|
+
* / `master_detail` / `user` / `tree` as live controls and the retired `owner`
|
|
105
|
+
* plus a nonsense spelling as dead ones — so no spec-compliant object schema can
|
|
106
|
+
* declare a field whose stored type is `reference`, and the dashboard's copies
|
|
107
|
+
* were carrying a dead spelling, not a member this set was missing. This set is
|
|
108
|
+
* unchanged by objectui#5692; the two dashboard faces simply stopped answering
|
|
109
|
+
* for a type no producer can emit.
|
|
110
|
+
*
|
|
111
|
+
* Stated so it reads as a decision rather than a surprise: **adding a member
|
|
112
|
+
* here also grants that type the form's data-source wiring.** That is the
|
|
113
|
+
* intended coupling — a type whose stored value is a foreign key needs a
|
|
114
|
+
* `DataSource` to resolve that key on the read path (expand) and on the edit
|
|
115
|
+
* path (the picker) alike.
|
|
26
116
|
*/
|
|
27
117
|
export const EXPANDABLE_FIELD_TYPES = new Set([
|
|
28
118
|
'lookup',
|
|
@@ -36,6 +36,33 @@ export declare const SYSTEM_VIEW_MARKER: unique symbol;
|
|
|
36
36
|
export type DeepReadonly<T> = T extends (...args: any[]) => any ? T : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepReadonly<U>> : T extends object ? {
|
|
37
37
|
readonly [K in keyof T]: DeepReadonly<T[K]>;
|
|
38
38
|
} : T;
|
|
39
|
+
/**
|
|
40
|
+
* The inverse of `DeepReadonly<T>`: recursively strip `readonly` at every depth.
|
|
41
|
+
*
|
|
42
|
+
* Named `DeepMutable` rather than `Mutable` for the same reason the type above
|
|
43
|
+
* is not called `Readonly` — TS' built-in `Readonly<T>` is SHALLOW, so a bare
|
|
44
|
+
* `Mutable` would understate what this does by exactly the depth gap that made
|
|
45
|
+
* `cloneAsOverride` wrong in the first place. A type whose name misreports its
|
|
46
|
+
* own reach is the defect this pair exists to close, not a naming preference.
|
|
47
|
+
*
|
|
48
|
+
* Symmetric with `DeepReadonly` arm for arm, deliberately including its two
|
|
49
|
+
* limits: a tuple widens to an array (`DeepReadonly` widens it the same way, so
|
|
50
|
+
* an inverse that preserved tuples would not be an inverse), and `Date`,
|
|
51
|
+
* `RegExp`, `Map` and `Set` are mapped as plain objects even though
|
|
52
|
+
* `isFreezableObject` skips them at runtime.
|
|
53
|
+
*
|
|
54
|
+
* One thing it deliberately does NOT do: drop the `SYSTEM_VIEW_MARKER` key.
|
|
55
|
+
* `cloneAsOverride` never copies the symbol, so a clone's marker is always
|
|
56
|
+
* absent — but the key is declared optional (`?: true`), and carrying it
|
|
57
|
+
* therefore states "may be absent", which is true. Excluding it needs
|
|
58
|
+
* `Exclude<keyof T, typeof SYSTEM_VIEW_MARKER>`, a NON-homomorphic mapped type
|
|
59
|
+
* that drops the `?` modifier from every other property and turns optional keys
|
|
60
|
+
* required — a strictly worse type, and a real break for callers, traded for
|
|
61
|
+
* removing a key that already reads as optional.
|
|
62
|
+
*/
|
|
63
|
+
export type DeepMutable<T> = T extends (...args: any[]) => any ? T : T extends ReadonlyArray<infer U> ? DeepMutable<U>[] : T extends object ? {
|
|
64
|
+
-readonly [K in keyof T]: DeepMutable<T[K]>;
|
|
65
|
+
} : T;
|
|
39
66
|
/**
|
|
40
67
|
* A schema that has been frozen by `defineSystemView()`. The marker symbol is
|
|
41
68
|
* non-enumerable and therefore invisible to consumers, but its presence
|
|
@@ -87,8 +114,18 @@ export declare function isSystemView(value: unknown): boolean;
|
|
|
87
114
|
* Produce a deep, mutable clone of a System View so callers can apply
|
|
88
115
|
* Tenant/User overrides without touching the source schema.
|
|
89
116
|
*
|
|
117
|
+
* Returns `DeepMutable<T>`, not `T`. The declaration used to hand back the
|
|
118
|
+
* input type unchanged, which meant cloning a `SystemView<S>` returned
|
|
119
|
+
* something still typed deep-readonly even though the implementation had always
|
|
120
|
+
* produced a plain mutable object — so the override flow this function exists
|
|
121
|
+
* for, `draft.columns.push(...)`, did not type-check (TS2339). The return type
|
|
122
|
+
* now relaxes TOWARD what the runtime already does, never away from it, which
|
|
123
|
+
* is why the change adds capability to a caller's type rather than removing it.
|
|
124
|
+
*
|
|
90
125
|
* Implementation note: uses `structuredClone` when available (Node 17+, all
|
|
91
126
|
* evergreen browsers) and falls back to a JSON round-trip. The marker
|
|
92
127
|
* symbol is intentionally NOT copied — the clone is no longer a System View.
|
|
128
|
+
* Neither path can copy it by accident: `structuredClone` ignores symbol keys,
|
|
129
|
+
* and `JSON.stringify` never sees them.
|
|
93
130
|
*/
|
|
94
|
-
export declare function cloneAsOverride<T>(view: T): T
|
|
131
|
+
export declare function cloneAsOverride<T>(view: T): DeepMutable<T>;
|
|
@@ -140,9 +140,19 @@ export function isSystemView(value) {
|
|
|
140
140
|
* Produce a deep, mutable clone of a System View so callers can apply
|
|
141
141
|
* Tenant/User overrides without touching the source schema.
|
|
142
142
|
*
|
|
143
|
+
* Returns `DeepMutable<T>`, not `T`. The declaration used to hand back the
|
|
144
|
+
* input type unchanged, which meant cloning a `SystemView<S>` returned
|
|
145
|
+
* something still typed deep-readonly even though the implementation had always
|
|
146
|
+
* produced a plain mutable object — so the override flow this function exists
|
|
147
|
+
* for, `draft.columns.push(...)`, did not type-check (TS2339). The return type
|
|
148
|
+
* now relaxes TOWARD what the runtime already does, never away from it, which
|
|
149
|
+
* is why the change adds capability to a caller's type rather than removing it.
|
|
150
|
+
*
|
|
143
151
|
* Implementation note: uses `structuredClone` when available (Node 17+, all
|
|
144
152
|
* evergreen browsers) and falls back to a JSON round-trip. The marker
|
|
145
153
|
* symbol is intentionally NOT copied — the clone is no longer a System View.
|
|
154
|
+
* Neither path can copy it by accident: `structuredClone` ignores symbol keys,
|
|
155
|
+
* and `JSON.stringify` never sees them.
|
|
146
156
|
*/
|
|
147
157
|
export function cloneAsOverride(view) {
|
|
148
158
|
if (view == null || typeof view !== 'object')
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — value-fallback label prettifier
|
|
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
|
+
* humanize-label — the ONE home for the VALUE fallback: a stored value becomes
|
|
10
|
+
* a display string when nothing else resolves it (objectui#5444).
|
|
11
|
+
*
|
|
12
|
+
* Two byte-identical copies preceded this module: `@object-ui/fields` (read by
|
|
13
|
+
* `plugin-grid`, `plugin-gantt`, `plugin-detail`, and by the field renderers in
|
|
14
|
+
* that package itself) and a deliberate local copy in `plugin-charts`'
|
|
15
|
+
* `ObjectChart.tsx`, whose comment said it existed "to avoid a dependency on
|
|
16
|
+
* `@object-ui/fields`". `@object-ui/core` is the shared ancestor both packages
|
|
17
|
+
* already depend on, so the convention gets a single home without a new
|
|
18
|
+
* dependency edge and without React reaching core — objectui#4389's ruling for
|
|
19
|
+
* this family: core-canonical logic, plugins consume; never core importing
|
|
20
|
+
* react. Both former sites now re-export THIS function, so there is one
|
|
21
|
+
* implementation and two doorways.
|
|
22
|
+
*
|
|
23
|
+
* ## Why this is NOT `humanizeFieldKey`, and why the two are kept apart
|
|
24
|
+
*
|
|
25
|
+
* The renderer carries a second prettifier — `humanizeFieldKey` in
|
|
26
|
+
* `@object-ui/plugin-dashboard`, the single home for the KEY fallback (a field
|
|
27
|
+
* key becomes a column header). It is a DIFFERENT convention, not a stale copy
|
|
28
|
+
* of this one: it additionally splits camelCase.
|
|
29
|
+
*
|
|
30
|
+
* input humanizeFieldKey humanizeLabel (here)
|
|
31
|
+
* ────────────────── ────────────────── ──────────────────
|
|
32
|
+
* needs_analysis Needs Analysis Needs Analysis
|
|
33
|
+
* NeedsAnalysis Needs Analysis NeedsAnalysis <- differ
|
|
34
|
+
* unitPrice Unit Price UnitPrice <- differ
|
|
35
|
+
* BestCase Best Case BestCase <- differ
|
|
36
|
+
* lost-to-competitor Lost-To-Competitor Lost To Competitor <- differ
|
|
37
|
+
*
|
|
38
|
+
* They are fed different inputs, which is why their outputs may differ. A field
|
|
39
|
+
* KEY is authored in the codebase and is expected to carry a machine spelling,
|
|
40
|
+
* so splitting camelCase recovers words its author meant. A stored VALUE is
|
|
41
|
+
* arbitrary tenant data — a picklist member, an object name, a code from
|
|
42
|
+
* another system — where a mid-token capital is not reliably a word boundary,
|
|
43
|
+
* and splitting it rewrites what the tenant wrote (`McDonald` -> `Mc Donald`).
|
|
44
|
+
* Note also that the two conventions do not nest: on the last row above each
|
|
45
|
+
* leaves alone the separator the other rewrites, so "converge" would have to
|
|
46
|
+
* pick a winner per input class rather than adopt one side wholesale.
|
|
47
|
+
*
|
|
48
|
+
* Kept distinct DELIBERATELY, and this paragraph is the write-down whose
|
|
49
|
+
* absence made the split read as drift. Converging them is a decision, not a
|
|
50
|
+
* refactor: this function is read by grid, gantt, detail and charts, so any
|
|
51
|
+
* change to what it RETURNS moves rendered output in four packages at once.
|
|
52
|
+
* That change is explicitly out of scope for the convergence that created this
|
|
53
|
+
* module; it needs its own card.
|
|
54
|
+
*
|
|
55
|
+
* Examples:
|
|
56
|
+
* "in_progress" -> "In Progress"
|
|
57
|
+
* "high-priority" -> "High Priority"
|
|
58
|
+
* "active" -> "Active"
|
|
59
|
+
*/
|
|
60
|
+
export declare function humanizeLabel(value: string): string;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — value-fallback label prettifier
|
|
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
|
+
* humanize-label — the ONE home for the VALUE fallback: a stored value becomes
|
|
10
|
+
* a display string when nothing else resolves it (objectui#5444).
|
|
11
|
+
*
|
|
12
|
+
* Two byte-identical copies preceded this module: `@object-ui/fields` (read by
|
|
13
|
+
* `plugin-grid`, `plugin-gantt`, `plugin-detail`, and by the field renderers in
|
|
14
|
+
* that package itself) and a deliberate local copy in `plugin-charts`'
|
|
15
|
+
* `ObjectChart.tsx`, whose comment said it existed "to avoid a dependency on
|
|
16
|
+
* `@object-ui/fields`". `@object-ui/core` is the shared ancestor both packages
|
|
17
|
+
* already depend on, so the convention gets a single home without a new
|
|
18
|
+
* dependency edge and without React reaching core — objectui#4389's ruling for
|
|
19
|
+
* this family: core-canonical logic, plugins consume; never core importing
|
|
20
|
+
* react. Both former sites now re-export THIS function, so there is one
|
|
21
|
+
* implementation and two doorways.
|
|
22
|
+
*
|
|
23
|
+
* ## Why this is NOT `humanizeFieldKey`, and why the two are kept apart
|
|
24
|
+
*
|
|
25
|
+
* The renderer carries a second prettifier — `humanizeFieldKey` in
|
|
26
|
+
* `@object-ui/plugin-dashboard`, the single home for the KEY fallback (a field
|
|
27
|
+
* key becomes a column header). It is a DIFFERENT convention, not a stale copy
|
|
28
|
+
* of this one: it additionally splits camelCase.
|
|
29
|
+
*
|
|
30
|
+
* input humanizeFieldKey humanizeLabel (here)
|
|
31
|
+
* ────────────────── ────────────────── ──────────────────
|
|
32
|
+
* needs_analysis Needs Analysis Needs Analysis
|
|
33
|
+
* NeedsAnalysis Needs Analysis NeedsAnalysis <- differ
|
|
34
|
+
* unitPrice Unit Price UnitPrice <- differ
|
|
35
|
+
* BestCase Best Case BestCase <- differ
|
|
36
|
+
* lost-to-competitor Lost-To-Competitor Lost To Competitor <- differ
|
|
37
|
+
*
|
|
38
|
+
* They are fed different inputs, which is why their outputs may differ. A field
|
|
39
|
+
* KEY is authored in the codebase and is expected to carry a machine spelling,
|
|
40
|
+
* so splitting camelCase recovers words its author meant. A stored VALUE is
|
|
41
|
+
* arbitrary tenant data — a picklist member, an object name, a code from
|
|
42
|
+
* another system — where a mid-token capital is not reliably a word boundary,
|
|
43
|
+
* and splitting it rewrites what the tenant wrote (`McDonald` -> `Mc Donald`).
|
|
44
|
+
* Note also that the two conventions do not nest: on the last row above each
|
|
45
|
+
* leaves alone the separator the other rewrites, so "converge" would have to
|
|
46
|
+
* pick a winner per input class rather than adopt one side wholesale.
|
|
47
|
+
*
|
|
48
|
+
* Kept distinct DELIBERATELY, and this paragraph is the write-down whose
|
|
49
|
+
* absence made the split read as drift. Converging them is a decision, not a
|
|
50
|
+
* refactor: this function is read by grid, gantt, detail and charts, so any
|
|
51
|
+
* change to what it RETURNS moves rendered output in four packages at once.
|
|
52
|
+
* That change is explicitly out of scope for the convergence that created this
|
|
53
|
+
* module; it needs its own card.
|
|
54
|
+
*
|
|
55
|
+
* Examples:
|
|
56
|
+
* "in_progress" -> "In Progress"
|
|
57
|
+
* "high-priority" -> "High Priority"
|
|
58
|
+
* "active" -> "Active"
|
|
59
|
+
*/
|
|
60
|
+
export function humanizeLabel(value) {
|
|
61
|
+
return value.replace(/[_-]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
62
|
+
}
|
|
@@ -51,8 +51,33 @@ export type UserActionOverride = boolean | {
|
|
|
51
51
|
disabledWhen?: RowCrudPredicates['disabledWhen'];
|
|
52
52
|
};
|
|
53
53
|
export interface UserActionsOverride {
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Bare boolean or the object form. Widened from `boolean` in objectui#4646:
|
|
56
|
+
* `@objectstack/spec@17.0.0` types this key
|
|
57
|
+
* `z.union([z.boolean(), RowCrudActionOverrideSchema])`, and the spec's
|
|
58
|
+
* resolver emits the parsed predicates as `CrudAffordances.createPredicates`.
|
|
59
|
+
* The local type said `boolean` — so an author writing the shape the spec
|
|
60
|
+
* accepts, and that the related-list toolbar now honours
|
|
61
|
+
* (`RelatedRecordActionsBridge`, objectui#4646), was rejected by objectui's
|
|
62
|
+
* own type while the runtime handled it.
|
|
63
|
+
*/
|
|
64
|
+
create?: UserActionOverride;
|
|
65
|
+
/**
|
|
66
|
+
* Bare boolean or the object form — the SAME union as `create` above, which
|
|
67
|
+
* is how `@objectstack/spec@17.0.0` types the two toolbar keys
|
|
68
|
+
* (`z.union([z.boolean(), RowCrudActionOverrideSchema])` for each), and its
|
|
69
|
+
* resolver emits `CrudAffordances.importPredicates` beside
|
|
70
|
+
* `createPredicates`.
|
|
71
|
+
*
|
|
72
|
+
* Widened in objectui#5142, deliberately IN THE SAME change that gave the
|
|
73
|
+
* object-list toolbar's Import button a consumer for those predicates
|
|
74
|
+
* (`ObjectView`). objectui#4646 left this key narrow on purpose and said why:
|
|
75
|
+
* widening a type ahead of its consumer re-declares the inert-metadata defect
|
|
76
|
+
* — objectui would have *claimed* to accept the object form while still
|
|
77
|
+
* dropping the predicate. Type and consumer travel together, so the
|
|
78
|
+
* declaration stays an honest statement of what this renderer honours.
|
|
79
|
+
*/
|
|
80
|
+
import?: UserActionOverride;
|
|
56
81
|
edit?: UserActionOverride;
|
|
57
82
|
delete?: UserActionOverride;
|
|
58
83
|
exportCsv?: boolean;
|
|
@@ -125,5 +125,15 @@ export declare function rowHeightToDensityMode(rowHeight: unknown): DensityMode
|
|
|
125
125
|
* breaking change with no inventory. Runs AFTER the `fields` → `columns` fold
|
|
126
126
|
* above, so a view that spells its column list the legacy way still gets its
|
|
127
127
|
* entries canonicalized.
|
|
128
|
+
* - the four PER-VIEW-TYPE config aliases phase 3 carried over
|
|
129
|
+
* ({@link PER_VIEW_CONFIG_ALIASES}): `kanban.groupField` → `groupByField`,
|
|
130
|
+
* `kanban.cardFields` → `columns`, `gallery.imageField` → `coverField`,
|
|
131
|
+
* `timeline.dateField` → `startDateField`. One read-site changes behaviour
|
|
132
|
+
* as a result, and it is a CORRECTION of the same inverted precedence A2
|
|
133
|
+
* fixed for `densityMode`: `ListView`'s kanban adapter resolves
|
|
134
|
+
* `cardFields || columns`, i.e. legacy over canonical, so a config carrying
|
|
135
|
+
* BOTH used to render the legacy value. After the fold the canonical
|
|
136
|
+
* `columns` reaches it. Every other reader of these four was already
|
|
137
|
+
* canonical-first and is unaffected.
|
|
128
138
|
*/
|
|
129
139
|
export declare function normalizeListViewSchema<T>(schema: T): T;
|
|
@@ -101,6 +101,32 @@ const ARIA_KEY_ALIASES = {
|
|
|
101
101
|
label: 'ariaLabel',
|
|
102
102
|
describedBy: 'ariaDescribedBy',
|
|
103
103
|
};
|
|
104
|
+
/**
|
|
105
|
+
* Per-view-type config aliases → the spec key each one aliases (#2890, the
|
|
106
|
+
* phase-3 carry-over).
|
|
107
|
+
*
|
|
108
|
+
* Phase 3 derived `kanban` / `gallery` / `timeline` from the spec configs but
|
|
109
|
+
* kept the pre-#2231 objectui spellings declared alongside the spec keys so
|
|
110
|
+
* stored view metadata would keep validating
|
|
111
|
+
* (`packages/types/src/zod/objectql.zod.ts`, each marked `@deprecated legacy
|
|
112
|
+
* alias for the spec's X`). Folding them here is what lets a read-site stop
|
|
113
|
+
* carrying its own `canonical || legacy` pair — the same move A1–A5 made for
|
|
114
|
+
* the top-level vocabulary.
|
|
115
|
+
*
|
|
116
|
+
* `calendar` is deliberately absent: its one local key, `defaultView`, is not
|
|
117
|
+
* an alias of anything. It has no spec counterpart at all, so it wants
|
|
118
|
+
* PROMOTION upstream, not a rename — folding it would delete an authored value
|
|
119
|
+
* with nowhere to put it.
|
|
120
|
+
*
|
|
121
|
+
* Note `kanban.columns` is the spec's "fields shown on each card", NOT the
|
|
122
|
+
* table columns that the same word names at the ListView top level. The fold
|
|
123
|
+
* is scoped to the nested config, so the two never meet.
|
|
124
|
+
*/
|
|
125
|
+
const PER_VIEW_CONFIG_ALIASES = {
|
|
126
|
+
kanban: { groupField: 'groupByField', cardFields: 'columns' },
|
|
127
|
+
gallery: { imageField: 'coverField' },
|
|
128
|
+
timeline: { dateField: 'startDateField' },
|
|
129
|
+
};
|
|
104
130
|
/**
|
|
105
131
|
* ObjectUI's `list-view` node historically used a different vocabulary from
|
|
106
132
|
* `@objectstack/spec` for the same concepts (`fields` where the spec says
|
|
@@ -164,6 +190,16 @@ const ARIA_KEY_ALIASES = {
|
|
|
164
190
|
* breaking change with no inventory. Runs AFTER the `fields` → `columns` fold
|
|
165
191
|
* above, so a view that spells its column list the legacy way still gets its
|
|
166
192
|
* entries canonicalized.
|
|
193
|
+
* - the four PER-VIEW-TYPE config aliases phase 3 carried over
|
|
194
|
+
* ({@link PER_VIEW_CONFIG_ALIASES}): `kanban.groupField` → `groupByField`,
|
|
195
|
+
* `kanban.cardFields` → `columns`, `gallery.imageField` → `coverField`,
|
|
196
|
+
* `timeline.dateField` → `startDateField`. One read-site changes behaviour
|
|
197
|
+
* as a result, and it is a CORRECTION of the same inverted precedence A2
|
|
198
|
+
* fixed for `densityMode`: `ListView`'s kanban adapter resolves
|
|
199
|
+
* `cardFields || columns`, i.e. legacy over canonical, so a config carrying
|
|
200
|
+
* BOTH used to render the legacy value. After the fold the canonical
|
|
201
|
+
* `columns` reaches it. Every other reader of these four was already
|
|
202
|
+
* canonical-first and is unaffected.
|
|
167
203
|
*/
|
|
168
204
|
export function normalizeListViewSchema(schema) {
|
|
169
205
|
if (!schema || typeof schema !== 'object')
|
|
@@ -195,9 +231,22 @@ export function normalizeListViewSchema(schema) {
|
|
|
195
231
|
: undefined;
|
|
196
232
|
const foldedColumns = normalizeColumnIdentities(columnsSource);
|
|
197
233
|
const foldColumnIdentity = foldedColumns !== columnsSource;
|
|
234
|
+
// Per-view-type config aliases (#2890 phase-3 carry-over). Collected before
|
|
235
|
+
// the early return so an otherwise-canonical view carrying only a nested
|
|
236
|
+
// legacy key still folds — and so a view carrying none still returns by
|
|
237
|
+
// reference.
|
|
238
|
+
const perViewFolds = Object.entries(PER_VIEW_CONFIG_ALIASES)
|
|
239
|
+
.map(([viewKey, aliases]) => {
|
|
240
|
+
const cfg = isRecord(s[viewKey]) ? s[viewKey] : undefined;
|
|
241
|
+
if (!cfg)
|
|
242
|
+
return undefined;
|
|
243
|
+
const legacyKeys = Object.keys(aliases).filter((legacy) => cfg[legacy] !== undefined);
|
|
244
|
+
return legacyKeys.length ? { viewKey, aliases, cfg, legacyKeys } : undefined;
|
|
245
|
+
})
|
|
246
|
+
.filter((entry) => entry !== undefined);
|
|
198
247
|
if (!foldColumns && !foldRowHeight && !foldFilter && !legacyFlags.length &&
|
|
199
248
|
!foldDescription && !foldAria && !foldSharing && !defaultViewKind &&
|
|
200
|
-
!foldColumnIdentity) {
|
|
249
|
+
!foldColumnIdentity && !perViewFolds.length) {
|
|
201
250
|
return schema;
|
|
202
251
|
}
|
|
203
252
|
const next = { ...s };
|
|
@@ -271,6 +320,19 @@ export function normalizeListViewSchema(schema) {
|
|
|
271
320
|
delete nextSharing.enabled;
|
|
272
321
|
next.sharing = nextSharing;
|
|
273
322
|
}
|
|
323
|
+
for (const { viewKey, aliases, cfg, legacyKeys } of perViewFolds) {
|
|
324
|
+
const nextCfg = { ...cfg };
|
|
325
|
+
for (const legacy of legacyKeys) {
|
|
326
|
+
const canonical = aliases[legacy];
|
|
327
|
+
// Same one-directional shape as every fold above: the canonical key wins
|
|
328
|
+
// when both are present, and the legacy key is REMOVED so a missed
|
|
329
|
+
// read-site fails loudly instead of quietly taking the legacy path.
|
|
330
|
+
if (nextCfg[canonical] === undefined)
|
|
331
|
+
nextCfg[canonical] = cfg[legacy];
|
|
332
|
+
delete nextCfg[legacy];
|
|
333
|
+
}
|
|
334
|
+
next[viewKey] = nextCfg;
|
|
335
|
+
}
|
|
274
336
|
if (defaultViewKind)
|
|
275
337
|
next.viewType = 'grid';
|
|
276
338
|
return next;
|
|
@@ -111,9 +111,13 @@ export interface DisplayNumberFormatOptions {
|
|
|
111
111
|
* repo's runner, `80.175` percentage points formatted to 2 decimals renders
|
|
112
112
|
* `80.18%` when formatted directly and `80.17%` after the divide-and-remultiply
|
|
113
113
|
* — 27,581 of 1,200,013 ordinary-magnitude en-US forms move, plus every
|
|
114
|
-
* value at the top of the double range (`MAX_SAFE_INTEGER`, `1e23`).
|
|
115
|
-
*
|
|
116
|
-
*
|
|
114
|
+
* value at the top of the double range (`MAX_SAFE_INTEGER`, `1e23`). That
|
|
115
|
+
* count is objectui#4576's tie-dense grid — 0.005 steps to 2,000, precisions
|
|
116
|
+
* 0/1/2 — measured on `formatDisplayNumber` / `formatMeasure`'s call shape.
|
|
117
|
+
* objectui#4590 re-measured the same route through `formatPercent` and
|
|
118
|
+
* reports 27,577 of 1,200,003: a different form set, not a correction of this
|
|
119
|
+
* one. A caller holding percentage points must therefore NOT reach for
|
|
120
|
+
* `'percent'`; that is exactly the trap this option removes.
|
|
117
121
|
*
|
|
118
122
|
* `'percentPoints'` is implemented with `Intl`'s `style: 'unit'` /
|
|
119
123
|
* `unit: 'percent'`, which was measured to produce a BYTE-IDENTICAL percent
|