@object-ui/core 17.6.0 → 17.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6610 -0
- package/README.md +101 -14
- package/dist/actions/ActionEngine.d.ts +4 -2
- package/dist/actions/ActionEngine.js +13 -8
- package/dist/actions/ActionRunner.d.ts +288 -27
- package/dist/actions/ActionRunner.js +443 -63
- package/dist/actions/TransactionManager.d.ts +7 -2
- package/dist/actions/actionKeys.d.ts +9 -7
- package/dist/actions/actionKeys.js +57 -14
- package/dist/actions/actionResponse.js +2 -2
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/actions/recordDelete.d.ts +118 -0
- package/dist/actions/recordDelete.js +112 -0
- package/dist/adapters/ValueDataSource.d.ts +6 -1
- package/dist/adapters/ValueDataSource.js +1090 -98
- package/dist/adapters/batchTransaction.d.ts +1 -1
- package/dist/adapters/batchTransaction.js +1 -1
- package/dist/builder/schema-builder.d.ts +38 -81
- package/dist/builder/schema-builder.js +33 -152
- package/dist/data-scope/ViewDataProvider.d.ts +1 -1
- package/dist/data-scope/ViewDataProvider.js +21 -1
- package/dist/data-scope/element-data-source.d.ts +261 -7
- package/dist/data-scope/element-data-source.js +342 -52
- package/dist/data-scope/index.d.ts +9 -4
- package/dist/data-scope/index.js +9 -4
- package/dist/evaluator/ExpressionEvaluator.d.ts +86 -0
- package/dist/evaluator/ExpressionEvaluator.js +184 -12
- package/dist/evaluator/FormulaFunctions.d.ts +0 -10
- package/dist/evaluator/FormulaFunctions.js +166 -27
- package/dist/evaluator/declaredPredicate.d.ts +93 -9
- package/dist/evaluator/declaredPredicate.js +117 -16
- package/dist/evaluator/fieldRules.d.ts +88 -4
- package/dist/evaluator/fieldRules.js +203 -31
- package/dist/evaluator/index.d.ts +1 -0
- package/dist/evaluator/index.js +1 -0
- package/dist/evaluator/listConditional.d.ts +103 -34
- package/dist/evaluator/listConditional.js +127 -50
- package/dist/evaluator/optionRules.d.ts +77 -6
- package/dist/evaluator/optionRules.js +87 -10
- package/dist/evaluator/predicateInput.d.ts +15 -4
- package/dist/evaluator/predicateInput.js +35 -2
- package/dist/evaluator/rowPredicateCanon.d.ts +7 -18
- package/dist/evaluator/rowPredicateCanon.js +65 -115
- package/dist/evaluator/subjectPermissions.d.ts +76 -0
- package/dist/evaluator/subjectPermissions.js +44 -0
- package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
- package/dist/evaluator/unevaluablePredicate.js +70 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +80 -2
- package/dist/protocols/index.d.ts +0 -1
- package/dist/protocols/index.js +9 -1
- package/dist/registry/Registry.d.ts +358 -31
- package/dist/registry/Registry.js +263 -11
- package/dist/registry/WidgetRegistry.js +7 -3
- package/dist/registry/html-tier-intrinsics.d.ts +90 -0
- package/dist/registry/html-tier-intrinsics.js +110 -0
- package/dist/registry/public-blocks.d.ts +8 -0
- package/dist/registry/public-blocks.js +57 -0
- package/dist/theme/ThemeEngine.js +1 -1
- package/dist/utils/chart-category-key.d.ts +82 -0
- package/dist/utils/chart-category-key.js +84 -0
- package/dist/utils/chart-measure-key.d.ts +64 -0
- package/dist/utils/chart-measure-key.js +65 -0
- package/dist/utils/chart-presentation.d.ts +95 -112
- package/dist/utils/chart-presentation.js +169 -104
- package/dist/utils/chart-series.d.ts +131 -5
- package/dist/utils/chart-series.js +98 -16
- package/dist/utils/column-sortability.d.ts +157 -0
- package/dist/utils/column-sortability.js +217 -0
- package/dist/utils/dashboard-filters.d.ts +38 -6
- package/dist/utils/dashboard-filters.js +107 -61
- package/dist/utils/dataset-format.d.ts +211 -30
- package/dist/utils/dataset-format.js +389 -46
- package/dist/utils/date-display.d.ts +354 -0
- package/dist/utils/date-display.js +463 -0
- package/dist/utils/date-macros.js +7 -0
- package/dist/utils/drill-down.d.ts +78 -0
- package/dist/utils/drill-down.js +93 -0
- package/dist/utils/emptiness.d.ts +96 -0
- package/dist/utils/emptiness.js +101 -0
- package/dist/utils/expand-fields.d.ts +76 -6
- package/dist/utils/expand-fields.js +76 -6
- package/dist/utils/extract-records.d.ts +65 -5
- package/dist/utils/extract-records.js +65 -8
- package/dist/utils/filter-converter.d.ts +152 -4
- package/dist/utils/filter-converter.js +1092 -22
- package/dist/utils/filter-tokens.d.ts +49 -5
- package/dist/utils/filter-tokens.js +105 -33
- package/dist/utils/grouping-fields.d.ts +61 -0
- package/dist/utils/grouping-fields.js +84 -0
- package/dist/utils/managedBy.d.ts +3 -3
- package/dist/utils/managedBy.js +5 -5
- package/dist/utils/native-date-value.d.ts +48 -0
- package/dist/utils/native-date-value.js +167 -0
- package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
- package/dist/utils/non-grid-row-ceiling.js +141 -0
- package/dist/utils/normalize-list-view.d.ts +121 -1
- package/dist/utils/normalize-list-view.js +186 -6
- package/dist/utils/number-display.d.ts +63 -14
- package/dist/utils/number-display.js +34 -12
- package/dist/utils/object-aggregate-query.d.ts +113 -0
- package/dist/utils/object-aggregate-query.js +50 -0
- package/dist/utils/parent-scope.d.ts +123 -0
- package/dist/utils/parent-scope.js +140 -0
- package/dist/utils/predicate-fields.d.ts +45 -12
- package/dist/utils/predicate-fields.js +137 -12
- package/dist/utils/record-color.d.ts +75 -0
- package/dist/utils/record-color.js +65 -0
- package/dist/utils/record-source.d.ts +298 -0
- package/dist/utils/record-source.js +331 -0
- package/dist/utils/record-title.d.ts +148 -8
- package/dist/utils/record-title.js +220 -26
- package/dist/utils/reference-keys.d.ts +21 -22
- package/dist/utils/reference-keys.js +532 -29
- package/dist/utils/sort-query.d.ts +115 -15
- package/dist/utils/sort-query.js +240 -13
- package/dist/utils/text-comparand.d.ts +92 -0
- package/dist/utils/text-comparand.js +99 -0
- package/dist/utils/without-denied-fields.d.ts +49 -0
- package/dist/utils/without-denied-fields.js +52 -0
- package/dist/validation/index.d.ts +8 -4
- package/dist/validation/index.js +8 -4
- package/dist/validation/schema-validator.d.ts +1 -1
- package/dist/validation/schema-validator.js +200 -82
- package/dist/validation/validators/object-validation-engine.d.ts +1 -1
- package/dist/validation/validators/object-validation-engine.js +14 -3
- package/package.json +8 -9
- package/dist/data-scope/DataScopeManager.d.ts +0 -127
- package/dist/data-scope/DataScopeManager.js +0 -229
- package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
- package/dist/protocols/ResponsiveProtocol.js +0 -158
- package/dist/validation/validation-engine.d.ts +0 -88
- package/dist/validation/validation-engine.js +0 -434
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
import { evalFieldPredicate } from './fieldRules.js';
|
|
9
|
+
import { isEmptyValue } from '../utils/emptiness.js';
|
|
9
10
|
/**
|
|
10
11
|
* Normalize a field's `dependsOn` to the list of sibling field names it reacts
|
|
11
12
|
* to. The lookup-only `{field,param}` form contributes just its `field`.
|
|
@@ -18,10 +19,12 @@ export function resolveDependsOnFields(dependsOn) {
|
|
|
18
19
|
.map((d) => (typeof d === 'string' ? d : d && typeof d === 'object' ? d.field : null))
|
|
19
20
|
.filter((f) => typeof f === 'string' && f.length > 0);
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
// A dependency counts as UNMET on exactly the shared floor — `null`,
|
|
23
|
+
// `undefined`, the empty string, the empty array — and this module is where
|
|
24
|
+
// those four members were first written down. objectui#8496 promoted them out
|
|
25
|
+
// of here into `utils/emptiness.ts` (byte-for-byte the same four) so the four
|
|
26
|
+
// other surfaces that had each re-spelled them could stop. No extension and no
|
|
27
|
+
// declension: a gated option list asks the floor and nothing more.
|
|
25
28
|
/**
|
|
26
29
|
* True when at least one `dependsOn` field is empty in the record — the option
|
|
27
30
|
* list is gated and the widget should prompt for the parent rather than show an
|
|
@@ -45,6 +48,71 @@ export function resolveVisibleOptions(options, record, scope) {
|
|
|
45
48
|
context: `visibleWhen of option '${String(o.value)}'`,
|
|
46
49
|
}));
|
|
47
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* The text an option widget SHOWS for one option: its `label`, or its `value`
|
|
53
|
+
* when the label is blank (`20b507aff`).
|
|
54
|
+
*
|
|
55
|
+
* ## Why a blank label reaches a renderer at all — it is a LEGAL document
|
|
56
|
+
*
|
|
57
|
+
* This is deliberately NOT the lenient renderer fallback AGENTS.md #0.1
|
|
58
|
+
* forbids. That rule governs metadata the contract REFUSES ("if the metadata
|
|
59
|
+
* is off-spec, fix it at the producer"); an empty label is metadata the
|
|
60
|
+
* contract ACCEPTS. Measured on the installed `@objectstack/spec` 17.6.0,
|
|
61
|
+
* `SelectOptionSchema`:
|
|
62
|
+
*
|
|
63
|
+
* { value: 'low', label: 'Low' } -> ACCEPT (lit control)
|
|
64
|
+
* { value: 'low', label: '' } -> ACCEPT <- the key reading
|
|
65
|
+
* { value: 'low' } -> REJECT invalid_type at [label]
|
|
66
|
+
*
|
|
67
|
+
* `@object-ui/types` mirrors that reading in prose on `SelectOptionBase`:
|
|
68
|
+
* "An empty string is a valid label; an ABSENT one is not." So the producer
|
|
69
|
+
* is right to emit `label: ''` — the field designer's `patchOptions` does,
|
|
70
|
+
* under the ruling `f0f774b0d` landed, and a pin
|
|
71
|
+
* (`ObjectFieldInspector.optionLabel.test.tsx`) fails if it ever invents
|
|
72
|
+
* content to fill the hole instead. A document every layer calls legal has to
|
|
73
|
+
* render as something a person can click.
|
|
74
|
+
*
|
|
75
|
+
* ## What the contract does NOT say, and who had already answered it
|
|
76
|
+
*
|
|
77
|
+
* The spec states no display default for a legal-but-blank label, so nothing
|
|
78
|
+
* upstream decides this. objectui had nevertheless already decided it — on
|
|
79
|
+
* HALF its read sites. Measured across the four option widgets before this
|
|
80
|
+
* helper existed, with `{ value: 'low', label: '' }` authored and
|
|
81
|
+
* `{ value: 'high', label: 'High' }` beside it as the lit control:
|
|
82
|
+
*
|
|
83
|
+
* | widget | readonly path | interactive path |
|
|
84
|
+
* | select | "low" | "" |
|
|
85
|
+
* | multiselect | "low" | "" |
|
|
86
|
+
* | radio | "low" | "" |
|
|
87
|
+
* | checkboxes | "low" | "" |
|
|
88
|
+
*
|
|
89
|
+
* Four summary paths spelled `opt?.label || value` and fell back; the four
|
|
90
|
+
* interactive paths spelled `{opt.label}` and rendered nothing. So the same
|
|
91
|
+
* option read "low" in a read-only form and blank in the editable one — the
|
|
92
|
+
* bug was never "tolerate or refuse", it was a widget family disagreeing with
|
|
93
|
+
* itself, and the blank half is the one a person has to click.
|
|
94
|
+
*
|
|
95
|
+
* This function is that decision written ONCE, the same "one definition, N
|
|
96
|
+
* surfaces" move as {@link CASCADE_OPTION_WIDGET_TYPES} below. All eight sites
|
|
97
|
+
* call it, so the two halves cannot drift apart again.
|
|
98
|
+
*
|
|
99
|
+
* ## Boundaries
|
|
100
|
+
*
|
|
101
|
+
* - Blank means TRIM-empty, so a whitespace-only label falls back too: it is
|
|
102
|
+
* the same invisible row to the person looking at it, and it is the
|
|
103
|
+
* spelling the producer's own reader already uses to call an authored
|
|
104
|
+
* `value` blank (`classifyOption`, `o.value.trim() === ''`).
|
|
105
|
+
* - `label` is typed required, but this reads defensively because renderers
|
|
106
|
+
* are handed unvalidated runtime metadata: a non-string label must fall back
|
|
107
|
+
* rather than throw where `{opt.label}` merely rendered nothing.
|
|
108
|
+
* - This changes DISPLAY only. It writes nothing, and it is not a licence to
|
|
109
|
+
* author blank labels — whether the contract should refuse them outright is
|
|
110
|
+
* a `packages/spec` question, not a renderer one.
|
|
111
|
+
*/
|
|
112
|
+
export function optionDisplayLabel(option) {
|
|
113
|
+
const { label } = option;
|
|
114
|
+
return typeof label === 'string' && label.trim() !== '' ? label : String(option.value);
|
|
115
|
+
}
|
|
48
116
|
/**
|
|
49
117
|
* Whether a scalar field value is still valid given the currently-visible
|
|
50
118
|
* options — used to decide a cascade clear (parent changed, child's old choice
|
|
@@ -112,12 +180,21 @@ export function resolveCascadingOptions(rawOptions, record, dependsOn, scope) {
|
|
|
112
180
|
*
|
|
113
181
|
* `dependentValues` is also the channel two widget-hint pickers read a specific
|
|
114
182
|
* SIBLING KEY from (`filter-condition` reads `object_name`, `recipient-picker`
|
|
115
|
-
* reads `recipient_type`), and the lookup family filters its query by it
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
183
|
+
* reads `recipient_type`), and the lookup family filters its query by it —
|
|
184
|
+
* neither of which is an option list, so ⛔ no member is ever added to THIS set
|
|
185
|
+
* to serve them. Each surface decides that separately, by OR-ing its own rule
|
|
186
|
+
* beside this one, and they do not agree:
|
|
187
|
+
*
|
|
188
|
+
* - the object form has fed both families all along — `needsDataSourceWiring`,
|
|
189
|
+
* its own line beside the one that reads this set;
|
|
190
|
+
* - the single-record action dialog feeds the lookup family since
|
|
191
|
+
* objectui#8672 ruling A — `paramNeedsDependentValues()`, same shape;
|
|
192
|
+
* - the bulk action dialog feeds neither, and nothing has ruled on it.
|
|
193
|
+
*
|
|
194
|
+
* ⇒ objectui#4771 is the open boundary for the widget-hint pickers everywhere,
|
|
195
|
+
* and for the lookup family on the bulk dialog. Stated here so the surfaces'
|
|
196
|
+
* copies of the statement become one; ⛔ do not read a member of this set as
|
|
197
|
+
* the answer to "does widget X get the record on surface Y".
|
|
121
198
|
*
|
|
122
199
|
* ## Normalization stays with the consumer
|
|
123
200
|
*
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* The normalized shape {@link ExpressionEvaluator.evaluateCondition} consumes:
|
|
10
10
|
* a boolean (short-circuits), a `${…}` template string (legacy JS path), a
|
|
11
11
|
* `{ dialect: 'cel', source }` envelope (canonical `@objectstack/formula`
|
|
12
|
-
* path),
|
|
12
|
+
* path), `undefined` for "no predicate declared", or a `cel` envelope with NO
|
|
13
|
+
* `source` for a gate that is declared but cannot be evaluated — which the
|
|
14
|
+
* evaluator treats as a fault, never as "absent" (objectui#11358).
|
|
13
15
|
*
|
|
14
16
|
* ## Why not `PredicateInput` (objectstack#4115 / objectui#3074)
|
|
15
17
|
*
|
|
@@ -24,7 +26,8 @@
|
|
|
24
26
|
* admits `boolean` and the `${…}` template spelling (neither of which the spec
|
|
25
27
|
* models as a predicate), narrows `dialect` to `cel` alone (every other dialect
|
|
26
28
|
* has already been flattened onto the legacy path by `toPredicateInput`),
|
|
27
|
-
* requires a non-empty `source
|
|
29
|
+
* requires a non-empty `source` (or none at all, for the declared-but-
|
|
30
|
+
* unevaluable state), and carries no `ast` / `meta`. The two unions
|
|
28
31
|
* are not mutually assignable in either direction.
|
|
29
32
|
*
|
|
30
33
|
* The rename is pinned from both sides by the tripwire in
|
|
@@ -36,6 +39,9 @@
|
|
|
36
39
|
export type EvaluatorPredicateInput = string | boolean | {
|
|
37
40
|
dialect: 'cel';
|
|
38
41
|
source: string;
|
|
42
|
+
} | {
|
|
43
|
+
readonly dialect: 'cel';
|
|
44
|
+
readonly source?: undefined;
|
|
39
45
|
} | undefined;
|
|
40
46
|
/**
|
|
41
47
|
* Normalize a schema-supplied predicate (`visible` / `enabled` / `disabled` /
|
|
@@ -52,8 +58,13 @@ export type EvaluatorPredicateInput = string | boolean | {
|
|
|
52
58
|
* authored predicate, including bare strings) → a `cel` dialect keeps
|
|
53
59
|
* its envelope; every other dialect is unwrapped and wrapped as
|
|
54
60
|
* `${source}`.
|
|
55
|
-
* - `null` / `undefined` /
|
|
56
|
-
* (default visible/enabled).
|
|
61
|
+
* - `null` / `undefined` / `''` / an envelope whose `source` is `''` →
|
|
62
|
+
* `undefined` (absent: default visible/enabled).
|
|
63
|
+
* - anything else that is present — an envelope with no string `source`
|
|
64
|
+
* (`{ dialect: 'cel', ast }`), a number, `{}`, an array → the frozen
|
|
65
|
+
* `{ dialect: 'cel' }` with no `source`: declared, and a FAULT to the
|
|
66
|
+
* evaluator (objectui#11358 — see `DECLARED_UNEVALUABLE` above). This used
|
|
67
|
+
* to fold into `undefined` with the absent shapes.
|
|
57
68
|
*
|
|
58
69
|
* ## Why the `cel` envelope must survive normalization (#2661 / #3314)
|
|
59
70
|
*
|
|
@@ -5,6 +5,28 @@
|
|
|
5
5
|
* This source code is licensed under the MIT license found in the
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
|
+
import { isUnevaluablePredicate } from './unevaluablePredicate.js';
|
|
9
|
+
/**
|
|
10
|
+
* The normalized answer for a gate that is DECLARED but has nothing the client
|
|
11
|
+
* can evaluate (objectui#11358) — the fourth member of
|
|
12
|
+
* {@link EvaluatorPredicateInput}, a `cel` envelope with no `source`.
|
|
13
|
+
*
|
|
14
|
+
* It exists so the normalizer can tell three states apart instead of two.
|
|
15
|
+
* `undefined` is "absent" and evaluates to `true` (visible / enabled) by the
|
|
16
|
+
* evaluator's one documented default; this value is "declared and faulting"
|
|
17
|
+
* and `ExpressionEvaluator.evaluateCondition` treats it as a FAULT — reported,
|
|
18
|
+
* thrown to a `throwOnError` caller, and answered with the fail-soft `true`
|
|
19
|
+
* otherwise — so every reader gets the fail direction its key already has for
|
|
20
|
+
* a predicate that cannot be evaluated. Folding it into `undefined`, as this
|
|
21
|
+
* function did, made an `ast`-only envelope, `0`, `{}` or an array read as
|
|
22
|
+
* "no gate" everywhere, in silence (ADR-0137 D4).
|
|
23
|
+
*
|
|
24
|
+
* Shape, not a marker: it says what the evaluator receives — a CEL predicate
|
|
25
|
+
* (a bare predicate is CEL by the spec's contract) whose `source` is absent —
|
|
26
|
+
* and carries nothing of the original value, so no reader can guess what an
|
|
27
|
+
* `ast` meant. Frozen and shared, so a reader's memo sees one value per state.
|
|
28
|
+
*/
|
|
29
|
+
const DECLARED_UNEVALUABLE = Object.freeze({ dialect: 'cel' });
|
|
8
30
|
/**
|
|
9
31
|
* Wrap a BARE expression as a `${…}` template, and leave a string that already
|
|
10
32
|
* carries template syntax alone (objectui#3871 — rationale in
|
|
@@ -29,8 +51,13 @@ function wrapIfBare(expression) {
|
|
|
29
51
|
* authored predicate, including bare strings) → a `cel` dialect keeps
|
|
30
52
|
* its envelope; every other dialect is unwrapped and wrapped as
|
|
31
53
|
* `${source}`.
|
|
32
|
-
* - `null` / `undefined` /
|
|
33
|
-
* (default visible/enabled).
|
|
54
|
+
* - `null` / `undefined` / `''` / an envelope whose `source` is `''` →
|
|
55
|
+
* `undefined` (absent: default visible/enabled).
|
|
56
|
+
* - anything else that is present — an envelope with no string `source`
|
|
57
|
+
* (`{ dialect: 'cel', ast }`), a number, `{}`, an array → the frozen
|
|
58
|
+
* `{ dialect: 'cel' }` with no `source`: declared, and a FAULT to the
|
|
59
|
+
* evaluator (objectui#11358 — see `DECLARED_UNEVALUABLE` above). This used
|
|
60
|
+
* to fold into `undefined` with the absent shapes.
|
|
34
61
|
*
|
|
35
62
|
* ## Why the `cel` envelope must survive normalization (#2661 / #3314)
|
|
36
63
|
*
|
|
@@ -113,6 +140,10 @@ export function toPredicateInput(value) {
|
|
|
113
140
|
return value;
|
|
114
141
|
if (typeof value === 'string')
|
|
115
142
|
return wrapIfBare(value);
|
|
143
|
+
// Declared, but nothing here can be evaluated (objectui#11358): kept as its
|
|
144
|
+
// own state rather than folded into "absent". See `DECLARED_UNEVALUABLE`.
|
|
145
|
+
if (isUnevaluablePredicate(value))
|
|
146
|
+
return DECLARED_UNEVALUABLE;
|
|
116
147
|
if (typeof value === 'object' && typeof value.source === 'string') {
|
|
117
148
|
const src = value.source;
|
|
118
149
|
if (!src)
|
|
@@ -125,5 +156,7 @@ export function toPredicateInput(value) {
|
|
|
125
156
|
// Every other dialect (template / unset) keeps the legacy `${…}` behavior.
|
|
126
157
|
return wrapIfBare(src);
|
|
127
158
|
}
|
|
159
|
+
// Unreachable: every present value is a boolean, a string, an envelope with a
|
|
160
|
+
// string `source`, or unevaluable — all answered above. Kept for the compiler.
|
|
128
161
|
return undefined;
|
|
129
162
|
}
|
|
@@ -26,8 +26,8 @@ export type NonCanonicalRowSpelling = {
|
|
|
26
26
|
* predicate is already canonical (or is not this module's verdict to give).
|
|
27
27
|
*
|
|
28
28
|
* Deliberately conservative in both arms — every condition below can only
|
|
29
|
-
* REMOVE a finding, never invent one, because a false
|
|
30
|
-
*
|
|
29
|
+
* REMOVE a finding, never invent one, because a false finding sends an author
|
|
30
|
+
* to rewrite a predicate that was correct:
|
|
31
31
|
*
|
|
32
32
|
* - **Unparseable source** → `null`. Syntax is another gate's verdict.
|
|
33
33
|
* - **Bare shorthand** is reported only when the undeclared identifier is an own
|
|
@@ -45,23 +45,12 @@ export type NonCanonicalRowSpelling = {
|
|
|
45
45
|
* predicate manages both it is the one the author must fix to have anything
|
|
46
46
|
* evaluate at all.
|
|
47
47
|
*
|
|
48
|
-
* @param source The predicate's CEL text.
|
|
49
|
-
*
|
|
50
|
-
* dialect
|
|
51
|
-
*
|
|
48
|
+
* @param source The predicate's CEL text. A legacy `${…}`-dialect string
|
|
49
|
+
* is not CEL and returns `null` here (unparseable); classify
|
|
50
|
+
* it by its own dialect's rules — on a runtime record surface
|
|
51
|
+
* it retired with the CEL spellings (`83fe6e741`), while on
|
|
52
|
+
* the schema/widget tier `data` is a different scope entirely.
|
|
52
53
|
* @param row The row the predicate is bound against.
|
|
53
54
|
* @param dataNamesRow Whether `data` is bound to that same row on this surface.
|
|
54
55
|
*/
|
|
55
56
|
export declare function detectNonCanonicalRowSpelling(source: string, row: Record<string, unknown> | null | undefined, dataNamesRow: boolean): NonCanonicalRowSpelling | null;
|
|
56
|
-
/** Reset the row-predicate spelling warn-once memo. Exported for tests. */
|
|
57
|
-
export declare function resetRowPredicateCanonWarnings(): void;
|
|
58
|
-
/**
|
|
59
|
-
* Report a non-canonical row-predicate spelling once, in dev.
|
|
60
|
-
*
|
|
61
|
-
* Phase 1 of the objectui#5330 window: the binding is UNCHANGED and every
|
|
62
|
-
* spelling still resolves — this only says so out loud, so the stored
|
|
63
|
-
* population stops growing and a later survey has something to count. It is a
|
|
64
|
-
* warning and deliberately not a refusal: turning it into one would move the
|
|
65
|
-
* accept/reject set, which this card is explicitly not entitled to do.
|
|
66
|
-
*/
|
|
67
|
-
export declare function warnNonCanonicalRowSpelling(source: string, row: Record<string, unknown> | null | undefined, dataNamesRow: boolean, label?: string): void;
|
|
@@ -6,70 +6,76 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
/**
|
|
9
|
-
* The row-predicate spelling CANON
|
|
10
|
-
*
|
|
9
|
+
* The row-predicate spelling CANON: on a runtime record surface the row is bound
|
|
10
|
+
* as `record.*` and nothing else (objectui#5330, maintainer ruling 2026-08-20,
|
|
11
|
+
* option B; Phase 2 executed by `83fe6e741`).
|
|
11
12
|
*
|
|
12
|
-
* ## The canon
|
|
13
|
+
* ## The canon
|
|
13
14
|
*
|
|
14
15
|
* A row predicate — `visible` / `disabled` / `enabled` on an action renderer, a
|
|
15
|
-
* row scope, a `record:alert
|
|
16
|
-
* `record.status`, bare shorthand `status`,
|
|
17
|
-
* (objectui#4075 / PR #4079 bound all four action
|
|
18
|
-
* restore consistency, deliberately without
|
|
19
|
-
* CONTRACT).
|
|
20
|
-
*
|
|
21
|
-
* > **The canon is `record.*`.**
|
|
22
|
-
* >
|
|
23
|
-
* >
|
|
24
|
-
* >
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* ## The canon states the SERVER's accept set, not this client's
|
|
35
|
-
*
|
|
36
|
-
* The ruling made that the dev's first measurement, because a canon that only
|
|
37
|
-
* describes the renderer would be the very thing it exists to end. Measured
|
|
38
|
-
* against `@objectstack/formula@17.1.0` — the engine the server evaluates with,
|
|
39
|
-
* and the one `fieldRules.ts` already delegates to:
|
|
16
|
+
* row scope, a `record:alert`, a conditional-formatting `condition` — used to
|
|
17
|
+
* bind the row THREE ways: canonical `record.status`, bare shorthand `status`,
|
|
18
|
+
* and legacy `data.status` (objectui#4075 / PR #4079 bound all four action
|
|
19
|
+
* renderers all three ways to restore consistency, deliberately without
|
|
20
|
+
* deciding which of them was CONTRACT). The ruling decided it, in two phases:
|
|
21
|
+
*
|
|
22
|
+
* > **The canon is `record.*`.** Phase 1 (`d1ab06f0f`) declared it and warned
|
|
23
|
+
* > once, in dev, on the two other spellings. Phase 2 (`83fe6e741`, ruled
|
|
24
|
+
* > 2026-09-02 and amended 2026-09-05) retired them: the bare shorthand and
|
|
25
|
+
* > `data.*` are no longer bound on runtime record surfaces, and the Phase-1
|
|
26
|
+
* > warning went with them. No stored-metadata survey was run (「不考虑存量」);
|
|
27
|
+
* > the Phase-1 warning period was the notice.
|
|
28
|
+
*
|
|
29
|
+
* ## What a retired spelling does now: it FAULTS, as it always did on the server
|
|
30
|
+
*
|
|
31
|
+
* The canon states the SERVER's accept set. Measured against
|
|
32
|
+
* `@objectstack/formula@17.1.0` — the engine the server evaluates with, and the
|
|
33
|
+
* one `fieldRules.ts` delegates to:
|
|
40
34
|
*
|
|
41
35
|
* | spelling | server runtime (`buildScope` + `celEngine`) | server authoring oracle |
|
|
42
36
|
* |---|---|---|
|
|
43
37
|
* | `record.status` | ✅ `{ ok: true, value: true }` | ✅ accepted |
|
|
44
38
|
* | bare `status` | ❌ `Unknown variable: status` | ❌ refused (`'status'`) |
|
|
45
|
-
* | `data.status` | ❌ `Unknown variable: data` | ⚠️
|
|
46
|
-
*
|
|
47
|
-
* `buildScope({ record })` mounts exactly `['record']
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
39
|
+
* | `data.status` | ❌ `Unknown variable: data` | ⚠️ silently accepted |
|
|
40
|
+
*
|
|
41
|
+
* `buildScope({ record })` mounts exactly `['record']`. Since Phase 2 the client
|
|
42
|
+
* binds the same set (`listConditional.ts`' scope bag and `@object-ui/react`'s
|
|
43
|
+
* `usePredicateRecordContext`), so both retired spellings fault on the client
|
|
44
|
+
* with the server's verdict, and each surface applies its EXISTING fault policy:
|
|
45
|
+
* fail-closed `visible` on the throwing `useCondition` legs, the caller's
|
|
46
|
+
* `fallback` on `evalRowPredicate` / `partitionRowsByPredicate`, fail-soft on
|
|
47
|
+
* the non-throwing `useCondition` legs. There is no runtime detector, no
|
|
48
|
+
* "treat as absent" special case and no uniform override — a retired spelling
|
|
49
|
+
* is not a recognised-and-rejected thing, it is an unknown variable like any
|
|
50
|
+
* other, and the existing fault warnings are what name it.
|
|
51
|
+
*
|
|
52
|
+
* One consequence, stated so it is not read back as a bug: a host scope may
|
|
53
|
+
* legitimately carry its OWN `data` (a rowless dialog's, or app-shell's ambient
|
|
54
|
+
* `data: {}`), and it is left standing. `data.*` on a record surface then reads
|
|
55
|
+
* the host's object rather than the row — which is exactly what "no longer bound
|
|
56
|
+
* to the row" means, and, against an ambient `data: {}`, the constant-false
|
|
57
|
+
* signature the Phase-1 warning text already described for the server.
|
|
58
|
+
*
|
|
59
|
+
* ## `data.*` is retired HERE, not everywhere — the canon is layer-scoped
|
|
64
60
|
*
|
|
65
61
|
* `data` is the CANONICAL root one layer over, in a metadata-editing form (the
|
|
66
62
|
* row under edit): objectstack's `CANONICAL_ROOT_BY_LAYER` reads
|
|
67
63
|
* `{ runtime: 'record', metadata: 'data' }` (ADR-0089 D3), and objectui's own
|
|
68
|
-
* `app-shell` metadata-admin `SchemaForm` binds `{ data: row }` on purpose
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
64
|
+
* `app-shell` metadata-admin `SchemaForm` binds `{ data: row }` on purpose,
|
|
65
|
+
* through its own evaluator (`views/metadata-admin/predicate.ts`) — never
|
|
66
|
+
* through `evalRowPredicate` or `usePredicateRecordContext`. So `data.*` is not
|
|
67
|
+
* retired platform-wide: it is a WRONG-LAYER spelling on a runtime record
|
|
68
|
+
* surface, and that is the only thing the detector below reports.
|
|
69
|
+
*
|
|
70
|
+
* ## What is left in this module, and why
|
|
71
|
+
*
|
|
72
|
+
* {@link detectNonCanonicalRowSpelling} is the OFFLINE instrument: it classifies
|
|
73
|
+
* an authored predicate's spelling against a row without evaluating it, so a
|
|
74
|
+
* sweep over stored or in-repo metadata (the `702c48a62` corpus sweep's
|
|
75
|
+
* recipe) can find the documents that still need rewriting. It is
|
|
76
|
+
* exported for that purpose and nothing on the hot path calls it — the runtime
|
|
77
|
+
* warning half Phase 1 built on it (`warnNonCanonicalRowSpelling`,
|
|
78
|
+
* `resetRowPredicateCanonWarnings`) was removed with the bindings.
|
|
73
79
|
*
|
|
74
80
|
* ## Why the detection reuses the server's oracle instead of a regex
|
|
75
81
|
*
|
|
@@ -94,8 +100,8 @@ const METADATA_LAYER_ROOT = 'data';
|
|
|
94
100
|
* predicate is already canonical (or is not this module's verdict to give).
|
|
95
101
|
*
|
|
96
102
|
* Deliberately conservative in both arms — every condition below can only
|
|
97
|
-
* REMOVE a finding, never invent one, because a false
|
|
98
|
-
*
|
|
103
|
+
* REMOVE a finding, never invent one, because a false finding sends an author
|
|
104
|
+
* to rewrite a predicate that was correct:
|
|
99
105
|
*
|
|
100
106
|
* - **Unparseable source** → `null`. Syntax is another gate's verdict.
|
|
101
107
|
* - **Bare shorthand** is reported only when the undeclared identifier is an own
|
|
@@ -113,10 +119,11 @@ const METADATA_LAYER_ROOT = 'data';
|
|
|
113
119
|
* predicate manages both it is the one the author must fix to have anything
|
|
114
120
|
* evaluate at all.
|
|
115
121
|
*
|
|
116
|
-
* @param source The predicate's CEL text.
|
|
117
|
-
*
|
|
118
|
-
* dialect
|
|
119
|
-
*
|
|
122
|
+
* @param source The predicate's CEL text. A legacy `${…}`-dialect string
|
|
123
|
+
* is not CEL and returns `null` here (unparseable); classify
|
|
124
|
+
* it by its own dialect's rules — on a runtime record surface
|
|
125
|
+
* it retired with the CEL spellings (`83fe6e741`), while on
|
|
126
|
+
* the schema/widget tier `data` is a different scope entirely.
|
|
120
127
|
* @param row The row the predicate is bound against.
|
|
121
128
|
* @param dataNamesRow Whether `data` is bound to that same row on this surface.
|
|
122
129
|
*/
|
|
@@ -149,60 +156,3 @@ export function detectNonCanonicalRowSpelling(source, row, dataNamesRow) {
|
|
|
149
156
|
}
|
|
150
157
|
return null;
|
|
151
158
|
}
|
|
152
|
-
/**
|
|
153
|
-
* Dev-mode gate, matching `utils/dashboard-filters.ts` and `actions/actionKeys.ts`
|
|
154
|
-
* — a deprecation warning that floods a production console is a warning that
|
|
155
|
-
* gets muted.
|
|
156
|
-
*/
|
|
157
|
-
const isDev = () => globalThis.process?.env
|
|
158
|
-
?.NODE_ENV !== 'production';
|
|
159
|
-
/**
|
|
160
|
-
* Warn-once memo, keyed by the `(label, predicate source)` pair — the same
|
|
161
|
-
* identity `warnEvalError` uses, and JSON-encoded for the same reason: the
|
|
162
|
-
* separator that boundary once used was a raw U+0000, which made the file
|
|
163
|
-
* carrying it binary to grep (objectstack#5450). Keying on the source alone
|
|
164
|
-
* would report the first surface carrying a shorthand `status` predicate and
|
|
165
|
-
* stay silent about every other one; the label is what sends the author to the
|
|
166
|
-
* right screen.
|
|
167
|
-
*
|
|
168
|
-
* Module scope, not per-call: these predicates are re-evaluated on every row of
|
|
169
|
-
* every render, so per-call state would warn once per frame — the flood the
|
|
170
|
-
* dedupe exists to prevent.
|
|
171
|
-
*/
|
|
172
|
-
const warnedSpellings = new Set();
|
|
173
|
-
/** Reset the row-predicate spelling warn-once memo. Exported for tests. */
|
|
174
|
-
export function resetRowPredicateCanonWarnings() {
|
|
175
|
-
warnedSpellings.clear();
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Report a non-canonical row-predicate spelling once, in dev.
|
|
179
|
-
*
|
|
180
|
-
* Phase 1 of the objectui#5330 window: the binding is UNCHANGED and every
|
|
181
|
-
* spelling still resolves — this only says so out loud, so the stored
|
|
182
|
-
* population stops growing and a later survey has something to count. It is a
|
|
183
|
-
* warning and deliberately not a refusal: turning it into one would move the
|
|
184
|
-
* accept/reject set, which this card is explicitly not entitled to do.
|
|
185
|
-
*/
|
|
186
|
-
export function warnNonCanonicalRowSpelling(source, row, dataNamesRow, label) {
|
|
187
|
-
if (!isDev())
|
|
188
|
-
return;
|
|
189
|
-
const finding = detectNonCanonicalRowSpelling(source, row, dataNamesRow);
|
|
190
|
-
if (!finding)
|
|
191
|
-
return;
|
|
192
|
-
const key = JSON.stringify([label ?? '', source, finding.kind]);
|
|
193
|
-
if (warnedSpellings.has(key))
|
|
194
|
-
return;
|
|
195
|
-
warnedSpellings.add(key);
|
|
196
|
-
const where = label ? ` (${label})` : '';
|
|
197
|
-
const detail = finding.kind === 'bare-shorthand'
|
|
198
|
-
? `it references the bare field ${JSON.stringify(finding.identifier)}; ` +
|
|
199
|
-
`the server refuses that spelling outright ("Unknown variable: ${finding.identifier}")`
|
|
200
|
-
: 'it is rooted at `data.`, which is the metadata-editing-form root — on a ' +
|
|
201
|
-
'record surface the server binds no `data` at all, so the predicate is a ' +
|
|
202
|
-
'constant false there rather than an error';
|
|
203
|
-
console.warn(`[object-ui] A row predicate${where} uses a DEPRECATED spelling: ` +
|
|
204
|
-
`${JSON.stringify(source)} — ${detail}. The canon is \`record.*\` ` +
|
|
205
|
-
`(objectui#5330, ruled 2026-08-20): write \`${finding.canonical}\`. ` +
|
|
206
|
-
'This still evaluates here for now; the tolerance retires after a ' +
|
|
207
|
-
'stored-metadata survey.');
|
|
208
|
-
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
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 acting subject's effective object permissions, carried to the one place
|
|
10
|
+
* this repo hands a predicate to the CEL engine (objectui#4421).
|
|
11
|
+
*
|
|
12
|
+
* ## What it is for
|
|
13
|
+
*
|
|
14
|
+
* `@objectstack/formula` registers `current_user.can(object, verb)` as a
|
|
15
|
+
* receiver method on the acting subject, and answers it from
|
|
16
|
+
* `EvalContext.permissions` — the verbatim `objects` map of the
|
|
17
|
+
* `/auth/me/permissions` response. That map is a separate field of the engine
|
|
18
|
+
* context, and the engine is explicit that it is ⛔ NOT a CEL variable: an
|
|
19
|
+
* authored predicate must not be able to read `permissions.crm_lead.allowEdit`
|
|
20
|
+
* and step around the closed verb table.
|
|
21
|
+
*
|
|
22
|
+
* This repo's evaluation seam (`evalFieldPredicate`) is not handed an engine
|
|
23
|
+
* context by its callers, though. Every action surface — the row menu, the
|
|
24
|
+
* record header, the `action:*` renderers, `DeclaredActionsBar` — hands it a
|
|
25
|
+
* variable BAG (the host's predicate scope, spread, merged, pushed through an
|
|
26
|
+
* `ExpressionContext`), and the bag's string keys all become CEL roots. So the
|
|
27
|
+
* permissions cannot ride on the bag under a name: any name would be a root.
|
|
28
|
+
*
|
|
29
|
+
* They ride on the SUBJECT instead, under a symbol:
|
|
30
|
+
*
|
|
31
|
+
* - The engine models the map as the subject's own answer sheet — it binds
|
|
32
|
+
* `permissions` only alongside a user, and `can` refuses any receiver that
|
|
33
|
+
* is not that user (`receiver !== subject`). The subject is the one object
|
|
34
|
+
* every bag already carries by reference under `current_user` (and the
|
|
35
|
+
* `user` / `ctx.user` / `os.user` aliases of it), through every spread and
|
|
36
|
+
* every `ExpressionContext` scope, so no surface needs its own copy of the
|
|
37
|
+
* hand-off.
|
|
38
|
+
* - A symbol is the one kind of key no predicate dialect can name: CEL
|
|
39
|
+
* identifiers and member names are strings, and so are the legacy
|
|
40
|
+
* evaluator's. `current_user.permissions` still faults as an unknown key.
|
|
41
|
+
*
|
|
42
|
+
* ## Absent is not empty
|
|
43
|
+
*
|
|
44
|
+
* A subject that carries NO map — the payload has not loaded, no permission
|
|
45
|
+
* provider is mounted, or the provider has no `/auth/me/permissions` answer at
|
|
46
|
+
* all — hands the engine no `permissions`, and `can` then refuses loudly
|
|
47
|
+
* (`ok: false`, `kind: 'runtime'`). That refusal is a FAULT, so each surface
|
|
48
|
+
* applies its own fault policy to it; it is never turned into a quiet `false`
|
|
49
|
+
* here, because the engine's contract is that an empty map means "holds
|
|
50
|
+
* nothing" and a caller that has no data must not claim it has that answer.
|
|
51
|
+
*/
|
|
52
|
+
import type { EvalPermissions } from '@objectstack/formula';
|
|
53
|
+
/**
|
|
54
|
+
* The key the subject carries its effective object permissions under.
|
|
55
|
+
*
|
|
56
|
+
* `Symbol.for` rather than `Symbol()` so two copies of this module in one page
|
|
57
|
+
* (a host that bundles `@object-ui/core` twice) still agree on the key; the
|
|
58
|
+
* namespace keeps it from colliding with anyone else's registry entry.
|
|
59
|
+
*/
|
|
60
|
+
export declare const SUBJECT_PERMISSIONS: unique symbol;
|
|
61
|
+
/**
|
|
62
|
+
* The subject to publish as `current_user`, carrying `permissions` when there
|
|
63
|
+
* are any.
|
|
64
|
+
*
|
|
65
|
+
* Returns a shallow copy so the caller's own user object is never mutated; the
|
|
66
|
+
* copy is what the whole predicate scope must use for every alias of the
|
|
67
|
+
* subject, because the engine compares the `can` receiver by IDENTITY. With no
|
|
68
|
+
* `permissions` the result carries none — a subject built from one that did is
|
|
69
|
+
* stripped rather than left answering from a stale map.
|
|
70
|
+
*/
|
|
71
|
+
export declare function bindSubjectPermissions<T extends object>(subject: T, permissions: EvalPermissions | undefined): T;
|
|
72
|
+
/**
|
|
73
|
+
* The effective object permissions a subject carries, or `undefined` when it
|
|
74
|
+
* carries none (see "Absent is not empty" above).
|
|
75
|
+
*/
|
|
76
|
+
export declare function subjectPermissionsOf(subject: unknown): EvalPermissions | undefined;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
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 key the subject carries its effective object permissions under.
|
|
10
|
+
*
|
|
11
|
+
* `Symbol.for` rather than `Symbol()` so two copies of this module in one page
|
|
12
|
+
* (a host that bundles `@object-ui/core` twice) still agree on the key; the
|
|
13
|
+
* namespace keeps it from colliding with anyone else's registry entry.
|
|
14
|
+
*/
|
|
15
|
+
export const SUBJECT_PERMISSIONS = Symbol.for('@object-ui/core:subject-permissions');
|
|
16
|
+
/**
|
|
17
|
+
* The subject to publish as `current_user`, carrying `permissions` when there
|
|
18
|
+
* are any.
|
|
19
|
+
*
|
|
20
|
+
* Returns a shallow copy so the caller's own user object is never mutated; the
|
|
21
|
+
* copy is what the whole predicate scope must use for every alias of the
|
|
22
|
+
* subject, because the engine compares the `can` receiver by IDENTITY. With no
|
|
23
|
+
* `permissions` the result carries none — a subject built from one that did is
|
|
24
|
+
* stripped rather than left answering from a stale map.
|
|
25
|
+
*/
|
|
26
|
+
export function bindSubjectPermissions(subject, permissions) {
|
|
27
|
+
if (permissions === undefined) {
|
|
28
|
+
if (!Object.prototype.hasOwnProperty.call(subject, SUBJECT_PERMISSIONS))
|
|
29
|
+
return subject;
|
|
30
|
+
const stripped = { ...subject };
|
|
31
|
+
delete stripped[SUBJECT_PERMISSIONS];
|
|
32
|
+
return stripped;
|
|
33
|
+
}
|
|
34
|
+
return { ...subject, [SUBJECT_PERMISSIONS]: permissions };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The effective object permissions a subject carries, or `undefined` when it
|
|
38
|
+
* carries none (see "Absent is not empty" above).
|
|
39
|
+
*/
|
|
40
|
+
export function subjectPermissionsOf(subject) {
|
|
41
|
+
if (subject === null || typeof subject !== 'object')
|
|
42
|
+
return undefined;
|
|
43
|
+
return subject[SUBJECT_PERMISSIONS];
|
|
44
|
+
}
|