@object-ui/core 11.4.0 → 13.1.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 +59 -0
- package/dist/actions/ActionRunner.js +8 -1
- package/dist/evaluator/ExpressionEvaluator.js +7 -2
- package/dist/evaluator/index.d.ts +1 -0
- package/dist/evaluator/index.js +1 -0
- package/dist/evaluator/optionRules.d.ts +70 -0
- package/dist/evaluator/optionRules.js +80 -0
- package/dist/styling/scoped-styles.js +10 -2
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
# @object-ui/core
|
|
2
2
|
|
|
3
|
+
## 13.1.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @object-ui/types@13.1.0
|
|
8
|
+
|
|
9
|
+
## 13.0.0
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Updated dependencies [619097e]
|
|
14
|
+
- @object-ui/types@13.0.0
|
|
15
|
+
|
|
16
|
+
## 12.1.0
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- Updated dependencies [c31874d]
|
|
21
|
+
- @object-ui/types@12.1.0
|
|
22
|
+
|
|
23
|
+
## 12.0.0
|
|
24
|
+
|
|
25
|
+
### Minor Changes
|
|
26
|
+
|
|
27
|
+
- 226fde9: Cascading & role-gated `select` options (#2284).
|
|
28
|
+
|
|
29
|
+
`select` options now accept a per-option `visibleWhen` CEL predicate — the option
|
|
30
|
+
is offered only when it evaluates TRUE against the live record **plus
|
|
31
|
+
`current_user`** (same engine/env as a field-level `visibleWhen`). Combined with a
|
|
32
|
+
field-level `dependsOn`, this drives dependent selects (country → province → city)
|
|
33
|
+
and role/context gating with no bespoke matrix — the same primitives dependent
|
|
34
|
+
lookups (#2215) already use.
|
|
35
|
+
|
|
36
|
+
- `@object-ui/core` exposes `resolveVisibleOptions` / `isOptionGroupGated` /
|
|
37
|
+
`resolveDependsOnFields` / `isValueStillOffered` (evaluator), reusing the
|
|
38
|
+
canonical `evalFieldPredicate`.
|
|
39
|
+
- The form renderer narrows a dependent select's option list, gates the control
|
|
40
|
+
with a "Select {parent} first" hint while a `dependsOn` field is empty, and
|
|
41
|
+
clears a now-invalid value when the parent changes.
|
|
42
|
+
- The standalone `SelectField` widget applies the same resolution via
|
|
43
|
+
`dependentValues` + the global predicate scope.
|
|
44
|
+
|
|
45
|
+
Client-side hiding is UX, not authorization: gate authorization-sensitive option
|
|
46
|
+
values on the server too. Aligns with `@objectstack/spec` `SelectOption.visibleWhen`.
|
|
47
|
+
|
|
48
|
+
### Patch Changes
|
|
49
|
+
|
|
50
|
+
- Updated dependencies [226fde9]
|
|
51
|
+
- Updated dependencies [e4de456]
|
|
52
|
+
- @object-ui/types@12.0.0
|
|
53
|
+
|
|
54
|
+
## 11.5.0
|
|
55
|
+
|
|
56
|
+
### Patch Changes
|
|
57
|
+
|
|
58
|
+
- Updated dependencies [9255686]
|
|
59
|
+
- Updated dependencies [1072701]
|
|
60
|
+
- @object-ui/types@11.5.0
|
|
61
|
+
|
|
3
62
|
## 11.4.0
|
|
4
63
|
|
|
5
64
|
### Patch Changes
|
|
@@ -412,7 +412,14 @@ export class ActionRunner {
|
|
|
412
412
|
}
|
|
413
413
|
}
|
|
414
414
|
try {
|
|
415
|
-
|
|
415
|
+
let result = this.evaluator.evaluate(`\${${script}}`);
|
|
416
|
+
// The expression itself is synchronous, but a formula function it calls
|
|
417
|
+
// may kick off an async write and return a Promise. Await it so the
|
|
418
|
+
// caller's success/failure — and the post-execution toast — reflects
|
|
419
|
+
// whether that write actually completed, not just that we called it.
|
|
420
|
+
if (result != null && typeof result.then === 'function') {
|
|
421
|
+
result = await result;
|
|
422
|
+
}
|
|
416
423
|
return { success: true, data: result };
|
|
417
424
|
}
|
|
418
425
|
catch (error) {
|
|
@@ -204,9 +204,14 @@ export class ExpressionEvaluator {
|
|
|
204
204
|
try {
|
|
205
205
|
return Boolean(this.evaluateExpression(trimmed, { sanitize: options.sanitize !== false }));
|
|
206
206
|
}
|
|
207
|
-
catch {
|
|
207
|
+
catch (error) {
|
|
208
208
|
// Unparseable predicate — preserve the historical "default to
|
|
209
|
-
// visible/enabled" behaviour rather than hiding/blocking on a typo
|
|
209
|
+
// visible/enabled" behaviour rather than hiding/blocking on a typo,
|
|
210
|
+
// UNLESS the caller opted into fail-closed semantics (mirrors the
|
|
211
|
+
// `${...}` template path above, which already honors this).
|
|
212
|
+
if (options.throwOnError) {
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
210
215
|
return true;
|
|
211
216
|
}
|
|
212
217
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
export * from './ExpressionContext.js';
|
|
9
9
|
export * from './ExpressionEvaluator.js';
|
|
10
10
|
export * from './fieldRules.js';
|
|
11
|
+
export * from './optionRules.js';
|
|
11
12
|
export * from './ExpressionCache.js';
|
|
12
13
|
export * from './FormulaFunctions.js';
|
|
13
14
|
export * from './SafeExpressionParser.js';
|
package/dist/evaluator/index.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
export * from './ExpressionContext.js';
|
|
9
9
|
export * from './ExpressionEvaluator.js';
|
|
10
10
|
export * from './fieldRules.js';
|
|
11
|
+
export * from './optionRules.js';
|
|
11
12
|
export * from './ExpressionCache.js';
|
|
12
13
|
export * from './FormulaFunctions.js';
|
|
13
14
|
export * from './SafeExpressionParser.js';
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
* Client-side resolution of per-option visibility for `select` / `radio` /
|
|
10
|
+
* `multiselect` fields — cascading (dependent) options and role/context gating.
|
|
11
|
+
*
|
|
12
|
+
* An option carries an optional `visibleWhen` CEL predicate; the option is
|
|
13
|
+
* offered only when it evaluates TRUE against the live record + `current_user`
|
|
14
|
+
* (the SAME engine and binding environment as a field-level `visibleWhen`, via
|
|
15
|
+
* {@link evalFieldPredicate}). This expresses both:
|
|
16
|
+
* - cascades — `record.country == 'cn'` (the dependent list narrows as the
|
|
17
|
+
* controlling field changes), and
|
|
18
|
+
* - role/context gating — `'admin' in current_user.roles`.
|
|
19
|
+
*
|
|
20
|
+
* A field declares which sibling fields its option list reacts to via
|
|
21
|
+
* `dependsOn` (aligns with `@objectstack/spec` Field.dependsOn — the same knob
|
|
22
|
+
* lookups use). While any dependency is empty the list is *gated*: we surface a
|
|
23
|
+
* "select the parent first" hint instead of an unfiltered set, mirroring the
|
|
24
|
+
* dependent-lookup UX (#2215).
|
|
25
|
+
*
|
|
26
|
+
* Evaluation is fail-open (a broken/absent predicate keeps the option) — the
|
|
27
|
+
* same safe default as field visibility. Client-side hiding is UX, not a
|
|
28
|
+
* security boundary: when an option is gated for access-control reasons the
|
|
29
|
+
* server must also reject writes of its value.
|
|
30
|
+
*/
|
|
31
|
+
import { type FieldRulePredicate } from './fieldRules.js';
|
|
32
|
+
/**
|
|
33
|
+
* Minimal shape of a select/radio option this module reads. Deliberately has no
|
|
34
|
+
* index signature so richer option types (`SelectOption`, `SelectOptionMetadata`)
|
|
35
|
+
* remain structurally assignable — the helpers only read `value`/`visibleWhen`.
|
|
36
|
+
*/
|
|
37
|
+
export interface OptionLike {
|
|
38
|
+
label: string;
|
|
39
|
+
value: string;
|
|
40
|
+
/** Per-option visibility predicate (CEL). Omit = always available. */
|
|
41
|
+
visibleWhen?: FieldRulePredicate;
|
|
42
|
+
}
|
|
43
|
+
/** A field's `dependsOn` as authored: a bare name, or a list of names / `{field,param}`. */
|
|
44
|
+
export type DependsOnInput = string | Array<string | {
|
|
45
|
+
field: string;
|
|
46
|
+
param?: string;
|
|
47
|
+
}> | undefined | null;
|
|
48
|
+
/**
|
|
49
|
+
* Normalize a field's `dependsOn` to the list of sibling field names it reacts
|
|
50
|
+
* to. The lookup-only `{field,param}` form contributes just its `field`.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveDependsOnFields(dependsOn: DependsOnInput): string[];
|
|
53
|
+
/**
|
|
54
|
+
* True when at least one `dependsOn` field is empty in the record — the option
|
|
55
|
+
* list is gated and the widget should prompt for the parent rather than show an
|
|
56
|
+
* unfiltered set. Returns false when there are no dependencies.
|
|
57
|
+
*/
|
|
58
|
+
export declare function isOptionGroupGated(dependsOn: DependsOnInput, record: Record<string, unknown>): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Filter options by their per-option `visibleWhen` predicate, evaluated against
|
|
61
|
+
* the live `record` (+ optional `scope`, e.g. `{ current_user }`). Options with
|
|
62
|
+
* no predicate are always kept; a predicate that errors fails open (kept).
|
|
63
|
+
*/
|
|
64
|
+
export declare function resolveVisibleOptions<T extends OptionLike>(options: readonly T[] | undefined | null, record: Record<string, unknown>, scope?: Record<string, unknown>): T[];
|
|
65
|
+
/**
|
|
66
|
+
* Whether a scalar field value is still valid given the currently-visible
|
|
67
|
+
* options — used to decide a cascade clear (parent changed, child's old choice
|
|
68
|
+
* no longer offered). An empty value is always "valid" (nothing to clear).
|
|
69
|
+
*/
|
|
70
|
+
export declare function isValueStillOffered(value: unknown, visibleOptions: readonly OptionLike[]): boolean;
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
* Client-side resolution of per-option visibility for `select` / `radio` /
|
|
10
|
+
* `multiselect` fields — cascading (dependent) options and role/context gating.
|
|
11
|
+
*
|
|
12
|
+
* An option carries an optional `visibleWhen` CEL predicate; the option is
|
|
13
|
+
* offered only when it evaluates TRUE against the live record + `current_user`
|
|
14
|
+
* (the SAME engine and binding environment as a field-level `visibleWhen`, via
|
|
15
|
+
* {@link evalFieldPredicate}). This expresses both:
|
|
16
|
+
* - cascades — `record.country == 'cn'` (the dependent list narrows as the
|
|
17
|
+
* controlling field changes), and
|
|
18
|
+
* - role/context gating — `'admin' in current_user.roles`.
|
|
19
|
+
*
|
|
20
|
+
* A field declares which sibling fields its option list reacts to via
|
|
21
|
+
* `dependsOn` (aligns with `@objectstack/spec` Field.dependsOn — the same knob
|
|
22
|
+
* lookups use). While any dependency is empty the list is *gated*: we surface a
|
|
23
|
+
* "select the parent first" hint instead of an unfiltered set, mirroring the
|
|
24
|
+
* dependent-lookup UX (#2215).
|
|
25
|
+
*
|
|
26
|
+
* Evaluation is fail-open (a broken/absent predicate keeps the option) — the
|
|
27
|
+
* same safe default as field visibility. Client-side hiding is UX, not a
|
|
28
|
+
* security boundary: when an option is gated for access-control reasons the
|
|
29
|
+
* server must also reject writes of its value.
|
|
30
|
+
*/
|
|
31
|
+
import { evalFieldPredicate } from './fieldRules.js';
|
|
32
|
+
/**
|
|
33
|
+
* Normalize a field's `dependsOn` to the list of sibling field names it reacts
|
|
34
|
+
* to. The lookup-only `{field,param}` form contributes just its `field`.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveDependsOnFields(dependsOn) {
|
|
37
|
+
if (!dependsOn)
|
|
38
|
+
return [];
|
|
39
|
+
const arr = Array.isArray(dependsOn) ? dependsOn : [dependsOn];
|
|
40
|
+
return arr
|
|
41
|
+
.map((d) => (typeof d === 'string' ? d : d && typeof d === 'object' ? d.field : null))
|
|
42
|
+
.filter((f) => typeof f === 'string' && f.length > 0);
|
|
43
|
+
}
|
|
44
|
+
/** A value counts as "empty" (dependency unmet) when nullish, blank, or an empty array. */
|
|
45
|
+
function isEmptyValue(v) {
|
|
46
|
+
return v === undefined || v === null || v === '' || (Array.isArray(v) && v.length === 0);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* True when at least one `dependsOn` field is empty in the record — the option
|
|
50
|
+
* list is gated and the widget should prompt for the parent rather than show an
|
|
51
|
+
* unfiltered set. Returns false when there are no dependencies.
|
|
52
|
+
*/
|
|
53
|
+
export function isOptionGroupGated(dependsOn, record) {
|
|
54
|
+
const fields = resolveDependsOnFields(dependsOn);
|
|
55
|
+
return fields.some((f) => isEmptyValue(record[f]));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Filter options by their per-option `visibleWhen` predicate, evaluated against
|
|
59
|
+
* the live `record` (+ optional `scope`, e.g. `{ current_user }`). Options with
|
|
60
|
+
* no predicate are always kept; a predicate that errors fails open (kept).
|
|
61
|
+
*/
|
|
62
|
+
export function resolveVisibleOptions(options, record, scope) {
|
|
63
|
+
if (!options || options.length === 0)
|
|
64
|
+
return [];
|
|
65
|
+
return options.filter((o) => o?.visibleWhen == null ? true : evalFieldPredicate(o.visibleWhen, record, true, undefined, scope));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Whether a scalar field value is still valid given the currently-visible
|
|
69
|
+
* options — used to decide a cascade clear (parent changed, child's old choice
|
|
70
|
+
* no longer offered). An empty value is always "valid" (nothing to clear).
|
|
71
|
+
*/
|
|
72
|
+
export function isValueStillOffered(value, visibleOptions) {
|
|
73
|
+
if (isEmptyValue(value))
|
|
74
|
+
return true;
|
|
75
|
+
if (Array.isArray(value)) {
|
|
76
|
+
// Multi-select: valid only when every selected value is still offered.
|
|
77
|
+
return value.every((v) => visibleOptions.some((o) => o.value === v));
|
|
78
|
+
}
|
|
79
|
+
return visibleOptions.some((o) => o.value === value);
|
|
80
|
+
}
|
|
@@ -28,10 +28,18 @@ export const STYLE_BREAKPOINTS = {
|
|
|
28
28
|
const camelToKebab = (k) => k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
29
29
|
/** Style-map → `prop: value;` declarations. Values pass through **verbatim** —
|
|
30
30
|
* that is the whole point: arbitrary values (`13px`, `#1a2b3c`) and design
|
|
31
|
-
* tokens (`var(--space-6)`) work with zero build step.
|
|
31
|
+
* tokens (`var(--space-6)`) work with zero build step.
|
|
32
|
+
*
|
|
33
|
+
* `!important` on every declaration is deliberate: this scoped rule and a
|
|
34
|
+
* component's own base utility class (e.g. `flex`/`grid` from the layout
|
|
35
|
+
* renderer) both land on the same element with equal (single-class)
|
|
36
|
+
* specificity, so without it the winner depends on stylesheet injection
|
|
37
|
+
* order — which flips between dev (HMR-injected Tailwind CSS) and prod
|
|
38
|
+
* (head `<link>` precedes body). An explicit per-node style is always the
|
|
39
|
+
* more specific author intent and must win deterministically. */
|
|
32
40
|
const declarations = (m) => Object.entries(m)
|
|
33
41
|
.filter(([, v]) => v != null && v !== '')
|
|
34
|
-
.map(([k, v]) => `${camelToKebab(k)}: ${typeof v === 'number' ? String(v) : v};`)
|
|
42
|
+
.map(([k, v]) => `${camelToKebab(k)}: ${typeof v === 'number' ? String(v) : v} !important;`)
|
|
35
43
|
.join(' ');
|
|
36
44
|
/** True when a node actually carries responsive styles worth compiling. */
|
|
37
45
|
export function hasResponsiveStyles(rs) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "13.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,15 +25,15 @@
|
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@objectstack/formula": "^
|
|
29
|
-
"@objectstack/spec": "^
|
|
28
|
+
"@objectstack/formula": "^12.3.0",
|
|
29
|
+
"@objectstack/spec": "^13.0.0",
|
|
30
30
|
"lodash": "^4.18.1",
|
|
31
31
|
"zod": "^4.4.3",
|
|
32
|
-
"@object-ui/types": "
|
|
32
|
+
"@object-ui/types": "13.1.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"typescript": "^6.0.3",
|
|
36
|
-
"vitest": "^4.1.
|
|
36
|
+
"vitest": "^4.1.10"
|
|
37
37
|
},
|
|
38
38
|
"keywords": [
|
|
39
39
|
"objectui",
|