@object-ui/core 17.5.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 +7404 -0
- package/README.md +127 -26
- package/dist/actions/ActionEngine.d.ts +5 -3
- package/dist/actions/ActionEngine.js +14 -10
- package/dist/actions/ActionRunner.d.ts +363 -30
- package/dist/actions/ActionRunner.js +544 -58
- package/dist/actions/TransactionManager.d.ts +7 -2
- package/dist/actions/actionKeys.d.ts +75 -13
- package/dist/actions/actionKeys.js +131 -16
- package/dist/actions/actionResponse.js +3 -3
- package/dist/actions/index.d.ts +2 -0
- package/dist/actions/index.js +2 -0
- package/dist/actions/recordDelete.d.ts +118 -0
- package/dist/actions/recordDelete.js +112 -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/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 +128 -1
- package/dist/evaluator/ExpressionEvaluator.js +226 -13
- package/dist/evaluator/FormulaFunctions.d.ts +0 -10
- package/dist/evaluator/FormulaFunctions.js +166 -27
- package/dist/evaluator/SafeExpressionParser.js +0 -4
- package/dist/evaluator/declaredPredicate.d.ts +93 -9
- package/dist/evaluator/declaredPredicate.js +118 -17
- package/dist/evaluator/fieldRules.d.ts +107 -4
- package/dist/evaluator/fieldRules.js +231 -35
- package/dist/evaluator/index.d.ts +2 -0
- package/dist/evaluator/index.js +2 -0
- package/dist/evaluator/listConditional.d.ts +125 -9
- package/dist/evaluator/listConditional.js +167 -26
- package/dist/evaluator/optionRules.d.ts +126 -0
- package/dist/evaluator/optionRules.js +141 -4
- package/dist/evaluator/predicateInput.d.ts +15 -4
- package/dist/evaluator/predicateInput.js +35 -2
- package/dist/evaluator/rowPredicateCanon.d.ts +56 -0
- package/dist/evaluator/rowPredicateCanon.js +158 -0
- 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 +18 -0
- package/dist/index.js +97 -2
- package/dist/protocols/index.d.ts +0 -1
- package/dist/protocols/index.js +9 -1
- package/dist/registry/Registry.d.ts +397 -48
- 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/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-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 +188 -0
- package/dist/utils/chart-presentation.js +345 -0
- package/dist/utils/chart-series.d.ts +271 -31
- package/dist/utils/chart-series.js +478 -55
- package/dist/utils/column-identity.d.ts +43 -0
- package/dist/utils/column-identity.js +47 -1
- 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 +407 -52
- 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/debug.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 +171 -11
- package/dist/utils/expand-fields.js +171 -11
- 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 -34
- package/dist/utils/freeze-schema.d.ts +38 -1
- package/dist/utils/freeze-schema.js +10 -0
- package/dist/utils/grouping-fields.d.ts +61 -0
- package/dist/utils/grouping-fields.js +84 -0
- package/dist/utils/humanize-label.d.ts +60 -0
- package/dist/utils/humanize-label.js +62 -0
- package/dist/utils/managedBy.d.ts +30 -5
- 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 +131 -1
- package/dist/utils/normalize-list-view.js +248 -6
- package/dist/utils/number-display.d.ts +70 -17
- 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 +62 -4
- package/dist/utils/predicate-fields.js +169 -4
- 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/retired-field-types.d.ts +90 -0
- package/dist/utils/retired-field-types.js +124 -0
- package/dist/utils/sort-query.d.ts +128 -26
- 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/unmaterialized-fields.d.ts +52 -0
- package/dist/utils/unmaterialized-fields.js +57 -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 +9 -4
- package/dist/validation/index.js +9 -4
- package/dist/validation/schema-validator.d.ts +1 -1
- package/dist/validation/schema-validator.js +305 -82
- 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.d.ts +1 -1
- package/dist/validation/validators/object-validation-engine.js +14 -5
- package/package.json +8 -9
- package/dist/__benchmarks__/core.bench.d.ts +0 -8
- package/dist/__benchmarks__/core.bench.js +0 -53
- 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
|
@@ -62,7 +62,14 @@ function warnLegacyDialect(source) {
|
|
|
62
62
|
}
|
|
63
63
|
const warnedError = new Set();
|
|
64
64
|
/**
|
|
65
|
-
* One-time
|
|
65
|
+
* One-time report for a predicate that could not be evaluated.
|
|
66
|
+
*
|
|
67
|
+
* The message names the caller's `fallback` as "its safe default" without
|
|
68
|
+
* asserting a direction, because the direction is the caller's: row surfaces
|
|
69
|
+
* fail CLOSED (a faulting `visible` hides the action), while a param-collection
|
|
70
|
+
* dialog fails OPEN (objectui#4640 — a silently hidden required param is the
|
|
71
|
+
* undiagnosable failure). Both are loud; only silence was ever ruled out
|
|
72
|
+
* (2026-08-06 on objectui#4051 / objectstack#5149).
|
|
66
73
|
*
|
|
67
74
|
* `reason` is the engine's own description of the failure (`"[runtime] No such
|
|
68
75
|
* key: owner_id"`) — the single most useful line for the author, and precisely
|
|
@@ -111,7 +118,7 @@ function evalCel(pred, record, scope) {
|
|
|
111
118
|
// verbatim — no second engine — so CEL semantics never drift from B2.
|
|
112
119
|
// `warn: false`: this caller reports the fault itself (the labelled
|
|
113
120
|
// `warnEvalError` in `evalRowPredicate`) — one warning per broken predicate,
|
|
114
|
-
// not two (#5149). `onFault` carries the engine's reason across that silence
|
|
121
|
+
// not two (objectstack-ai/objectstack#5149). `onFault` carries the engine's reason across that silence
|
|
115
122
|
// so the labelled report can still name it (objectui#3792); both probes fault
|
|
116
123
|
// identically, so the first reason is the reason.
|
|
117
124
|
let reason;
|
|
@@ -127,38 +134,99 @@ function evalCel(pred, record, scope) {
|
|
|
127
134
|
}
|
|
128
135
|
/**
|
|
129
136
|
* Evaluate a single boolean predicate against a row record on the canonical CEL
|
|
130
|
-
* engine (with a legacy-dialect fallback — see the module note). The row
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
+
* engine (with a legacy-dialect fallback — see the module note). The row is
|
|
138
|
+
* bound ONE way: as `record.*` — **the canon** (maintainer ruling 2026-08-20 on
|
|
139
|
+
* objectui#5330, option B; Phase 2 executed by `83fe6e741`).
|
|
140
|
+
*
|
|
141
|
+
* Until Phase 2 the row was also bound as bare fields (`status`, the row-action
|
|
142
|
+
* shorthand) and as `data.*`, and Phase 1 (`d1ab06f0f`) warned once per
|
|
143
|
+
* non-canonical spelling. Both bindings and that warning are gone. A bare-field
|
|
144
|
+
* or `data.*` predicate on a record surface now FAULTS here exactly as it always
|
|
145
|
+
* did on the server — measured on `@objectstack/formula@17.1.0`,
|
|
146
|
+
* `buildScope({ record })` mounts exactly `['record']`, so a bare field faults
|
|
147
|
+
* `Unknown variable: status` and `data.*` faults `Unknown variable: data` — and
|
|
148
|
+
* takes this function's EXISTING fault policy: the caller's `fallback`, reported
|
|
149
|
+
* once by `warnEvalError` (when `warnOnError` is set) or by the canonical
|
|
150
|
+
* helper's own one-time warning, either of which names the unknown variable.
|
|
151
|
+
* Nothing detects a retired spelling on this path; it is simply unbound. The
|
|
152
|
+
* same holds on the legacy `${…}` path below — one scope shape per surface,
|
|
153
|
+
* both dialects — so `${data.x}` / `${x}` strings on a row surface fault there
|
|
154
|
+
* too, and land in the same fallback / warning.
|
|
155
|
+
*
|
|
156
|
+
* The retirement is scoped to the RUNTIME RECORD layer. `data` is the canonical
|
|
157
|
+
* root one layer over, in a metadata-editing form (ADR-0089 D3,
|
|
158
|
+
* `CANONICAL_ROOT_BY_LAYER` = `{ runtime: 'record', metadata: 'data' }`), and
|
|
159
|
+
* that layer evaluates through its own entry (`app-shell`'s metadata-admin
|
|
160
|
+
* `predicate.ts`), never through this one. See {@link ./rowPredicateCanon.ts}
|
|
161
|
+
* for the canon statement, the server measurement and the offline detector.
|
|
162
|
+
*
|
|
163
|
+
* The optional `scope` (host predicate scope) is bound alongside so
|
|
164
|
+
* `features.*` / `user.*` predicates keep working — but the row is the subject:
|
|
165
|
+
* `record` always names THIS row, on both dialect paths, even when the host
|
|
166
|
+
* scope carries a key of that name (objectui#3796). Every OTHER host key —
|
|
167
|
+
* a host's own `data` included — reaches the predicate as the host's own.
|
|
168
|
+
*
|
|
169
|
+
* A caller with no row at all passes {@link RowPredicateOptions.rowless}, and
|
|
170
|
+
* then nothing is bound over the scope — see that option for why an absent row
|
|
171
|
+
* must not be spelled as an empty one.
|
|
137
172
|
*/
|
|
138
173
|
export function evalRowPredicate(pred, row, opts = {}) {
|
|
139
174
|
const fallback = opts.fallback ?? false;
|
|
140
175
|
if (pred == null)
|
|
141
176
|
return fallback;
|
|
177
|
+
// A BLANK predicate is not answered here. It used to be, for the bare-string
|
|
178
|
+
// spelling only: `''` / whitespace returned the fallback on this line, in
|
|
179
|
+
// silence, while a blank ENVELOPE went on to `evalFieldPredicate`'s `[blank]`
|
|
180
|
+
// report below (objectui#11262, ADR-0137 D4: a blank gate predicate is
|
|
181
|
+
// "diagnosed, never a silent `true`"). Both spellings now take that one
|
|
182
|
+
// route: no legacy marker can match blank text, so a blank string reaches
|
|
183
|
+
// the CEL path like its envelope twin and gets the same verdict — the
|
|
184
|
+
// caller's fallback — and the same report, labelled on the fail-closed route
|
|
185
|
+
// and by the canonical helper on the single-eval one.
|
|
142
186
|
const source = typeof pred === 'string' ? pred : undefined;
|
|
143
|
-
if (source !== undefined && !source.trim())
|
|
144
|
-
return fallback;
|
|
145
187
|
// Relations collapse to the foreign key the server stores, so `record.owner`
|
|
146
188
|
// means one thing whether or not this surface expanded the column
|
|
147
189
|
// (objectui#3501). Returned by reference when there is nothing to collapse.
|
|
148
|
-
|
|
149
|
-
|
|
190
|
+
// A `rowless` caller has no row to collapse, so `opts.fields` is moot there.
|
|
191
|
+
const rowObj = opts.rowless
|
|
192
|
+
? {}
|
|
193
|
+
: toPredicateRecord(row && typeof row === 'object' ? row : {}, opts.fields);
|
|
194
|
+
// `record.*` + the host scope, all top-level. The row is bound ONE way
|
|
195
|
+
// (`83fe6e741`, Phase 2 of the objectui#5330 canon): no bare-field spread
|
|
196
|
+
// and no `data` — a predicate spelled either way is simply unbound here and
|
|
197
|
+
// faults, on both dialect paths, exactly as it does on the server.
|
|
150
198
|
//
|
|
151
|
-
// `
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
// deliberately (objectui#3796). Pinning here fixes both paths at the merge,
|
|
199
|
+
// `record` is pinned AFTER the spread, so a host scope that happens to carry
|
|
200
|
+
// that key is background and the ROW stays the subject of this function — on
|
|
201
|
+
// BOTH dialect paths. Before objectui#3796 it relied instead on each engine's
|
|
202
|
+
// own binding, which disagreed: the legacy evaluator re-pinned `record` (row
|
|
203
|
+
// won) while the CEL engine takes `extra` over its `record` binding (host
|
|
204
|
+
// scope won). Same function, same predicate text, opposite subjects — decided
|
|
205
|
+
// by whether the string happens to contain `===`/`${…}`, which no author is
|
|
206
|
+
// choosing deliberately. Pinning here fixes both paths at the merge,
|
|
160
207
|
// independently of either engine's precedence.
|
|
161
|
-
|
|
208
|
+
//
|
|
209
|
+
// …UNLESS the caller has no row (`rowless`), in which case there is nothing
|
|
210
|
+
// to pin and the host scope keeps its own `record` — see the option.
|
|
211
|
+
const scope = opts.rowless
|
|
212
|
+
? { ...(opts.scope ?? {}) }
|
|
213
|
+
: { ...(opts.scope ?? {}), record: rowObj };
|
|
214
|
+
// The predicate TEXT for diagnostics: a bare string is itself, an envelope is
|
|
215
|
+
// its `source`. Reported separately from `source` above, which is `undefined`
|
|
216
|
+
// for an envelope *by design* (it drives the dialect routing, and an envelope
|
|
217
|
+
// is never routed to the legacy engine). Reusing it for the warning is what
|
|
218
|
+
// made every faulting envelope report as the literal `"(expression)"` — no
|
|
219
|
+
// predicate text for the author, and, because the one-time key is
|
|
220
|
+
// (label, source), the FIRST faulting envelope on a given label silenced every
|
|
221
|
+
// other one under it (objectui#4640: measured on `evalRowPredicate` — a second
|
|
222
|
+
// envelope with a different, also-broken source warned zero times). The
|
|
223
|
+
// envelope is not an exotic spelling: `@objectstack/spec`'s
|
|
224
|
+
// `ExpressionInputSchema` normalizes every authored predicate into one, so it
|
|
225
|
+
// is the likeliest shape in served metadata and was the least diagnosable.
|
|
226
|
+
const predicateText = source ??
|
|
227
|
+
(pred && typeof pred === 'object' && typeof pred.source === 'string'
|
|
228
|
+
? pred.source
|
|
229
|
+
: '(expression)');
|
|
162
230
|
// Legacy-dialect *strings* route to the legacy engine (back-compat). The
|
|
163
231
|
// `{ dialect: 'cel', source }` envelope is never routed here — it is CEL.
|
|
164
232
|
if (source !== undefined && isLegacyDialectSource(source)) {
|
|
@@ -176,10 +244,17 @@ export function evalRowPredicate(pred, row, opts = {}) {
|
|
|
176
244
|
return fallback;
|
|
177
245
|
}
|
|
178
246
|
}
|
|
179
|
-
// CEL path
|
|
247
|
+
// CEL path — everything reaching here is CEL. No spelling detector runs here
|
|
248
|
+
// (`83fe6e741` removed the Phase-1 warning with the bindings): a retired
|
|
249
|
+
// bare-field / `data.*` spelling is unbound above and faults in the engine
|
|
250
|
+
// like any other unknown variable, and the fault report below is what names
|
|
251
|
+
// it. `detectNonCanonicalRowSpelling` stays exported for OFFLINE sweeps of
|
|
252
|
+
// authored metadata, not for this hot path.
|
|
253
|
+
//
|
|
254
|
+
// The fault-aware `evalCel` costs two evaluations (to tell a fault
|
|
180
255
|
// from a genuine `false`), so only pay it when a caller wants the labelled
|
|
181
256
|
// fail-closed warning — the formatting hot path takes the single-eval fast
|
|
182
|
-
// route. Since #5149 the fast route is no longer silent either: the
|
|
257
|
+
// route. Since objectstack-ai/objectstack#5149 the fast route is no longer silent either: the
|
|
183
258
|
// canonical helper itself warns once per broken predicate (with `opts.label`
|
|
184
259
|
// as the locator when given).
|
|
185
260
|
if (!opts.warnOnError) {
|
|
@@ -187,11 +262,77 @@ export function evalRowPredicate(pred, row, opts = {}) {
|
|
|
187
262
|
}
|
|
188
263
|
const verdict = evalCel(pred, rowObj, scope);
|
|
189
264
|
if (!verdict.ok) {
|
|
190
|
-
warnEvalError(
|
|
265
|
+
warnEvalError(predicateText, opts.label, verdict.reason);
|
|
191
266
|
return fallback;
|
|
192
267
|
}
|
|
193
268
|
return verdict.value;
|
|
194
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* Split a set of records by one per-record predicate — the SET-shaped
|
|
272
|
+
* counterpart of {@link evalRowPredicate}, and the reason a bulk gate never
|
|
273
|
+
* needs a hook.
|
|
274
|
+
*
|
|
275
|
+
* A bulk affordance spans N records, so its gate is a **loop**, and React
|
|
276
|
+
* forbids calling `useRowPredicate` (or any hook) per iteration. Every bulk
|
|
277
|
+
* surface therefore reaches for `evalRowPredicate` directly; this wrapper is
|
|
278
|
+
* that loop written once, with the three cases each caller was otherwise
|
|
279
|
+
* re-deriving:
|
|
280
|
+
*
|
|
281
|
+
* - **absent** (`null` / `undefined` / `''`) — nothing is gated, so `rows` is
|
|
282
|
+
* returned BY REFERENCE and `skipped` is 0. The common case allocates
|
|
283
|
+
* nothing and a caller's downstream memo still holds. Whitespace-only text
|
|
284
|
+
* and an envelope whose `source` is blank are NOT absent here: they are
|
|
285
|
+
* evaluated, and fail closed like any other predicate that cannot run.
|
|
286
|
+
* - **boolean** — a verdict, not an expression, short-circuited exactly as
|
|
287
|
+
* `useCondition` / `useRowPredicate` do. Handing `true` to the engine
|
|
288
|
+
* produces `{ dialect: 'cel', source: undefined }`, which faults and — on
|
|
289
|
+
* this fail-closed path — would disqualify every record, so the most
|
|
290
|
+
* explicit way to say "always" would exclude everyone (objectui#3492).
|
|
291
|
+
* - **expression** — evaluated once per record with that record in scope,
|
|
292
|
+
* **fail-closed**: a predicate that faults must not hand the caller records
|
|
293
|
+
* it was written to exclude. `warnOnError` (default `true`) makes the
|
|
294
|
+
* resulting exclusion diagnosable once per predicate instead of silent.
|
|
295
|
+
*
|
|
296
|
+
* The fail-closed default is what separates this from a lenient record-free
|
|
297
|
+
* evaluation, and the difference is not mere strictness: evaluated with NO
|
|
298
|
+
* record bound, `record.status != 'paid'` returns `true` for every row —
|
|
299
|
+
* including the ones it was written to exclude — so an authored gate is not
|
|
300
|
+
* weakened, it is inverted for half its inputs (objectui#3067).
|
|
301
|
+
*
|
|
302
|
+
* ## Who calls it, and who decides "is a gate declared?" (objectui#11322)
|
|
303
|
+
*
|
|
304
|
+
* This is a shared partitioner, and its opening test is not the action
|
|
305
|
+
* family's "is a gate declared?" question. Its callers:
|
|
306
|
+
*
|
|
307
|
+
* - the bulk ACTION fold, plugin-grid's `partitionBulkRows` (a bulk def's
|
|
308
|
+
* `visible`). The action family decides "declared?" BEFORE calling here,
|
|
309
|
+
* through `hasDeclaredVisibilityGate`, and hands an undeclared gate over as
|
|
310
|
+
* `undefined`. So a whitespace-only `visible` or a blank-`source` envelope
|
|
311
|
+
* never reaches the test below from that caller.
|
|
312
|
+
* - the built-in Delete's `userActions.delete.visibleWhen`, a field-rule key,
|
|
313
|
+
* from plugin-grid's `ObjectGrid` and from plugin-list's `ListView`
|
|
314
|
+
* (`bulkDeleteEligibility`). The test below is THEIR answer: a blank one
|
|
315
|
+
* that is not `''` is evaluated and excludes every record.
|
|
316
|
+
*
|
|
317
|
+
* A caller that needs the action family's answer asks it first; it does not
|
|
318
|
+
* write a second blank test in front of this one.
|
|
319
|
+
*/
|
|
320
|
+
export function partitionRowsByPredicate(pred, rows, opts = {}) {
|
|
321
|
+
if (pred == null || pred === '') {
|
|
322
|
+
return { eligible: rows, skipped: 0 };
|
|
323
|
+
}
|
|
324
|
+
if (typeof pred === 'boolean') {
|
|
325
|
+
return pred
|
|
326
|
+
? { eligible: rows, skipped: 0 }
|
|
327
|
+
: { eligible: [], skipped: rows.length };
|
|
328
|
+
}
|
|
329
|
+
const eligible = rows.filter(row => evalRowPredicate(pred, row, {
|
|
330
|
+
...opts,
|
|
331
|
+
warnOnError: opts.warnOnError ?? true,
|
|
332
|
+
fallback: false,
|
|
333
|
+
}));
|
|
334
|
+
return { eligible, skipped: rows.length - eligible.length };
|
|
335
|
+
}
|
|
195
336
|
/** Serialize a JS value as a CEL literal (string / number / bool / list / null). */
|
|
196
337
|
function celLiteral(v) {
|
|
197
338
|
if (v === null || v === undefined)
|
|
@@ -69,6 +69,68 @@ export declare function isOptionGroupGated(dependsOn: DependsOnInput, record: Re
|
|
|
69
69
|
* no predicate are always kept; a predicate that errors fails open (kept).
|
|
70
70
|
*/
|
|
71
71
|
export declare function resolveVisibleOptions<T extends OptionLike>(options: readonly T[] | undefined | null, record: Record<string, unknown>, scope?: Record<string, unknown>): T[];
|
|
72
|
+
/**
|
|
73
|
+
* The text an option widget SHOWS for one option: its `label`, or its `value`
|
|
74
|
+
* when the label is blank (`20b507aff`).
|
|
75
|
+
*
|
|
76
|
+
* ## Why a blank label reaches a renderer at all — it is a LEGAL document
|
|
77
|
+
*
|
|
78
|
+
* This is deliberately NOT the lenient renderer fallback AGENTS.md #0.1
|
|
79
|
+
* forbids. That rule governs metadata the contract REFUSES ("if the metadata
|
|
80
|
+
* is off-spec, fix it at the producer"); an empty label is metadata the
|
|
81
|
+
* contract ACCEPTS. Measured on the installed `@objectstack/spec` 17.6.0,
|
|
82
|
+
* `SelectOptionSchema`:
|
|
83
|
+
*
|
|
84
|
+
* { value: 'low', label: 'Low' } -> ACCEPT (lit control)
|
|
85
|
+
* { value: 'low', label: '' } -> ACCEPT <- the key reading
|
|
86
|
+
* { value: 'low' } -> REJECT invalid_type at [label]
|
|
87
|
+
*
|
|
88
|
+
* `@object-ui/types` mirrors that reading in prose on `SelectOptionBase`:
|
|
89
|
+
* "An empty string is a valid label; an ABSENT one is not." So the producer
|
|
90
|
+
* is right to emit `label: ''` — the field designer's `patchOptions` does,
|
|
91
|
+
* under the ruling `f0f774b0d` landed, and a pin
|
|
92
|
+
* (`ObjectFieldInspector.optionLabel.test.tsx`) fails if it ever invents
|
|
93
|
+
* content to fill the hole instead. A document every layer calls legal has to
|
|
94
|
+
* render as something a person can click.
|
|
95
|
+
*
|
|
96
|
+
* ## What the contract does NOT say, and who had already answered it
|
|
97
|
+
*
|
|
98
|
+
* The spec states no display default for a legal-but-blank label, so nothing
|
|
99
|
+
* upstream decides this. objectui had nevertheless already decided it — on
|
|
100
|
+
* HALF its read sites. Measured across the four option widgets before this
|
|
101
|
+
* helper existed, with `{ value: 'low', label: '' }` authored and
|
|
102
|
+
* `{ value: 'high', label: 'High' }` beside it as the lit control:
|
|
103
|
+
*
|
|
104
|
+
* | widget | readonly path | interactive path |
|
|
105
|
+
* | select | "low" | "" |
|
|
106
|
+
* | multiselect | "low" | "" |
|
|
107
|
+
* | radio | "low" | "" |
|
|
108
|
+
* | checkboxes | "low" | "" |
|
|
109
|
+
*
|
|
110
|
+
* Four summary paths spelled `opt?.label || value` and fell back; the four
|
|
111
|
+
* interactive paths spelled `{opt.label}` and rendered nothing. So the same
|
|
112
|
+
* option read "low" in a read-only form and blank in the editable one — the
|
|
113
|
+
* bug was never "tolerate or refuse", it was a widget family disagreeing with
|
|
114
|
+
* itself, and the blank half is the one a person has to click.
|
|
115
|
+
*
|
|
116
|
+
* This function is that decision written ONCE, the same "one definition, N
|
|
117
|
+
* surfaces" move as {@link CASCADE_OPTION_WIDGET_TYPES} below. All eight sites
|
|
118
|
+
* call it, so the two halves cannot drift apart again.
|
|
119
|
+
*
|
|
120
|
+
* ## Boundaries
|
|
121
|
+
*
|
|
122
|
+
* - Blank means TRIM-empty, so a whitespace-only label falls back too: it is
|
|
123
|
+
* the same invisible row to the person looking at it, and it is the
|
|
124
|
+
* spelling the producer's own reader already uses to call an authored
|
|
125
|
+
* `value` blank (`classifyOption`, `o.value.trim() === ''`).
|
|
126
|
+
* - `label` is typed required, but this reads defensively because renderers
|
|
127
|
+
* are handed unvalidated runtime metadata: a non-string label must fall back
|
|
128
|
+
* rather than throw where `{opt.label}` merely rendered nothing.
|
|
129
|
+
* - This changes DISPLAY only. It writes nothing, and it is not a licence to
|
|
130
|
+
* author blank labels — whether the contract should refuse them outright is
|
|
131
|
+
* a `packages/spec` question, not a renderer one.
|
|
132
|
+
*/
|
|
133
|
+
export declare function optionDisplayLabel(option: OptionLike): string;
|
|
72
134
|
/**
|
|
73
135
|
* Whether a scalar field value is still valid given the currently-visible
|
|
74
136
|
* options — used to decide a cascade clear (parent changed, child's old choice
|
|
@@ -97,3 +159,67 @@ export interface CascadingOptions<T extends OptionLike> {
|
|
|
97
159
|
* purely by `visibleWhen`). Callers format their own hint from `dependsOnFields`.
|
|
98
160
|
*/
|
|
99
161
|
export declare function resolveCascadingOptions<T extends OptionLike>(rawOptions: readonly T[] | undefined | null, record: Record<string, unknown>, dependsOn: DependsOnInput, scope?: Record<string, unknown>): CascadingOptions<T>;
|
|
162
|
+
/**
|
|
163
|
+
* Widget keys whose OFFERED option set is re-resolved against a live record by
|
|
164
|
+
* {@link resolveCascadingOptions} — i.e. the allow-list of widgets a surface
|
|
165
|
+
* must thread its live record to (`dependentValues`). Each of their options may
|
|
166
|
+
* carry a `visibleWhen` predicate read against `record.*`, and the field may
|
|
167
|
+
* declare a `dependsOn` gate (ADR-0058 / objectui#2284 / objectui#1583).
|
|
168
|
+
* Without the record a cascading select never sees its controlling field and
|
|
169
|
+
* stays permanently gated on the "select the parent first" hint.
|
|
170
|
+
*
|
|
171
|
+
* ## One definition, three surfaces (objectui#4770)
|
|
172
|
+
*
|
|
173
|
+
* Three surfaces feed this one evaluator, and each carried its own private copy
|
|
174
|
+
* of these four keys until this constant existed — with no gate able to notice
|
|
175
|
+
* them drifting apart on what "the record" means:
|
|
176
|
+
*
|
|
177
|
+
* - the object form — `CASCADE_OPTION_FIELD_TYPES` in
|
|
178
|
+
* `components/src/renderers/form/form.tsx` (the original);
|
|
179
|
+
* - the single-record action dialog — `CASCADE_OPTION_WIDGET_TYPES` in
|
|
180
|
+
* `app-shell/src/views/ActionParamDialog.tsx` (objectui#3765 / PR #4756);
|
|
181
|
+
* - the bulk action dialog — the same name in
|
|
182
|
+
* `plugin-grid/src/components/BulkActionDialog.tsx` (objectui#4757 / PR #4763).
|
|
183
|
+
*
|
|
184
|
+
* The set lives in `@object-ui/core`, next to the evaluator it describes,
|
|
185
|
+
* because core is the one package all three already depend on — the form layer
|
|
186
|
+
* cannot reach `@object-ui/fields` (that dependency runs the other way). Same
|
|
187
|
+
* shape and same reason as `EXPANDABLE_FIELD_TYPES` in
|
|
188
|
+
* `core/src/utils/expand-fields.ts`, the in-repo precedent for a form-layer
|
|
189
|
+
* allow-table more than one surface reads. `@object-ui/fields` re-exports it
|
|
190
|
+
* next to `resolveFormWidgetType` for discoverability — a re-export, never a
|
|
191
|
+
* second definition.
|
|
192
|
+
*
|
|
193
|
+
* ## An ALLOW-LIST, not a blanket pass-through
|
|
194
|
+
*
|
|
195
|
+
* `dependentValues` is also the channel two widget-hint pickers read a specific
|
|
196
|
+
* SIBLING KEY from (`filter-condition` reads `object_name`, `recipient-picker`
|
|
197
|
+
* reads `recipient_type`), and the lookup family filters its query by it —
|
|
198
|
+
* neither of which is an option list, so ⛔ no member is ever added to THIS set
|
|
199
|
+
* to serve them. Each surface decides that separately, by OR-ing its own rule
|
|
200
|
+
* beside this one, and they do not agree:
|
|
201
|
+
*
|
|
202
|
+
* - the object form has fed both families all along — `needsDataSourceWiring`,
|
|
203
|
+
* its own line beside the one that reads this set;
|
|
204
|
+
* - the single-record action dialog feeds the lookup family since
|
|
205
|
+
* objectui#8672 ruling A — `paramNeedsDependentValues()`, same shape;
|
|
206
|
+
* - the bulk action dialog feeds neither, and nothing has ruled on it.
|
|
207
|
+
*
|
|
208
|
+
* ⇒ objectui#4771 is the open boundary for the widget-hint pickers everywhere,
|
|
209
|
+
* and for the lookup family on the bulk dialog. Stated here so the surfaces'
|
|
210
|
+
* copies of the statement become one; ⛔ do not read a member of this set as
|
|
211
|
+
* the answer to "does widget X get the record on surface Y".
|
|
212
|
+
*
|
|
213
|
+
* ## Normalization stays with the consumer
|
|
214
|
+
*
|
|
215
|
+
* The members are WIDGET keys — `resolveFormWidgetType` output in the two
|
|
216
|
+
* dialogs, `normalizeFieldType` output (the `field:` prefix stripped) in the
|
|
217
|
+
* form — not authored `type` spellings. Each consumer keeps its own
|
|
218
|
+
* normalization; the two normalizations agree on these four members, which is
|
|
219
|
+
* what makes one shared set safe to read from both.
|
|
220
|
+
*
|
|
221
|
+
* `select` + `multiple: true` is not listed separately: `SelectField` delegates
|
|
222
|
+
* to `MultiSelectField` with the whole prop object, so the key it resolves
|
|
223
|
+
* under (`select`) is the one that must carry the record.
|
|
224
|
+
*/
|
|
225
|
+
export declare const CASCADE_OPTION_WIDGET_TYPES: ReadonlySet<string>;
|
|
@@ -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
|
|
@@ -77,3 +145,72 @@ export function resolveCascadingOptions(rawOptions, record, dependsOn, scope) {
|
|
|
77
145
|
const options = gated ? [] : resolveVisibleOptions(rawOptions, record, scope);
|
|
78
146
|
return { options, gated, dependsOnFields };
|
|
79
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Widget keys whose OFFERED option set is re-resolved against a live record by
|
|
150
|
+
* {@link resolveCascadingOptions} — i.e. the allow-list of widgets a surface
|
|
151
|
+
* must thread its live record to (`dependentValues`). Each of their options may
|
|
152
|
+
* carry a `visibleWhen` predicate read against `record.*`, and the field may
|
|
153
|
+
* declare a `dependsOn` gate (ADR-0058 / objectui#2284 / objectui#1583).
|
|
154
|
+
* Without the record a cascading select never sees its controlling field and
|
|
155
|
+
* stays permanently gated on the "select the parent first" hint.
|
|
156
|
+
*
|
|
157
|
+
* ## One definition, three surfaces (objectui#4770)
|
|
158
|
+
*
|
|
159
|
+
* Three surfaces feed this one evaluator, and each carried its own private copy
|
|
160
|
+
* of these four keys until this constant existed — with no gate able to notice
|
|
161
|
+
* them drifting apart on what "the record" means:
|
|
162
|
+
*
|
|
163
|
+
* - the object form — `CASCADE_OPTION_FIELD_TYPES` in
|
|
164
|
+
* `components/src/renderers/form/form.tsx` (the original);
|
|
165
|
+
* - the single-record action dialog — `CASCADE_OPTION_WIDGET_TYPES` in
|
|
166
|
+
* `app-shell/src/views/ActionParamDialog.tsx` (objectui#3765 / PR #4756);
|
|
167
|
+
* - the bulk action dialog — the same name in
|
|
168
|
+
* `plugin-grid/src/components/BulkActionDialog.tsx` (objectui#4757 / PR #4763).
|
|
169
|
+
*
|
|
170
|
+
* The set lives in `@object-ui/core`, next to the evaluator it describes,
|
|
171
|
+
* because core is the one package all three already depend on — the form layer
|
|
172
|
+
* cannot reach `@object-ui/fields` (that dependency runs the other way). Same
|
|
173
|
+
* shape and same reason as `EXPANDABLE_FIELD_TYPES` in
|
|
174
|
+
* `core/src/utils/expand-fields.ts`, the in-repo precedent for a form-layer
|
|
175
|
+
* allow-table more than one surface reads. `@object-ui/fields` re-exports it
|
|
176
|
+
* next to `resolveFormWidgetType` for discoverability — a re-export, never a
|
|
177
|
+
* second definition.
|
|
178
|
+
*
|
|
179
|
+
* ## An ALLOW-LIST, not a blanket pass-through
|
|
180
|
+
*
|
|
181
|
+
* `dependentValues` is also the channel two widget-hint pickers read a specific
|
|
182
|
+
* SIBLING KEY from (`filter-condition` reads `object_name`, `recipient-picker`
|
|
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".
|
|
198
|
+
*
|
|
199
|
+
* ## Normalization stays with the consumer
|
|
200
|
+
*
|
|
201
|
+
* The members are WIDGET keys — `resolveFormWidgetType` output in the two
|
|
202
|
+
* dialogs, `normalizeFieldType` output (the `field:` prefix stripped) in the
|
|
203
|
+
* form — not authored `type` spellings. Each consumer keeps its own
|
|
204
|
+
* normalization; the two normalizations agree on these four members, which is
|
|
205
|
+
* what makes one shared set safe to read from both.
|
|
206
|
+
*
|
|
207
|
+
* `select` + `multiple: true` is not listed separately: `SelectField` delegates
|
|
208
|
+
* to `MultiSelectField` with the whole prop object, so the key it resolves
|
|
209
|
+
* under (`select`) is the one that must carry the record.
|
|
210
|
+
*/
|
|
211
|
+
export const CASCADE_OPTION_WIDGET_TYPES = new Set([
|
|
212
|
+
'select',
|
|
213
|
+
'multiselect',
|
|
214
|
+
'radio',
|
|
215
|
+
'checkboxes',
|
|
216
|
+
]);
|
|
@@ -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
|
*
|