@object-ui/core 17.2.0 → 17.5.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 +1436 -0
- package/README.md +31 -12
- package/dist/actions/ActionEngine.d.ts +53 -31
- package/dist/actions/ActionEngine.js +87 -66
- package/dist/actions/ActionRunner.d.ts +215 -24
- package/dist/actions/ActionRunner.js +334 -17
- package/dist/actions/actionErrorDetail.d.ts +26 -0
- package/dist/actions/actionErrorDetail.js +37 -0
- package/dist/actions/actionKeys.d.ts +96 -20
- package/dist/actions/actionKeys.js +168 -18
- package/dist/actions/actionResponse.d.ts +42 -0
- package/dist/actions/actionResponse.js +104 -0
- package/dist/actions/index.d.ts +3 -0
- package/dist/actions/index.js +3 -0
- package/dist/actions/serverActionHandler.d.ts +143 -0
- package/dist/actions/serverActionHandler.js +137 -0
- package/dist/data-scope/ViewDataProvider.d.ts +37 -12
- package/dist/data-scope/ViewDataProvider.js +83 -5
- package/dist/data-scope/element-data-source.d.ts +113 -0
- package/dist/data-scope/element-data-source.js +216 -0
- package/dist/data-scope/index.d.ts +2 -1
- package/dist/data-scope/index.js +1 -0
- package/dist/evaluator/ExpressionEvaluator.js +4 -2
- package/dist/evaluator/declaredPredicate.d.ts +119 -0
- package/dist/evaluator/declaredPredicate.js +152 -0
- package/dist/evaluator/fieldRules.d.ts +42 -2
- package/dist/evaluator/fieldRules.js +101 -11
- package/dist/evaluator/index.d.ts +2 -0
- package/dist/evaluator/index.js +2 -0
- package/dist/evaluator/listConditional.d.ts +18 -3
- package/dist/evaluator/listConditional.js +0 -0
- package/dist/evaluator/optionRules.js +5 -1
- package/dist/evaluator/predicateInput.d.ts +132 -0
- package/dist/evaluator/predicateInput.js +129 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +26 -1
- package/dist/protocols/DndProtocol.d.ts +42 -1
- package/dist/protocols/KeyboardProtocol.d.ts +31 -1
- package/dist/protocols/SharingProtocol.d.ts +11 -1
- package/dist/protocols/index.d.ts +0 -1
- package/dist/protocols/index.js +8 -1
- package/dist/registry/Registry.d.ts +24 -0
- package/dist/theme/ThemeEngine.d.ts +0 -8
- package/dist/theme/ThemeEngine.js +22 -103
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.js +7 -1
- package/dist/types/index.d.ts +42 -8
- package/dist/utils/chart-series.d.ts +258 -5
- package/dist/utils/chart-series.js +423 -20
- package/dist/utils/compare-to.d.ts +40 -13
- package/dist/utils/compare-to.js +4 -32
- package/dist/utils/dashboard-filters.d.ts +54 -8
- package/dist/utils/dashboard-filters.js +261 -12
- package/dist/utils/dataset-format.d.ts +79 -15
- package/dist/utils/dataset-format.js +130 -9
- package/dist/utils/dataset-pivot.d.ts +78 -0
- package/dist/utils/dataset-pivot.js +78 -0
- package/dist/utils/dom-props.d.ts +137 -0
- package/dist/utils/dom-props.js +153 -0
- package/dist/utils/filter-converter.d.ts +39 -11
- package/dist/utils/filter-converter.js +87 -7
- package/dist/utils/normalize-list-view.d.ts +26 -6
- package/dist/utils/normalize-list-view.js +30 -8
- package/dist/utils/number-display.d.ts +151 -0
- package/dist/utils/number-display.js +71 -0
- package/dist/utils/predicate-fields.d.ts +96 -0
- package/dist/utils/predicate-fields.js +163 -0
- package/dist/utils/predicate-record.d.ts +32 -0
- package/dist/utils/predicate-record.js +120 -0
- package/dist/utils/record-title.js +0 -0
- package/dist/utils/resolve-view-id.d.ts +31 -0
- package/dist/utils/resolve-view-id.js +45 -0
- package/dist/utils/sort-query.d.ts +55 -0
- package/dist/utils/sort-query.js +35 -0
- package/package.json +5 -5
- package/dist/protocols/NotificationProtocol.d.ts +0 -71
- package/dist/protocols/NotificationProtocol.js +0 -89
- package/dist/utils/merge-views-into-objects.d.ts +0 -19
- package/dist/utils/merge-views-into-objects.js +0 -39
|
@@ -0,0 +1,216 @@
|
|
|
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
|
+
* `@object-ui/core` — ElementDataSource resolution (objectstack#5576).
|
|
10
|
+
*
|
|
11
|
+
* `PageComponentSchema.dataSource` (the spec's `ElementDataSourceSchema`) is the
|
|
12
|
+
* per-element data binding a page component uses to query a different object
|
|
13
|
+
* than the page's own context:
|
|
14
|
+
*
|
|
15
|
+
* ```json
|
|
16
|
+
* { "object": "account", "view": "hot", "filter": { … }, "sort": [ … ], "limit": 20 }
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* This module owns the two pure halves of consuming it — telling the METADATA
|
|
20
|
+
* apart from a runtime data-source adapter, and composing a named saved view
|
|
21
|
+
* with the binding's own keys. Both are needed in two places (the render path in
|
|
22
|
+
* `@object-ui/plugin-list`, and `ViewDataProvider.resolveElementDataSource`), so
|
|
23
|
+
* they live here rather than being written twice.
|
|
24
|
+
*
|
|
25
|
+
* ## Composition semantics
|
|
26
|
+
*
|
|
27
|
+
* The spec's own `describe()` text settles the one case that could go either
|
|
28
|
+
* way. `filter` is documented as "**Additional** filter criteria" — additional
|
|
29
|
+
* to what the named view already restricts — so a view filter and a binding
|
|
30
|
+
* filter COMBINE under `and`; the binding can only narrow the view, never widen
|
|
31
|
+
* it. That is also the safe direction for AI-authored metadata: a typo in a
|
|
32
|
+
* per-element filter cannot silently expose rows the saved view excluded.
|
|
33
|
+
*
|
|
34
|
+
* Nothing else in the binding is described as additional, and the remaining keys
|
|
35
|
+
* are single-valued (one ordering, one cap), so for those the rule is **the view
|
|
36
|
+
* provides the baseline and an explicit key overrides it**:
|
|
37
|
+
*
|
|
38
|
+
* | key | source | rule |
|
|
39
|
+
* |------------|-----------------------------------------|-----------------------------|
|
|
40
|
+
* | `object` | binding (required) | binding wins |
|
|
41
|
+
* | `columns` | view only — the binding has no such key | view |
|
|
42
|
+
* | `filter` | view + binding | AND-combined ("additional") |
|
|
43
|
+
* | `sort` | view or binding | binding overrides view |
|
|
44
|
+
* | `limit` | view (`pagination.pageSize`) or binding | binding overrides view |
|
|
45
|
+
* | `viewType` | view only | view |
|
|
46
|
+
*
|
|
47
|
+
* A lone `filter` — only the view has one, or only the binding does — is passed
|
|
48
|
+
* through in the shape it was stored in; only the two-source case is lowered to
|
|
49
|
+
* an ObjectQL AST, because that is what combining requires.
|
|
50
|
+
*
|
|
51
|
+
* `viewType` rides along because `list-view` IS the multi-kind view container
|
|
52
|
+
* (its registry `inputs` enumerate grid/kanban/gallery/…): naming a saved kanban
|
|
53
|
+
* view and rendering a grid would be a silently wrong answer.
|
|
54
|
+
*/
|
|
55
|
+
import { mergeFilterNodes } from '../utils/filter-converter.js';
|
|
56
|
+
import { resolveViewId } from '../utils/resolve-view-id.js';
|
|
57
|
+
const isPlainObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
58
|
+
/**
|
|
59
|
+
* Is this value the spec's `dataSource` METADATA, rather than a runtime
|
|
60
|
+
* data-source adapter?
|
|
61
|
+
*
|
|
62
|
+
* This guard exists because the two collide by name and the collision was live:
|
|
63
|
+
* `SchemaRenderer` spreads every non-metadata schema key onto the component as a
|
|
64
|
+
* React prop, so a spec-compliant `dataSource: { object, view }` on a page
|
|
65
|
+
* component landed on `ListView`'s `dataSource` PROP — which is the injected
|
|
66
|
+
* adapter — and shadowed it. The first `dataSource.find(…)` then threw
|
|
67
|
+
* `dataSource.find is not a function`, which the list reported as
|
|
68
|
+
* "Couldn't load records": writing the binding the spec documents BROKE the
|
|
69
|
+
* component (objectstack#5576).
|
|
70
|
+
*
|
|
71
|
+
* The test is positive on the metadata rather than negative on the adapter: a
|
|
72
|
+
* binding is a plain object with a non-empty string `object` and no `find`
|
|
73
|
+
* method. An adapter never has a string `object`; metadata never has `find`.
|
|
74
|
+
*/
|
|
75
|
+
export function isElementDataSourceConfig(value) {
|
|
76
|
+
if (!isPlainObject(value))
|
|
77
|
+
return false;
|
|
78
|
+
if (typeof value.find === 'function')
|
|
79
|
+
return false;
|
|
80
|
+
const object = value.object;
|
|
81
|
+
return typeof object === 'string' && object.length > 0;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Flatten every place saved views for one object can live into `id → config`.
|
|
85
|
+
*
|
|
86
|
+
* Two sources are real and both are read, in the order app-shell's `ObjectView`
|
|
87
|
+
* reads them: the object definition's embedded `listViews` / `list_views` map
|
|
88
|
+
* (metadata-defined) and the metadata overlay rows an adapter returns from
|
|
89
|
+
* `listViews()` (user-defined). Later sources win, so a saved view shadows a
|
|
90
|
+
* metadata view of the same id — the same precedence `ObjectView` applies when
|
|
91
|
+
* it dedups its tab list.
|
|
92
|
+
*
|
|
93
|
+
* Accepts a record keyed by view id or an array of view configs (overlay rows
|
|
94
|
+
* are an array and carry their identity in `name`, falling back to `id`).
|
|
95
|
+
*/
|
|
96
|
+
export function collectSavedViews(...sources) {
|
|
97
|
+
const out = {};
|
|
98
|
+
for (const source of sources) {
|
|
99
|
+
if (Array.isArray(source)) {
|
|
100
|
+
for (const entry of source) {
|
|
101
|
+
if (!isPlainObject(entry))
|
|
102
|
+
continue;
|
|
103
|
+
const id = entry.name ?? entry.id;
|
|
104
|
+
if (typeof id === 'string' && id)
|
|
105
|
+
out[id] = entry;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else if (isPlainObject(source)) {
|
|
109
|
+
for (const [id, entry] of Object.entries(source)) {
|
|
110
|
+
if (isPlainObject(entry))
|
|
111
|
+
out[id] = entry;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Find the saved view a `dataSource.view` name refers to.
|
|
119
|
+
*
|
|
120
|
+
* Name matching is {@link resolveViewId}'s — the same short/qualified tolerance
|
|
121
|
+
* the object page applies to a URL-requested view — so a name that works in one
|
|
122
|
+
* surface works in the other. Returns `undefined` when nothing matches; the
|
|
123
|
+
* caller must REPORT that rather than fall back to the object's default view.
|
|
124
|
+
* Silently widening a named view to "all records" is the failure mode this whole
|
|
125
|
+
* binding exists to remove.
|
|
126
|
+
*/
|
|
127
|
+
export function resolveSavedView(views, requested, object) {
|
|
128
|
+
const id = resolveViewId(requested, Object.keys(views), object);
|
|
129
|
+
return id === undefined ? undefined : views[id];
|
|
130
|
+
}
|
|
131
|
+
/** Read a saved view's row cap — `pagination.pageSize`, or a flat `limit`. */
|
|
132
|
+
function savedViewLimit(view) {
|
|
133
|
+
if (!view)
|
|
134
|
+
return undefined;
|
|
135
|
+
const pagination = view.pagination;
|
|
136
|
+
if (isPlainObject(pagination) && typeof pagination.pageSize === 'number') {
|
|
137
|
+
return pagination.pageSize;
|
|
138
|
+
}
|
|
139
|
+
return typeof view.limit === 'number' ? view.limit : undefined;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Read a saved view's render kind. `type` is the spec's key (and what
|
|
143
|
+
* `ObjectView` reads); `viewType` is objectui's legacy spelling, folded by
|
|
144
|
+
* `normalizeListViewSchema` downstream but accepted here so a view stored either
|
|
145
|
+
* way resolves to the same kind.
|
|
146
|
+
*/
|
|
147
|
+
function savedViewType(view) {
|
|
148
|
+
if (!view)
|
|
149
|
+
return undefined;
|
|
150
|
+
const kind = view.type ?? view.viewType;
|
|
151
|
+
return typeof kind === 'string' && kind ? kind : undefined;
|
|
152
|
+
}
|
|
153
|
+
/** Read a saved view's column list — canonical `columns`, legacy `fields`. */
|
|
154
|
+
function savedViewColumns(view) {
|
|
155
|
+
if (!view)
|
|
156
|
+
return undefined;
|
|
157
|
+
if (Array.isArray(view.columns))
|
|
158
|
+
return view.columns;
|
|
159
|
+
if (Array.isArray(view.fields))
|
|
160
|
+
return view.fields;
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Compose an `ElementDataSource` binding with the saved view it names.
|
|
165
|
+
*
|
|
166
|
+
* Pure — the caller resolves the view (see {@link resolveSavedView}) and owns
|
|
167
|
+
* the "named a view that does not exist" decision. Passing `undefined` /`null`
|
|
168
|
+
* for `view` composes a binding with no view, which is exactly the pre-#5576
|
|
169
|
+
* behaviour for the other four keys.
|
|
170
|
+
*
|
|
171
|
+
* See the module doc for the per-key rule table; `filter` is the only key that
|
|
172
|
+
* combines rather than overrides, because the spec describes the binding's
|
|
173
|
+
* filter as *additional*.
|
|
174
|
+
*/
|
|
175
|
+
export function composeElementDataSource(config, view) {
|
|
176
|
+
const composed = { object: config.object };
|
|
177
|
+
const columns = savedViewColumns(view);
|
|
178
|
+
if (columns !== undefined)
|
|
179
|
+
composed.columns = columns;
|
|
180
|
+
// "Additional filter criteria" (spec) — the view restricts, the binding
|
|
181
|
+
// restricts further, so two filters COMBINE under `and` via the repo's single
|
|
182
|
+
// combiner. A lone source passes through VERBATIM: lowering a stored filter to
|
|
183
|
+
// an ObjectQL AST belongs at the last hop before the wire and nowhere earlier
|
|
184
|
+
// (see `toFilterNode`'s doc on why), and every consumer of this result hands
|
|
185
|
+
// the value to that hop anyway. Combining is the one case that cannot preserve
|
|
186
|
+
// the input shape — putting two sources under one `and` requires AST — and
|
|
187
|
+
// `mergeFilterNodes` is the sanctioned place for it.
|
|
188
|
+
const viewFilter = view?.filter;
|
|
189
|
+
const filter = viewFilter !== undefined && config.filter !== undefined
|
|
190
|
+
? mergeFilterNodes(viewFilter, config.filter)
|
|
191
|
+
: (config.filter ?? viewFilter);
|
|
192
|
+
if (filter !== undefined)
|
|
193
|
+
composed.filter = filter;
|
|
194
|
+
const sort = config.sort ?? view?.sort;
|
|
195
|
+
if (sort !== undefined)
|
|
196
|
+
composed.sort = sort;
|
|
197
|
+
const limit = config.limit ?? savedViewLimit(view);
|
|
198
|
+
if (limit !== undefined)
|
|
199
|
+
composed.limit = limit;
|
|
200
|
+
const viewType = savedViewType(view);
|
|
201
|
+
if (viewType !== undefined)
|
|
202
|
+
composed.viewType = viewType;
|
|
203
|
+
return composed;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* The message shown when `dataSource.view` names a view the object does not
|
|
207
|
+
* have. Built here so the render path and `ViewDataProvider` report the same
|
|
208
|
+
* defect the same way, and so the known-view list (the thing that actually gets
|
|
209
|
+
* an author unstuck) is never omitted by one caller.
|
|
210
|
+
*/
|
|
211
|
+
export function elementDataSourceViewNotFoundMessage(object, view, knownViews) {
|
|
212
|
+
const known = knownViews.length
|
|
213
|
+
? `Known views: ${[...knownViews].sort().join(', ')}.`
|
|
214
|
+
: 'This object has no saved views.';
|
|
215
|
+
return `dataSource.view "${view}" was not found on object "${object}". ${known}`;
|
|
216
|
+
}
|
|
@@ -8,4 +8,5 @@
|
|
|
8
8
|
* @packageDocumentation
|
|
9
9
|
*/
|
|
10
10
|
export { DataScopeManager, defaultDataScopeManager, type RowLevelFilter, type DataScopeConfig, } from './DataScopeManager.js';
|
|
11
|
-
export { ViewDataProvider, type ViewDataConfig, type
|
|
11
|
+
export { ViewDataProvider, type ViewDataConfig, type DataFetcher, type ResolvedData, } from './ViewDataProvider.js';
|
|
12
|
+
export { collectSavedViews, composeElementDataSource, elementDataSourceViewNotFoundMessage, isElementDataSourceConfig, resolveSavedView, type ComposedElementDataSource, type ElementDataSourceConfig, type ElementDataSourceSort, type ElementSavedView, } from './element-data-source.js';
|
package/dist/data-scope/index.js
CHANGED
|
@@ -9,3 +9,4 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export { DataScopeManager, defaultDataScopeManager, } from './DataScopeManager.js';
|
|
11
11
|
export { ViewDataProvider, } from './ViewDataProvider.js';
|
|
12
|
+
export { collectSavedViews, composeElementDataSource, elementDataSourceViewNotFoundMessage, isElementDataSourceConfig, resolveSavedView, } from './element-data-source.js';
|
|
@@ -256,8 +256,10 @@ export class ExpressionEvaluator {
|
|
|
256
256
|
// Fail-closed callers need to tell a genuine `false` from a fault. The
|
|
257
257
|
// canonical helper fails soft to the fallback, so a value that tracks the
|
|
258
258
|
// fallback in BOTH runs means the predicate faulted — then we throw.
|
|
259
|
-
|
|
260
|
-
|
|
259
|
+
// `warn: false`: the throw below IS this path's diagnostic; without it one
|
|
260
|
+
// broken predicate would log AND throw (#5149).
|
|
261
|
+
const asTrue = evalFieldPredicate(source, record, true, undefined, bag, { warn: false });
|
|
262
|
+
const asFalse = evalFieldPredicate(source, record, false, undefined, bag, { warn: false });
|
|
261
263
|
if (asTrue !== asFalse) {
|
|
262
264
|
throw new Error(`CEL predicate failed to evaluate: ${source}`);
|
|
263
265
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
* Is a predicate gate DECLARED on this value — i.e. after normalization, is
|
|
10
|
+
* there still a CONDITION for {@link ExpressionEvaluator.evaluateCondition} to
|
|
11
|
+
* reach a verdict on? (objectui#3850's ruling, key-neutral: `visible` /
|
|
12
|
+
* `hidden` / `enabled` / `disabled` / `condition` all ask it.)
|
|
13
|
+
*
|
|
14
|
+
* This is the ONE definition of that question in the repo, and it lives here —
|
|
15
|
+
* one layer under every consumer, beside {@link toPredicateInput}, whose answer
|
|
16
|
+
* it is derived from. It has to be asked separately from the verdict because
|
|
17
|
+
* `evaluateCondition` documents and implements exactly one default for "there
|
|
18
|
+
* is nothing here to evaluate": it returns `true`, meaning *visible/enabled*.
|
|
19
|
+
* That default is right on `visible` / `enabled` and INVERTED on `disabled`,
|
|
20
|
+
* where `true` means "greyed out" — so a gate that hands an empty predicate to
|
|
21
|
+
* the evaluator gets the strongest possible "yes, disable it" for a value the
|
|
22
|
+
* metadata never used to say anything. Truthiness cannot answer it either, and
|
|
23
|
+
* asking it that way was objectui#3812: `if (action.visible && !isVisible)`
|
|
24
|
+
* read `visible: false` — the most explicit "never show this" an author can
|
|
25
|
+
* write — as *ungated*, and rendered the action for everyone.
|
|
26
|
+
*
|
|
27
|
+
* ## What counts as "nothing to evaluate"
|
|
28
|
+
*
|
|
29
|
+
* Exactly the shapes {@link toPredicateInput} folds to `undefined`, plus the two
|
|
30
|
+
* BLANK spellings it wraps or passes through instead of folding:
|
|
31
|
+
*
|
|
32
|
+
* - `null` / `undefined` — no key at all.
|
|
33
|
+
* - `''` — an empty predicate (objectui#3492 / objectui#3842).
|
|
34
|
+
* - blank predicate TEXT, in either spelling — a whitespace-only string
|
|
35
|
+
* (`' '`, which the normalizer wraps into `'${ }'`) and an envelope whose
|
|
36
|
+
* `source` is whitespace-only (`{ dialect: 'cel', source: ' ' }`, which the
|
|
37
|
+
* normalizer passes through because it only folds a `source` of `''`).
|
|
38
|
+
* Neither can be read off the normalizer's answer, so both are named here —
|
|
39
|
+
* see {@link isBlankPredicateText} for why this is the layer that says it.
|
|
40
|
+
* Core's other predicate entries already treat both as blank:
|
|
41
|
+
* `evaluateCondition` (`if (!trimmed) return true`), `evaluateCelCondition`
|
|
42
|
+
* (`if (!source.trim()) return true`) and `evalRowPredicate`
|
|
43
|
+
* (`evaluator/listConditional.ts`, `if (!source.trim())`).
|
|
44
|
+
* - `{ dialect, source: '' }` — the empty ENVELOPE. This is not an exotic
|
|
45
|
+
* spelling: `@objectstack/spec`'s `ExpressionInputSchema` normalizes every
|
|
46
|
+
* authored predicate into an envelope, so "author left the predicate empty"
|
|
47
|
+
* compiles to exactly this, which makes it the likeliest empty shape in real
|
|
48
|
+
* metadata (objectui#3850).
|
|
49
|
+
* - anything that is not a predicate at all (`0`, `{}`, an array): a value the
|
|
50
|
+
* evaluator cannot read must not be the reason a control is disabled or an
|
|
51
|
+
* action refuses to run. Fail-open on junk, which is the posture
|
|
52
|
+
* `ActionRunner` already committed to (`catch { isDisabled = false }`).
|
|
53
|
+
*
|
|
54
|
+
* A declared-and-`false` gate is DECLARED (`toPredicateInput` returns the
|
|
55
|
+
* boolean unchanged): `disabled: false` / `visible: false` are verdicts, and
|
|
56
|
+
* routing them to the evaluator — which short-circuits booleans — is the point
|
|
57
|
+
* of asking "declared?" rather than "truthy?".
|
|
58
|
+
*
|
|
59
|
+
* ## Why blankness is decided HERE and not in the normalizer (objectui#3960)
|
|
60
|
+
*
|
|
61
|
+
* The blank-`source` envelope arrived one release late: objectui#3850's ruling
|
|
62
|
+
* enumerated three empty spellings, `toPredicateInput` folds a `source` of `''`
|
|
63
|
+
* and does not trim, so `{ dialect: 'cel', source: ' ' }` stayed "declared"
|
|
64
|
+
* while core's own CEL entry called that exact value "no predicate" — `disabled`
|
|
65
|
+
* greyed out and `ActionRunner` refused to execute for a predicate that says
|
|
66
|
+
* nothing (objectui#3960). The asymmetry was that this definition trimmed the
|
|
67
|
+
* STRING spelling of a blank predicate and not the ENVELOPE spelling of the same
|
|
68
|
+
* blank.
|
|
69
|
+
*
|
|
70
|
+
* Fixing it at the normalizer instead (`if (!src.trim()) return undefined`) would
|
|
71
|
+
* have aligned every `evaluateCondition(toPredicateInput(x))` consumer at once,
|
|
72
|
+
* and that is precisely why it is the wider, wrong lever: it changes what
|
|
73
|
+
* normalization MEANS, which is a shape question ("what does the evaluator
|
|
74
|
+
* accept"), to answer a declaredness question ("is there a condition"). Those are
|
|
75
|
+
* different concepts on purpose — a blank `source` is a shape the evaluator
|
|
76
|
+
* accepts and answers `true` for, by its own documented rule. The blast radius is
|
|
77
|
+
* the concrete difference: at the normalizer, a NON-cel blank envelope
|
|
78
|
+
* (`{ source: ' ' }` → `'${ }'` → falsy) flips verdict for every
|
|
79
|
+
* `useCondition(toPredicateInput(…))` call site including the container-level
|
|
80
|
+
* `visible` reads in `action-bar` / `action-group` / `action-menu` /
|
|
81
|
+
* `RelatedList` / `record-alert`, none of which ask this question at all; here it
|
|
82
|
+
* flips only for the consumers that DO ask it, and only on the inverted-polarity
|
|
83
|
+
* keys where "declared" is what turns a control off.
|
|
84
|
+
*
|
|
85
|
+
* ## Why the callers still evaluate the RAW value
|
|
86
|
+
*
|
|
87
|
+
* Every consumer normalizes to decide DECLAREDNESS and then evaluates whatever
|
|
88
|
+
* it was given, `evaluateCondition(raw)` or `evaluateCondition(toPredicateInput(
|
|
89
|
+
* raw))` as it did before. Both agree since objectui#3871 made the normalizer
|
|
90
|
+
* idempotent for an already-`${…}` string; before that they did not, and a
|
|
91
|
+
* template-spelled predicate normalized twice came back as a constant. Nothing
|
|
92
|
+
* here changes a verdict for a value that HAS one — this gate only decides
|
|
93
|
+
* whether there is one to reach.
|
|
94
|
+
*
|
|
95
|
+
* ## Scope history (objectui#3850)
|
|
96
|
+
*
|
|
97
|
+
* The question used to be answered three times with three different scopes: the
|
|
98
|
+
* renderer-side `hasDeclaredVisibilityGate` (`!= null && !== ''`, so every
|
|
99
|
+
* object counted — including the empty envelope), `SchemaRenderer`'s inline
|
|
100
|
+
* `!== undefined` (wider still: `disabled: null` greyed the control out,
|
|
101
|
+
* objectui#3862), and `ActionRunner`'s module-private helper (this scope, which
|
|
102
|
+
* arrived first with objectui#3848 and is what the ruling adopted). One
|
|
103
|
+
* definition with one scope replaces all three:
|
|
104
|
+
* `components/renderers/action/visibility-gate.ts` re-exports this function
|
|
105
|
+
* under its historic name `hasDeclaredVisibilityGate` (the five member-action
|
|
106
|
+
* renderer call sites are unchanged), `SchemaRenderer`'s `disabled` /
|
|
107
|
+
* `disabledOn` chain reads it, and `ActionRunner`'s two gates read it instead of
|
|
108
|
+
* a private twin.
|
|
109
|
+
*
|
|
110
|
+
* Two more consumers joined afterwards, and with them the last two places that
|
|
111
|
+
* answered this question with a range of their own:
|
|
112
|
+
* `SchemaRenderer`'s `hidden` / `hiddenOn` legs, whose verdict is NOT negated so
|
|
113
|
+
* an empty predicate made the node VANISH (objectui#3955), and
|
|
114
|
+
* `ActionEngine.getActionsForLocation`'s `visible` filter, which folded some
|
|
115
|
+
* empty spellings by hand and coerced the rest with `Boolean(raw)`, so
|
|
116
|
+
* `visible: 0` hid an action the renderers showed (objectui#3957). Nothing in the
|
|
117
|
+
* repo now asks "is a gate declared?" anywhere but here.
|
|
118
|
+
*/
|
|
119
|
+
export declare function hasDeclaredPredicate(value: unknown): boolean;
|
|
@@ -0,0 +1,152 @@
|
|
|
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
|
+
import { toPredicateInput } from './predicateInput';
|
|
9
|
+
/**
|
|
10
|
+
* Is the predicate TEXT blank — in EITHER spelling? (objectui#3960)
|
|
11
|
+
*
|
|
12
|
+
* `' '` and `{ dialect: 'cel', source: ' ' }` are the same author mistake
|
|
13
|
+
* written two ways, and {@link toPredicateInput} folds neither: it wraps a
|
|
14
|
+
* whitespace-only string into `'${ }'`, and it only folds an envelope whose
|
|
15
|
+
* `source` is exactly `''`. So blankness cannot be derived from the normalizer's
|
|
16
|
+
* answer and has to be stated once, here, for both spellings — trimming one and
|
|
17
|
+
* not the other is the asymmetry objectui#3960 measured (`disabled` greyed out,
|
|
18
|
+
* `ActionRunner` refused to execute, for a predicate that says nothing).
|
|
19
|
+
*
|
|
20
|
+
* This is not a fourth dialect of "empty": it is the SAME rule core's evaluation
|
|
21
|
+
* entries already apply on the value side — `evaluateCondition`
|
|
22
|
+
* (`if (!trimmed) return true`), `evaluateCelCondition` (`if (!source.trim())
|
|
23
|
+
* return true`), `evalRowPredicate` (`listConditional.ts`) — brought to the one
|
|
24
|
+
* place that answers "is there a condition at all?", so the two halves cannot
|
|
25
|
+
* disagree about the same blank.
|
|
26
|
+
*/
|
|
27
|
+
function isBlankPredicateText(value) {
|
|
28
|
+
if (typeof value === 'string')
|
|
29
|
+
return value.trim() === '';
|
|
30
|
+
if (value !== null && typeof value === 'object') {
|
|
31
|
+
const source = value.source;
|
|
32
|
+
if (typeof source === 'string')
|
|
33
|
+
return source.trim() === '';
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Is a predicate gate DECLARED on this value — i.e. after normalization, is
|
|
39
|
+
* there still a CONDITION for {@link ExpressionEvaluator.evaluateCondition} to
|
|
40
|
+
* reach a verdict on? (objectui#3850's ruling, key-neutral: `visible` /
|
|
41
|
+
* `hidden` / `enabled` / `disabled` / `condition` all ask it.)
|
|
42
|
+
*
|
|
43
|
+
* This is the ONE definition of that question in the repo, and it lives here —
|
|
44
|
+
* one layer under every consumer, beside {@link toPredicateInput}, whose answer
|
|
45
|
+
* it is derived from. It has to be asked separately from the verdict because
|
|
46
|
+
* `evaluateCondition` documents and implements exactly one default for "there
|
|
47
|
+
* is nothing here to evaluate": it returns `true`, meaning *visible/enabled*.
|
|
48
|
+
* That default is right on `visible` / `enabled` and INVERTED on `disabled`,
|
|
49
|
+
* where `true` means "greyed out" — so a gate that hands an empty predicate to
|
|
50
|
+
* the evaluator gets the strongest possible "yes, disable it" for a value the
|
|
51
|
+
* metadata never used to say anything. Truthiness cannot answer it either, and
|
|
52
|
+
* asking it that way was objectui#3812: `if (action.visible && !isVisible)`
|
|
53
|
+
* read `visible: false` — the most explicit "never show this" an author can
|
|
54
|
+
* write — as *ungated*, and rendered the action for everyone.
|
|
55
|
+
*
|
|
56
|
+
* ## What counts as "nothing to evaluate"
|
|
57
|
+
*
|
|
58
|
+
* Exactly the shapes {@link toPredicateInput} folds to `undefined`, plus the two
|
|
59
|
+
* BLANK spellings it wraps or passes through instead of folding:
|
|
60
|
+
*
|
|
61
|
+
* - `null` / `undefined` — no key at all.
|
|
62
|
+
* - `''` — an empty predicate (objectui#3492 / objectui#3842).
|
|
63
|
+
* - blank predicate TEXT, in either spelling — a whitespace-only string
|
|
64
|
+
* (`' '`, which the normalizer wraps into `'${ }'`) and an envelope whose
|
|
65
|
+
* `source` is whitespace-only (`{ dialect: 'cel', source: ' ' }`, which the
|
|
66
|
+
* normalizer passes through because it only folds a `source` of `''`).
|
|
67
|
+
* Neither can be read off the normalizer's answer, so both are named here —
|
|
68
|
+
* see {@link isBlankPredicateText} for why this is the layer that says it.
|
|
69
|
+
* Core's other predicate entries already treat both as blank:
|
|
70
|
+
* `evaluateCondition` (`if (!trimmed) return true`), `evaluateCelCondition`
|
|
71
|
+
* (`if (!source.trim()) return true`) and `evalRowPredicate`
|
|
72
|
+
* (`evaluator/listConditional.ts`, `if (!source.trim())`).
|
|
73
|
+
* - `{ dialect, source: '' }` — the empty ENVELOPE. This is not an exotic
|
|
74
|
+
* spelling: `@objectstack/spec`'s `ExpressionInputSchema` normalizes every
|
|
75
|
+
* authored predicate into an envelope, so "author left the predicate empty"
|
|
76
|
+
* compiles to exactly this, which makes it the likeliest empty shape in real
|
|
77
|
+
* metadata (objectui#3850).
|
|
78
|
+
* - anything that is not a predicate at all (`0`, `{}`, an array): a value the
|
|
79
|
+
* evaluator cannot read must not be the reason a control is disabled or an
|
|
80
|
+
* action refuses to run. Fail-open on junk, which is the posture
|
|
81
|
+
* `ActionRunner` already committed to (`catch { isDisabled = false }`).
|
|
82
|
+
*
|
|
83
|
+
* A declared-and-`false` gate is DECLARED (`toPredicateInput` returns the
|
|
84
|
+
* boolean unchanged): `disabled: false` / `visible: false` are verdicts, and
|
|
85
|
+
* routing them to the evaluator — which short-circuits booleans — is the point
|
|
86
|
+
* of asking "declared?" rather than "truthy?".
|
|
87
|
+
*
|
|
88
|
+
* ## Why blankness is decided HERE and not in the normalizer (objectui#3960)
|
|
89
|
+
*
|
|
90
|
+
* The blank-`source` envelope arrived one release late: objectui#3850's ruling
|
|
91
|
+
* enumerated three empty spellings, `toPredicateInput` folds a `source` of `''`
|
|
92
|
+
* and does not trim, so `{ dialect: 'cel', source: ' ' }` stayed "declared"
|
|
93
|
+
* while core's own CEL entry called that exact value "no predicate" — `disabled`
|
|
94
|
+
* greyed out and `ActionRunner` refused to execute for a predicate that says
|
|
95
|
+
* nothing (objectui#3960). The asymmetry was that this definition trimmed the
|
|
96
|
+
* STRING spelling of a blank predicate and not the ENVELOPE spelling of the same
|
|
97
|
+
* blank.
|
|
98
|
+
*
|
|
99
|
+
* Fixing it at the normalizer instead (`if (!src.trim()) return undefined`) would
|
|
100
|
+
* have aligned every `evaluateCondition(toPredicateInput(x))` consumer at once,
|
|
101
|
+
* and that is precisely why it is the wider, wrong lever: it changes what
|
|
102
|
+
* normalization MEANS, which is a shape question ("what does the evaluator
|
|
103
|
+
* accept"), to answer a declaredness question ("is there a condition"). Those are
|
|
104
|
+
* different concepts on purpose — a blank `source` is a shape the evaluator
|
|
105
|
+
* accepts and answers `true` for, by its own documented rule. The blast radius is
|
|
106
|
+
* the concrete difference: at the normalizer, a NON-cel blank envelope
|
|
107
|
+
* (`{ source: ' ' }` → `'${ }'` → falsy) flips verdict for every
|
|
108
|
+
* `useCondition(toPredicateInput(…))` call site including the container-level
|
|
109
|
+
* `visible` reads in `action-bar` / `action-group` / `action-menu` /
|
|
110
|
+
* `RelatedList` / `record-alert`, none of which ask this question at all; here it
|
|
111
|
+
* flips only for the consumers that DO ask it, and only on the inverted-polarity
|
|
112
|
+
* keys where "declared" is what turns a control off.
|
|
113
|
+
*
|
|
114
|
+
* ## Why the callers still evaluate the RAW value
|
|
115
|
+
*
|
|
116
|
+
* Every consumer normalizes to decide DECLAREDNESS and then evaluates whatever
|
|
117
|
+
* it was given, `evaluateCondition(raw)` or `evaluateCondition(toPredicateInput(
|
|
118
|
+
* raw))` as it did before. Both agree since objectui#3871 made the normalizer
|
|
119
|
+
* idempotent for an already-`${…}` string; before that they did not, and a
|
|
120
|
+
* template-spelled predicate normalized twice came back as a constant. Nothing
|
|
121
|
+
* here changes a verdict for a value that HAS one — this gate only decides
|
|
122
|
+
* whether there is one to reach.
|
|
123
|
+
*
|
|
124
|
+
* ## Scope history (objectui#3850)
|
|
125
|
+
*
|
|
126
|
+
* The question used to be answered three times with three different scopes: the
|
|
127
|
+
* renderer-side `hasDeclaredVisibilityGate` (`!= null && !== ''`, so every
|
|
128
|
+
* object counted — including the empty envelope), `SchemaRenderer`'s inline
|
|
129
|
+
* `!== undefined` (wider still: `disabled: null` greyed the control out,
|
|
130
|
+
* objectui#3862), and `ActionRunner`'s module-private helper (this scope, which
|
|
131
|
+
* arrived first with objectui#3848 and is what the ruling adopted). One
|
|
132
|
+
* definition with one scope replaces all three:
|
|
133
|
+
* `components/renderers/action/visibility-gate.ts` re-exports this function
|
|
134
|
+
* under its historic name `hasDeclaredVisibilityGate` (the five member-action
|
|
135
|
+
* renderer call sites are unchanged), `SchemaRenderer`'s `disabled` /
|
|
136
|
+
* `disabledOn` chain reads it, and `ActionRunner`'s two gates read it instead of
|
|
137
|
+
* a private twin.
|
|
138
|
+
*
|
|
139
|
+
* Two more consumers joined afterwards, and with them the last two places that
|
|
140
|
+
* answered this question with a range of their own:
|
|
141
|
+
* `SchemaRenderer`'s `hidden` / `hiddenOn` legs, whose verdict is NOT negated so
|
|
142
|
+
* an empty predicate made the node VANISH (objectui#3955), and
|
|
143
|
+
* `ActionEngine.getActionsForLocation`'s `visible` filter, which folded some
|
|
144
|
+
* empty spellings by hand and coerced the rest with `Boolean(raw)`, so
|
|
145
|
+
* `visible: 0` hid an action the renderers showed (objectui#3957). Nothing in the
|
|
146
|
+
* repo now asks "is a gate declared?" anywhere but here.
|
|
147
|
+
*/
|
|
148
|
+
export function hasDeclaredPredicate(value) {
|
|
149
|
+
if (isBlankPredicateText(value))
|
|
150
|
+
return false;
|
|
151
|
+
return toPredicateInput(value) !== undefined;
|
|
152
|
+
}
|
|
@@ -10,6 +10,37 @@ export type FieldRulePredicate = string | {
|
|
|
10
10
|
dialect?: string;
|
|
11
11
|
source: string;
|
|
12
12
|
};
|
|
13
|
+
/** Diagnostic options for {@link evalFieldPredicate} evaluation failures. */
|
|
14
|
+
export interface FieldPredicateDiagnostic {
|
|
15
|
+
/**
|
|
16
|
+
* Human-readable locator included in the one-time failure warning — e.g.
|
|
17
|
+
* `"visibleWhen of field 'amount'"`. Pass whatever the call site knows; the
|
|
18
|
+
* predicate source text and failure reason are always included.
|
|
19
|
+
*/
|
|
20
|
+
context?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Set `false` when the caller deliberately probes for faults (evaluating
|
|
23
|
+
* twice with both fallbacks) and surfaces its own diagnostic — prevents two
|
|
24
|
+
* warnings for one broken predicate. Defaults to `true`.
|
|
25
|
+
*/
|
|
26
|
+
warn?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Called with the engine's failure reason (`"[kind] message"`, the exact text
|
|
29
|
+
* the built-in warning prints after `Reason:`) when the predicate cannot be
|
|
30
|
+
* evaluated. This is the passback for callers that set `warn: false` and
|
|
31
|
+
* report the fault themselves — without it, silencing the built-in warning
|
|
32
|
+
* also discards the only description of *why* the predicate failed, which is
|
|
33
|
+
* how the fail-closed row-predicate path ended up the least debuggable one
|
|
34
|
+
* (objectui#3792).
|
|
35
|
+
*
|
|
36
|
+
* Independent of `warn` and of the one-time-warning dedupe: it fires on every
|
|
37
|
+
* fault, so a caller doing its own warn-once bookkeeping keeps control of it.
|
|
38
|
+
* The returned verdict is unaffected. Must not throw — it is invoked outside
|
|
39
|
+
* the engine guard, so an exception here propagates to the caller rather than
|
|
40
|
+
* being reported as an engine fault.
|
|
41
|
+
*/
|
|
42
|
+
onFault?: (reason: string) => void;
|
|
43
|
+
}
|
|
13
44
|
/**
|
|
14
45
|
* Evaluate a field-rule CEL predicate against a record.
|
|
15
46
|
*
|
|
@@ -24,14 +55,23 @@ export type FieldRulePredicate = string | {
|
|
|
24
55
|
* e.g. `{ parent }` so an inline line-item cell can reference
|
|
25
56
|
* its header (`parent.status == 'paid'`) as well as its own
|
|
26
57
|
* row (`record.quantity`). Bound via the engine's `extra`.
|
|
58
|
+
* @param diagnostic Failure-warning options: a `context` locator for the
|
|
59
|
+
* one-time warning, and `warn: false` for callers that probe
|
|
60
|
+
* for faults and report them themselves. The returned value
|
|
61
|
+
* is unaffected either way.
|
|
27
62
|
*/
|
|
28
|
-
export declare function evalFieldPredicate(pred: FieldRulePredicate | undefined | null, record: Record<string, unknown>, fallback: boolean, previous?: Record<string, unknown>, scope?: Record<string, unknown
|
|
63
|
+
export declare function evalFieldPredicate(pred: FieldRulePredicate | undefined | null, record: Record<string, unknown>, fallback: boolean, previous?: Record<string, unknown>, scope?: Record<string, unknown>, diagnostic?: FieldPredicateDiagnostic): boolean;
|
|
29
64
|
/**
|
|
30
65
|
* Resolve the effective `{ visible, readonly, required }` state for a field
|
|
31
66
|
* given its conditional rules and the live record. Each `*When` rule, when
|
|
32
67
|
* present, *overrides* the static flag. A static `true` is never weakened by a
|
|
33
68
|
* `false` predicate result for required/readonly — but `visibleWhen` is
|
|
34
69
|
* authoritative when present (so a field can be conditionally shown/hidden).
|
|
70
|
+
*
|
|
71
|
+
* @param fieldContext Optional locator for failure warnings — e.g.
|
|
72
|
+
* `"field 'amount'"`. The warning then reads
|
|
73
|
+
* `visibleWhen of field 'amount' …`; without it, the rule
|
|
74
|
+
* kind alone is reported.
|
|
35
75
|
*/
|
|
36
76
|
export declare function resolveFieldRuleState(rules: {
|
|
37
77
|
visibleWhen?: FieldRulePredicate;
|
|
@@ -40,7 +80,7 @@ export declare function resolveFieldRuleState(rules: {
|
|
|
40
80
|
}, record: Record<string, unknown>, statics: {
|
|
41
81
|
required?: boolean;
|
|
42
82
|
readonly?: boolean;
|
|
43
|
-
}, previous?: Record<string, unknown>, scope?: Record<string, unknown
|
|
83
|
+
}, previous?: Record<string, unknown>, scope?: Record<string, unknown>, fieldContext?: string): {
|
|
44
84
|
visible: boolean;
|
|
45
85
|
readonly: boolean;
|
|
46
86
|
required: boolean;
|