@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
|
@@ -61,8 +61,22 @@
|
|
|
61
61
|
* logs `treating the field as LOCKED` instead). The *default* stays
|
|
62
62
|
* fail-open on purpose — flipping it is a shipped-behavior change tracked
|
|
63
63
|
* separately in objectstack#5149 (appeal 1, undecided).
|
|
64
|
+
*
|
|
65
|
+
* A predicate the author DECLARED and left BLANK is loud too, since
|
|
66
|
+
* objectui#8069 — and it did not used to be. It is a third state, not a
|
|
67
|
+
* spelling of either neighbour: the key is present (so it is not "no rule")
|
|
68
|
+
* and nothing evaluates (so no engine fault is raised), and it used to return
|
|
69
|
+
* the caller's permissive fallback before any warning could fire. `''` and
|
|
70
|
+
* `' '` are authorable — `ExpressionWireSchema` is a bare `z.string()`, and
|
|
71
|
+
* `resolveFieldRuleState`'s own guard is `!= null` — so the one state an author
|
|
72
|
+
* reaches by *starting* a rule and not finishing it was the one state that said
|
|
73
|
+
* nothing at all. Both spellings now report `[blank]` through the same single
|
|
74
|
+
* site as every other fault; the VERDICT is unchanged for every input.
|
|
64
75
|
*/
|
|
65
76
|
import { ExpressionEngine } from '@objectstack/formula';
|
|
77
|
+
import { subjectCapabilitiesOf, subjectPermissionsOf } from './subjectPermissions.js';
|
|
78
|
+
import { isBlankPredicateText } from './declaredPredicate.js';
|
|
79
|
+
import { isUnevaluablePredicate, UNEVALUABLE_PREDICATE_REASON } from './unevaluablePredicate.js';
|
|
66
80
|
/** Normalize a predicate into the `Expression` shape the engine expects. */
|
|
67
81
|
function toExpression(pred) {
|
|
68
82
|
if (typeof pred === 'string')
|
|
@@ -70,15 +84,35 @@ function toExpression(pred) {
|
|
|
70
84
|
return { dialect: (pred.dialect ?? 'cel'), source: pred.source };
|
|
71
85
|
}
|
|
72
86
|
const warnedPredicates = new Set();
|
|
87
|
+
/**
|
|
88
|
+
* The `reason` reported for a predicate the author DECLARED and left blank.
|
|
89
|
+
*
|
|
90
|
+
* Tagged like an engine reason (`[parse]`, `[type]`, `[throw]`) because it
|
|
91
|
+
* travels the same two channels — the built-in warning and the `onFault`
|
|
92
|
+
* passback — and a caller that routes on the tag must be able to tell this
|
|
93
|
+
* apart from a predicate that faulted with text in it: the fix for a blank one
|
|
94
|
+
* is to finish it or delete the key, never to debug its syntax.
|
|
95
|
+
*/
|
|
96
|
+
const BLANK_PREDICATE_REASON = '[blank] the predicate is declared but empty — nothing to evaluate';
|
|
73
97
|
/**
|
|
74
98
|
* One-time warning for a predicate that could not be evaluated. Deduped per
|
|
75
99
|
* predicate TEXT (dialect + source): a broken predicate is re-evaluated on
|
|
76
100
|
* every render/keystroke, and the point is one loud line, not a scrolling
|
|
77
101
|
* wall. The dedupe key is JSON-encoded — never a control-character separator
|
|
78
102
|
* (objectstack#5450 made a sibling of this file binary to grep that way).
|
|
103
|
+
*
|
|
104
|
+
* ⚠️ One class joins the LOCATOR to that key: a BLANK predicate. The text is
|
|
105
|
+
* what identifies the authoring site for every other fault — it carries the
|
|
106
|
+
* typo — but every blank predicate in an app shares the key `""`, so text
|
|
107
|
+
* alone would let the first blank rule silence every other author's. Non-blank
|
|
108
|
+
* keys are unchanged (objectui#8069).
|
|
79
109
|
*/
|
|
80
110
|
function warnPredicateFailure(expr, fallback, reason, context) {
|
|
81
|
-
|
|
111
|
+
// `Expression['source']` is optional in the spec; an absent source is blank
|
|
112
|
+
// by the same rule as a whitespace-only one, and lands in the same key shape.
|
|
113
|
+
const key = expr.source?.trim()
|
|
114
|
+
? JSON.stringify([expr.dialect, expr.source])
|
|
115
|
+
: JSON.stringify([expr.dialect, expr.source, context ?? '']);
|
|
82
116
|
if (warnedPredicates.has(key))
|
|
83
117
|
return;
|
|
84
118
|
warnedPredicates.add(key);
|
|
@@ -92,12 +126,54 @@ function warnPredicateFailure(expr, fallback, reason, context) {
|
|
|
92
126
|
/**
|
|
93
127
|
* Evaluate a field-rule CEL predicate against a record.
|
|
94
128
|
*
|
|
129
|
+
* ## `fallback` is a per-CALL-SITE policy, not a default this helper owns
|
|
130
|
+
*
|
|
131
|
+
* The parameter is required and carries no default value, so every call site
|
|
132
|
+
* states a direction of its own — and the directions in this repo do not
|
|
133
|
+
* agree. Five distinct fault policies share this one helper today, which is
|
|
134
|
+
* readable nowhere but here (objectui#8069):
|
|
135
|
+
*
|
|
136
|
+
* 1. **Fixed permissive, paired with an equal "no rule" literal.**
|
|
137
|
+
* {@link resolveFieldRuleState} (`true` / `false` / `false`) and
|
|
138
|
+
* `resolveVisibleOptions` (`evaluator/optionRules.ts`, `true`). A fault
|
|
139
|
+
* produces the permissive verdict, indistinguishable from the verdict for
|
|
140
|
+
* "the author wrote no rule" — see the `…_FAULTED` / `…_ABSENT` note below.
|
|
141
|
+
* 2. **Fixed permissive, unguarded — ONE literal answers absent *and*
|
|
142
|
+
* faulted.** Every visibility call site outside core: the form renderer
|
|
143
|
+
* (`components/renderers/form/form.tsx`), console's `FormPage`, app-shell's
|
|
144
|
+
* `ScreenView`, `plugin-form`'s `WizardForm`. All of them pass `true`, and
|
|
145
|
+
* none has a separate absent branch at all.
|
|
146
|
+
* 3. **Caller-parameterised.** `evalRowPredicate`'s single-eval fast route
|
|
147
|
+
* (`evaluator/listConditional.ts`) forwards `opts.fallback`, so the
|
|
148
|
+
* direction is the mounting surface's to state.
|
|
149
|
+
* 4. **Divergence probe → fault FLAG.** `evalCel`
|
|
150
|
+
* (`evaluator/listConditional.ts`) calls this helper once per direction with
|
|
151
|
+
* `warn: false` + `onFault`: a verdict that tracks the fallback in BOTH runs
|
|
152
|
+
* is a fault, and the caller emits its own labelled warning and returns its
|
|
153
|
+
* own fallback.
|
|
154
|
+
* 5. **Divergence probe → THROW.** `ExpressionEvaluator.evaluateCelCondition`
|
|
155
|
+
* under `throwOnError` runs the same two-call trick and converts the
|
|
156
|
+
* disagreement into `throw new Error('CEL predicate failed to evaluate: …')`.
|
|
157
|
+
*
|
|
158
|
+
* ⚠️ The last two policies exist **because** `fallback` is free to specify: they
|
|
159
|
+
* detect a fault by disagreeing with themselves. Fixing a direction *inside*
|
|
160
|
+
* this helper would remove the mechanism they are built on, which is why
|
|
161
|
+
* ADR-0137 D5 keeps `fallback` free and declares the direction at the
|
|
162
|
+
* field-rule and gate layer instead.
|
|
163
|
+
*
|
|
95
164
|
* @param pred The `visibleWhen` / `readonlyWhen` / `requiredWhen` predicate.
|
|
96
165
|
* @param record The live form values (overlays prior persisted record).
|
|
97
|
-
* @param fallback Value to return when the predicate is
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
166
|
+
* @param fallback Value to return when the predicate is ABSENT (`null` /
|
|
167
|
+
* `undefined`), and — separately — the value returned when a
|
|
168
|
+
* present predicate cannot be evaluated. ⚠️ Those are two
|
|
169
|
+
* questions this one parameter answers with one value; a
|
|
170
|
+
* caller that wants different answers must branch before the
|
|
171
|
+
* call, as {@link resolveFieldRuleState} does. The historic
|
|
172
|
+
* advice is to pick the *safe* default (`false` for
|
|
173
|
+
* readonly/required — don't lock/block on error; `true` for
|
|
174
|
+
* visibility — don't hide on error). ADR-0137 kept those
|
|
175
|
+
* directions for RENDER (D3) and put the answer to a broken
|
|
176
|
+
* field rule at SUBMIT (D2) — see {@link FieldRuleFaults}.
|
|
101
177
|
* @param previous The prior persisted record, if any (for `previous.*` refs).
|
|
102
178
|
* @param scope Extra top-level scope variables bound alongside `record` —
|
|
103
179
|
* e.g. `{ parent }` so an inline line-item cell can reference
|
|
@@ -109,30 +185,68 @@ function warnPredicateFailure(expr, fallback, reason, context) {
|
|
|
109
185
|
* is unaffected either way.
|
|
110
186
|
*/
|
|
111
187
|
export function evalFieldPredicate(pred, record, fallback, previous, scope, diagnostic) {
|
|
112
|
-
if (pred == null
|
|
188
|
+
if (pred == null)
|
|
113
189
|
return fallback;
|
|
114
190
|
const expr = toExpression(pred);
|
|
115
|
-
// The
|
|
116
|
-
// engine's "never throws" contract — converge on one reason
|
|
117
|
-
// reporting site below, so the built-in warning and the
|
|
118
|
-
// can never describe the failure differently.
|
|
191
|
+
// The fault sources — a blank predicate, a not-ok verdict, and a throw that
|
|
192
|
+
// slipped past the engine's "never throws" contract — converge on one reason
|
|
193
|
+
// string and one reporting site below, so the built-in warning and the
|
|
194
|
+
// `onFault` passback can never describe the failure differently.
|
|
119
195
|
let reason;
|
|
120
196
|
let value = fallback;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
value = res.value === true;
|
|
197
|
+
if (isBlankPredicateText(pred)) {
|
|
198
|
+
// Declared-and-blank, in EITHER spelling. Answered here rather than by the
|
|
199
|
+
// engine for two reasons: the bare-string spelling never reached the engine
|
|
200
|
+
// at all (this line used to `return fallback` before any warning could
|
|
201
|
+
// fire — objectui#8069), and the envelope spelling reached it and came back
|
|
202
|
+
// with `AST-only evaluation not yet supported; persist \`source\`` for
|
|
203
|
+
// `{ source: '' }`, a reason that sends the author looking for a
|
|
204
|
+
// serialization bug. `isBlankPredicateText` is the repo's one definition of
|
|
205
|
+
// this question (objectui#3960) — a fourth local `trim()` here is exactly
|
|
206
|
+
// the drift it was consolidated to stop.
|
|
207
|
+
reason = BLANK_PREDICATE_REASON;
|
|
133
208
|
}
|
|
134
|
-
|
|
135
|
-
|
|
209
|
+
else if (isUnevaluablePredicate(pred)) {
|
|
210
|
+
// Declared, and nothing here can be evaluated: an envelope with no string
|
|
211
|
+
// `source` (an `ast`-only one), a number, `{}`, an array (objectui#11358).
|
|
212
|
+
// Answered here, beside the blank, for the blank's reasons: there is no
|
|
213
|
+
// `source` to hand the engine, and the engine's answer for one —
|
|
214
|
+
// `AST-only evaluation not yet supported; persist \`source\`` — was what
|
|
215
|
+
// a `0` reported too, which sends its author looking for a serializer.
|
|
216
|
+
// One reason, one tag, the same two channels.
|
|
217
|
+
reason = UNEVALUABLE_PREDICATE_REASON;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
try {
|
|
221
|
+
// objectui#4421 — the acting subject's effective object permissions, the
|
|
222
|
+
// one input `current_user.can(object, verb)` is answered from. They ride
|
|
223
|
+
// on the subject the scope already carries (`subjectPermissions.ts`),
|
|
224
|
+
// never under a scope key, because every key of `scope` is a CEL root
|
|
225
|
+
// and the engine keeps this map out of the variable namespace. A subject
|
|
226
|
+
// with none passes none, and `can` then refuses loudly: that fault goes
|
|
227
|
+
// through the caller's `fallback` like any other.
|
|
228
|
+
const permissions = subjectPermissionsOf(scope?.current_user);
|
|
229
|
+
// objectui#13006 — the subject's held system capabilities, the one input
|
|
230
|
+
// `current_user.hasCapability(name)` is answered from, carried the same
|
|
231
|
+
// way and for the same reason; absent stays absent, so the call refuses.
|
|
232
|
+
const systemPermissions = subjectCapabilitiesOf(scope?.current_user);
|
|
233
|
+
const res = ExpressionEngine.evaluate(expr, {
|
|
234
|
+
record,
|
|
235
|
+
previous,
|
|
236
|
+
...(scope ? { extra: scope } : {}),
|
|
237
|
+
...(permissions !== undefined ? { permissions } : {}),
|
|
238
|
+
...(systemPermissions !== undefined ? { systemPermissions } : {}),
|
|
239
|
+
});
|
|
240
|
+
// Parse error, type error, unbound identifier, engine fault … — every
|
|
241
|
+
// not-ok verdict resolves to the fallback, but never silently (objectstack-ai/objectstack#5149).
|
|
242
|
+
if (!res.ok)
|
|
243
|
+
reason = `[${res.error.kind}] ${res.error.message}`;
|
|
244
|
+
else
|
|
245
|
+
value = res.value === true;
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
reason = `[throw] ${err instanceof Error ? err.message : String(err)}`;
|
|
249
|
+
}
|
|
136
250
|
}
|
|
137
251
|
if (reason !== undefined) {
|
|
138
252
|
if (diagnostic?.warn !== false) {
|
|
@@ -145,6 +259,60 @@ export function evalFieldPredicate(pred, record, fallback, previous, scope, diag
|
|
|
145
259
|
}
|
|
146
260
|
return value;
|
|
147
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* The verdict {@link resolveFieldRuleState} applies when a rule's predicate
|
|
264
|
+
* CANNOT BE EVALUATED — parse error, unbound identifier, engine fault, or a
|
|
265
|
+
* predicate the author left blank.
|
|
266
|
+
*
|
|
267
|
+
* ⚠️ These answer **"what should the form do when this rule is BROKEN?"** —
|
|
268
|
+
* *not* "what should the form do when the author declared NO rule?" That
|
|
269
|
+
* second question is answered separately, by the `…_ABSENT` set below. The two
|
|
270
|
+
* sets hold pairwise EQUAL values today, and that equality is a copy, not an
|
|
271
|
+
* implication: each `rules.<key> != null` ternary answers the absent case with
|
|
272
|
+
* its own literal and hands `evalFieldPredicate` the same literal as the fault
|
|
273
|
+
* fallback beside it, so every permissive value is written twice and the "it
|
|
274
|
+
* broke" answer was chosen by aligning it with the "it is absent" answer next
|
|
275
|
+
* to it. Nothing was *inherited* from a shared default — `evalFieldPredicate`'s
|
|
276
|
+
* `fallback` parameter is required and has none (objectui#8069 measured both
|
|
277
|
+
* halves).
|
|
278
|
+
*
|
|
279
|
+
* All three directions are the PERMISSIVE one, so a single mistyped column in
|
|
280
|
+
* one authored predicate yields a form that shows more, locks less and demands
|
|
281
|
+
* less — three faults from one typo, composing rather than cancelling.
|
|
282
|
+
* ADR-0137 answered that without moving any of the three: they stay the RENDER
|
|
283
|
+
* direction (D3 — a faulting `visibleWhen` shows the field), and the composite
|
|
284
|
+
* is closed at SUBMIT instead (D2), from the `faults` report
|
|
285
|
+
* {@link resolveFieldRuleState} returns beside the verdicts.
|
|
286
|
+
*
|
|
287
|
+
* **What the history records** (read on full, unshallowed history — a shallow
|
|
288
|
+
* clone answers this with one commit and no warning): the direction *was*
|
|
289
|
+
* reasoned about, once, when the helper landed (objectui#1578). This module's
|
|
290
|
+
* head and ADR-0036 both record the same rationale — the fallbacks are "chosen
|
|
291
|
+
* so a fault is *safe*: `true` for visibility (don't hide content on error),
|
|
292
|
+
* `false` for required/readonly (don't block submit or lock a field on
|
|
293
|
+
* error)". What no commit records is the COMPOSITION: every recorded argument
|
|
294
|
+
* is per-key, and the case where all three faults arrive from one typo was
|
|
295
|
+
* never put. objectstack#5149 then removed the SILENCE and left the direction
|
|
296
|
+
* explicitly undecided ("appeal 1").
|
|
297
|
+
*
|
|
298
|
+
* ⛔ These values are shipped behaviour, declared by ADR-0137 D3. Changing one
|
|
299
|
+
* is not a refactor — and `6a449fc49` leans on the `visibleWhen` half staying
|
|
300
|
+
* fail-open (a broken predicate must never silently null a stored column).
|
|
301
|
+
*/
|
|
302
|
+
const VISIBLE_WHEN_FAULTED = true;
|
|
303
|
+
const READONLY_WHEN_FAULTED = false;
|
|
304
|
+
const REQUIRED_WHEN_FAULTED = false;
|
|
305
|
+
/**
|
|
306
|
+
* The verdict {@link resolveFieldRuleState} applies when the author declared NO
|
|
307
|
+
* rule for that key at all — the `: <literal>` arm of each `!= null` ternary.
|
|
308
|
+
*
|
|
309
|
+
* Spelled apart from the `…_FAULTED` set above because they answer a different
|
|
310
|
+
* question, not because they differ: changing one of THESE changes what an
|
|
311
|
+
* unconditional field does, which is a louder and quite separate decision.
|
|
312
|
+
*/
|
|
313
|
+
const VISIBLE_WHEN_ABSENT = true;
|
|
314
|
+
const READONLY_WHEN_ABSENT = false;
|
|
315
|
+
const REQUIRED_WHEN_ABSENT = false;
|
|
148
316
|
/**
|
|
149
317
|
* Resolve the effective `{ visible, readonly, required }` state for a field
|
|
150
318
|
* given its conditional rules and the live record. Each `*When` rule, when
|
|
@@ -174,18 +342,27 @@ export function evalFieldPredicate(pred, record, fallback, previous, scope, diag
|
|
|
174
342
|
* `"field 'amount'"`. The warning then reads
|
|
175
343
|
* `visibleWhen of field 'amount' …`; without it, the rule
|
|
176
344
|
* kind alone is reported.
|
|
345
|
+
* @returns The three verdicts, plus `faults` — see {@link FieldRuleFaults} for
|
|
346
|
+
* what a submit path reads from it and what it leaves out.
|
|
177
347
|
*/
|
|
178
348
|
export function resolveFieldRuleState(rules, record, statics, previous, scope, fieldContext) {
|
|
349
|
+
const faults = {};
|
|
350
|
+
// `onFault` fires beside the built-in warning, not instead of it (`warn`
|
|
351
|
+
// stays on), so recording the report costs no diagnostic and no second
|
|
352
|
+
// engine call.
|
|
179
353
|
const diag = (rule) => ({
|
|
180
354
|
context: fieldContext ? `${rule} of ${fieldContext}` : rule,
|
|
355
|
+
onFault: (reason) => {
|
|
356
|
+
faults[rule] = reason;
|
|
357
|
+
},
|
|
181
358
|
});
|
|
182
359
|
const visible = rules.visibleWhen != null
|
|
183
|
-
? evalFieldPredicate(rules.visibleWhen, record,
|
|
184
|
-
:
|
|
360
|
+
? evalFieldPredicate(rules.visibleWhen, record, VISIBLE_WHEN_FAULTED, previous, scope, diag('visibleWhen'))
|
|
361
|
+
: VISIBLE_WHEN_ABSENT;
|
|
185
362
|
const readonly = statics.readonly === true ||
|
|
186
363
|
(rules.readonlyWhen != null
|
|
187
|
-
? evalFieldPredicate(rules.readonlyWhen, record,
|
|
188
|
-
:
|
|
364
|
+
? evalFieldPredicate(rules.readonlyWhen, record, READONLY_WHEN_FAULTED, previous, scope, diag('readonlyWhen'))
|
|
365
|
+
: READONLY_WHEN_ABSENT);
|
|
189
366
|
// Short-circuited, not evaluated-and-discarded: the verdict cannot depend on
|
|
190
367
|
// the predicate, so running it would only spend an engine call per field per
|
|
191
368
|
// render. A broken `requiredWhen` still warns wherever it decides something
|
|
@@ -195,7 +372,7 @@ export function resolveFieldRuleState(rules, record, statics, previous, scope, f
|
|
|
195
372
|
? false
|
|
196
373
|
: statics.required === true ||
|
|
197
374
|
(rules.requiredWhen != null
|
|
198
|
-
? evalFieldPredicate(rules.requiredWhen, record,
|
|
199
|
-
:
|
|
200
|
-
return { visible, readonly, required };
|
|
375
|
+
? evalFieldPredicate(rules.requiredWhen, record, REQUIRED_WHEN_FAULTED, previous, scope, diag('requiredWhen'))
|
|
376
|
+
: REQUIRED_WHEN_ABSENT);
|
|
377
|
+
return { visible, readonly, required, faults };
|
|
201
378
|
}
|
|
@@ -10,8 +10,10 @@ export * from './ExpressionEvaluator.js';
|
|
|
10
10
|
export * from './predicateInput.js';
|
|
11
11
|
export * from './declaredPredicate.js';
|
|
12
12
|
export * from './fieldRules.js';
|
|
13
|
+
export * from './subjectPermissions.js';
|
|
13
14
|
export * from './rowPredicateCanon.js';
|
|
14
15
|
export * from './listConditional.js';
|
|
16
|
+
export * from './deadlineSettled.js';
|
|
15
17
|
export * from './optionRules.js';
|
|
16
18
|
export * from './optionLint.js';
|
|
17
19
|
export * from './ExpressionCache.js';
|
package/dist/evaluator/index.js
CHANGED
|
@@ -10,8 +10,10 @@ export * from './ExpressionEvaluator.js';
|
|
|
10
10
|
export * from './predicateInput.js';
|
|
11
11
|
export * from './declaredPredicate.js';
|
|
12
12
|
export * from './fieldRules.js';
|
|
13
|
+
export * from './subjectPermissions.js';
|
|
13
14
|
export * from './rowPredicateCanon.js';
|
|
14
15
|
export * from './listConditional.js';
|
|
16
|
+
export * from './deadlineSettled.js';
|
|
15
17
|
export * from './optionRules.js';
|
|
16
18
|
export * from './optionLint.js';
|
|
17
19
|
export * from './ExpressionCache.js';
|
|
@@ -44,8 +44,10 @@ export interface RowPredicateOptions {
|
|
|
44
44
|
* row-action `visible`/`disabled`; `false` (no style) for formatting. */
|
|
45
45
|
fallback?: boolean;
|
|
46
46
|
/** Extra top-level scope merged alongside the row — e.g. the global predicate
|
|
47
|
-
* scope (`features` / `user`
|
|
48
|
-
* collision: a `record`
|
|
47
|
+
* scope (`features` / `user`) a host shell provides. The row wins on
|
|
48
|
+
* collision: a `record` key here never shadows the row. Every OTHER key —
|
|
49
|
+
* a host's own `data` included — reaches the predicate as the host's own
|
|
50
|
+
* (`83fe6e741`: `data` no longer names the row on a record surface). */
|
|
49
51
|
scope?: Record<string, unknown>;
|
|
50
52
|
/** When true, log a one-time warning if a *present* predicate faults. */
|
|
51
53
|
warnOnError?: boolean;
|
|
@@ -53,16 +55,16 @@ export interface RowPredicateOptions {
|
|
|
53
55
|
label?: string;
|
|
54
56
|
/**
|
|
55
57
|
* This surface has **no row of its own** — bind NOTHING for the row instead
|
|
56
|
-
* of binding an empty one, so a `record`
|
|
57
|
-
*
|
|
58
|
+
* of binding an empty one, so a `record` the HOST SCOPE carries survives to
|
|
59
|
+
* the predicate (objectui#4640).
|
|
58
60
|
*
|
|
59
61
|
* The default (`false`) is this function's whole subject rule: the row is
|
|
60
|
-
* pinned over the scope
|
|
61
|
-
*
|
|
62
|
+
* pinned over the scope as `record`, on both dialect paths, so `record`
|
|
63
|
+
* always names THIS row even when the host scope carries a key of that name
|
|
62
64
|
* (objectui#3796). That is right for every row surface — and exactly wrong
|
|
63
65
|
* for a surface that has no row, because the pin then writes an EMPTY
|
|
64
|
-
* `record`
|
|
65
|
-
*
|
|
66
|
+
* `record` over the host's real one and every `record.*` predicate faults
|
|
67
|
+
* with "No such key". "This surface has no row of its own" and "this
|
|
66
68
|
* surface's row is empty" are different facts; only the latter is entitled to
|
|
67
69
|
* shadow the scope. Same distinction, same words, as `usePredicateRecordContext`
|
|
68
70
|
* in `@object-ui/react` (objectui#4075) — which is the BINDING half of this
|
|
@@ -86,38 +88,105 @@ export interface RowPredicateOptions {
|
|
|
86
88
|
}
|
|
87
89
|
/**
|
|
88
90
|
* Evaluate a single boolean predicate against a row record on the canonical CEL
|
|
89
|
-
* engine (with a legacy-dialect fallback — see the module note). The row
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* `
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* the
|
|
114
|
-
*
|
|
91
|
+
* engine (with a legacy-dialect fallback — see the module note). The row is
|
|
92
|
+
* bound ONE way: as `record.*` — **the canon** (maintainer ruling 2026-08-20 on
|
|
93
|
+
* objectui#5330, option B; Phase 2 executed by `83fe6e741`).
|
|
94
|
+
*
|
|
95
|
+
* Until Phase 2 the row was also bound as bare fields (`status`, the row-action
|
|
96
|
+
* shorthand) and as `data.*`, and Phase 1 (`d1ab06f0f`) warned once per
|
|
97
|
+
* non-canonical spelling. Both bindings and that warning are gone. A bare-field
|
|
98
|
+
* or `data.*` predicate on a record surface now FAULTS here exactly as it always
|
|
99
|
+
* did on the server — measured on `@objectstack/formula@17.1.0`,
|
|
100
|
+
* `buildScope({ record })` mounts exactly `['record']`, so a bare field faults
|
|
101
|
+
* `Unknown variable: status` and `data.*` faults `Unknown variable: data` — and
|
|
102
|
+
* takes this function's EXISTING fault policy: the caller's `fallback`, reported
|
|
103
|
+
* once by `warnEvalError` (when `warnOnError` is set) or by the canonical
|
|
104
|
+
* helper's own one-time warning, either of which names the unknown variable.
|
|
105
|
+
* Nothing detects a retired spelling on this path; it is simply unbound. The
|
|
106
|
+
* same holds on the legacy `${…}` path below — one scope shape per surface,
|
|
107
|
+
* both dialects — so `${data.x}` / `${x}` strings on a row surface fault there
|
|
108
|
+
* too, and land in the same fallback / warning.
|
|
109
|
+
*
|
|
110
|
+
* The retirement is scoped to the RUNTIME RECORD layer. `data` is the canonical
|
|
111
|
+
* root one layer over, in a metadata-editing form (ADR-0089 D3,
|
|
112
|
+
* `CANONICAL_ROOT_BY_LAYER` = `{ runtime: 'record', metadata: 'data' }`), and
|
|
113
|
+
* that layer evaluates through its own entry (`app-shell`'s metadata-admin
|
|
114
|
+
* `predicate.ts`), never through this one. See {@link ./rowPredicateCanon.ts}
|
|
115
|
+
* for the canon statement, the server measurement and the offline detector.
|
|
116
|
+
*
|
|
117
|
+
* The optional `scope` (host predicate scope) is bound alongside so
|
|
118
|
+
* `features.*` / `user.*` predicates keep working — but the row is the subject:
|
|
119
|
+
* `record` always names THIS row, on both dialect paths, even when the host
|
|
120
|
+
* scope carries a key of that name (objectui#3796). Every OTHER host key —
|
|
121
|
+
* a host's own `data` included — reaches the predicate as the host's own.
|
|
115
122
|
*
|
|
116
123
|
* A caller with no row at all passes {@link RowPredicateOptions.rowless}, and
|
|
117
124
|
* then nothing is bound over the scope — see that option for why an absent row
|
|
118
125
|
* must not be spelled as an empty one.
|
|
119
126
|
*/
|
|
120
127
|
export declare function evalRowPredicate(pred: FieldRulePredicate | undefined | null, row: Record<string, unknown> | null | undefined, opts?: RowPredicateOptions): boolean;
|
|
128
|
+
/** The two halves of a {@link partitionRowsByPredicate} verdict over a selection. */
|
|
129
|
+
export interface RowPartition<TRow> {
|
|
130
|
+
/** Records whose predicate passed — the ones an operation may act on. */
|
|
131
|
+
eligible: TRow[];
|
|
132
|
+
/**
|
|
133
|
+
* How many records were filtered out. A caller that shrinks a user's
|
|
134
|
+
* selection owes them this number: a run over fewer records than they picked
|
|
135
|
+
* must say so rather than quietly shrinking.
|
|
136
|
+
*/
|
|
137
|
+
skipped: number;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Split a set of records by one per-record predicate — the SET-shaped
|
|
141
|
+
* counterpart of {@link evalRowPredicate}, and the reason a bulk gate never
|
|
142
|
+
* needs a hook.
|
|
143
|
+
*
|
|
144
|
+
* A bulk affordance spans N records, so its gate is a **loop**, and React
|
|
145
|
+
* forbids calling `useRowPredicate` (or any hook) per iteration. Every bulk
|
|
146
|
+
* surface therefore reaches for `evalRowPredicate` directly; this wrapper is
|
|
147
|
+
* that loop written once, with the three cases each caller was otherwise
|
|
148
|
+
* re-deriving:
|
|
149
|
+
*
|
|
150
|
+
* - **absent** (`null` / `undefined` / `''`) — nothing is gated, so `rows` is
|
|
151
|
+
* returned BY REFERENCE and `skipped` is 0. The common case allocates
|
|
152
|
+
* nothing and a caller's downstream memo still holds. Whitespace-only text
|
|
153
|
+
* and an envelope whose `source` is blank are NOT absent here: they are
|
|
154
|
+
* evaluated, and fail closed like any other predicate that cannot run.
|
|
155
|
+
* - **boolean** — a verdict, not an expression, short-circuited exactly as
|
|
156
|
+
* `useCondition` / `useRowPredicate` do. Handing `true` to the engine
|
|
157
|
+
* produces `{ dialect: 'cel', source: undefined }`, which faults and — on
|
|
158
|
+
* this fail-closed path — would disqualify every record, so the most
|
|
159
|
+
* explicit way to say "always" would exclude everyone (objectui#3492).
|
|
160
|
+
* - **expression** — evaluated once per record with that record in scope,
|
|
161
|
+
* **fail-closed**: a predicate that faults must not hand the caller records
|
|
162
|
+
* it was written to exclude. `warnOnError` (default `true`) makes the
|
|
163
|
+
* resulting exclusion diagnosable once per predicate instead of silent.
|
|
164
|
+
*
|
|
165
|
+
* The fail-closed default is what separates this from a lenient record-free
|
|
166
|
+
* evaluation, and the difference is not mere strictness: evaluated with NO
|
|
167
|
+
* record bound, `record.status != 'paid'` returns `true` for every row —
|
|
168
|
+
* including the ones it was written to exclude — so an authored gate is not
|
|
169
|
+
* weakened, it is inverted for half its inputs (objectui#3067).
|
|
170
|
+
*
|
|
171
|
+
* ## Who calls it, and who decides "is a gate declared?" (objectui#11322)
|
|
172
|
+
*
|
|
173
|
+
* This is a shared partitioner, and its opening test is not the action
|
|
174
|
+
* family's "is a gate declared?" question. Its callers:
|
|
175
|
+
*
|
|
176
|
+
* - the bulk ACTION fold, plugin-grid's `partitionBulkRows` (a bulk def's
|
|
177
|
+
* `visible`). The action family decides "declared?" BEFORE calling here,
|
|
178
|
+
* through `hasDeclaredVisibilityGate`, and hands an undeclared gate over as
|
|
179
|
+
* `undefined`. So a whitespace-only `visible` or a blank-`source` envelope
|
|
180
|
+
* never reaches the test below from that caller.
|
|
181
|
+
* - the built-in Delete's `userActions.delete.visibleWhen`, a field-rule key,
|
|
182
|
+
* from plugin-grid's `ObjectGrid` and from plugin-list's `ListView`
|
|
183
|
+
* (`bulkDeleteEligibility`). The test below is THEIR answer: a blank one
|
|
184
|
+
* that is not `''` is evaluated and excludes every record.
|
|
185
|
+
*
|
|
186
|
+
* A caller that needs the action family's answer asks it first; it does not
|
|
187
|
+
* write a second blank test in front of this one.
|
|
188
|
+
*/
|
|
189
|
+
export declare function partitionRowsByPredicate<TRow extends Record<string, unknown>>(pred: FieldRulePredicate | boolean | undefined | null, rows: readonly TRow[], opts?: Omit<RowPredicateOptions, 'fallback' | 'rowless'>): RowPartition<TRow>;
|
|
121
190
|
/** A conditional-formatting rule in any of the three historical shapes. */
|
|
122
191
|
export type ConditionalFormattingRuleLike = {
|
|
123
192
|
/** Spec shape: a CEL predicate (`ExpressionInputSchema`). */
|