@object-ui/core 11.5.0 → 13.2.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 CHANGED
@@ -1,5 +1,62 @@
1
1
  # @object-ui/core
2
2
 
3
+ ## 13.2.0
4
+
5
+ ### Patch Changes
6
+
7
+ - @object-ui/types@13.2.0
8
+
9
+ ## 13.1.0
10
+
11
+ ### Patch Changes
12
+
13
+ - @object-ui/types@13.1.0
14
+
15
+ ## 13.0.0
16
+
17
+ ### Patch Changes
18
+
19
+ - Updated dependencies [619097e]
20
+ - @object-ui/types@13.0.0
21
+
22
+ ## 12.1.0
23
+
24
+ ### Patch Changes
25
+
26
+ - Updated dependencies [c31874d]
27
+ - @object-ui/types@12.1.0
28
+
29
+ ## 12.0.0
30
+
31
+ ### Minor Changes
32
+
33
+ - 226fde9: Cascading & role-gated `select` options (#2284).
34
+
35
+ `select` options now accept a per-option `visibleWhen` CEL predicate — the option
36
+ is offered only when it evaluates TRUE against the live record **plus
37
+ `current_user`** (same engine/env as a field-level `visibleWhen`). Combined with a
38
+ field-level `dependsOn`, this drives dependent selects (country → province → city)
39
+ and role/context gating with no bespoke matrix — the same primitives dependent
40
+ lookups (#2215) already use.
41
+
42
+ - `@object-ui/core` exposes `resolveVisibleOptions` / `isOptionGroupGated` /
43
+ `resolveDependsOnFields` / `isValueStillOffered` (evaluator), reusing the
44
+ canonical `evalFieldPredicate`.
45
+ - The form renderer narrows a dependent select's option list, gates the control
46
+ with a "Select {parent} first" hint while a `dependsOn` field is empty, and
47
+ clears a now-invalid value when the parent changes.
48
+ - The standalone `SelectField` widget applies the same resolution via
49
+ `dependentValues` + the global predicate scope.
50
+
51
+ Client-side hiding is UX, not authorization: gate authorization-sensitive option
52
+ values on the server too. Aligns with `@objectstack/spec` `SelectOption.visibleWhen`.
53
+
54
+ ### Patch Changes
55
+
56
+ - Updated dependencies [226fde9]
57
+ - Updated dependencies [e4de456]
58
+ - @object-ui/types@12.0.0
59
+
3
60
  ## 11.5.0
4
61
 
5
62
  ### Patch Changes
@@ -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';
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/core",
3
- "version": "11.5.0",
3
+ "version": "13.2.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": "^11.5.0",
29
- "@objectstack/spec": "^11.7.0",
28
+ "@objectstack/formula": "^14.3.0",
29
+ "@objectstack/spec": "^14.3.0",
30
30
  "lodash": "^4.18.1",
31
31
  "zod": "^4.4.3",
32
- "@object-ui/types": "11.5.0"
32
+ "@object-ui/types": "13.2.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "typescript": "^6.0.3",
36
- "vitest": "^4.1.9"
36
+ "vitest": "^4.1.10"
37
37
  },
38
38
  "keywords": [
39
39
  "objectui",