@object-ui/core 16.0.0 → 17.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 +1589 -1
- package/dist/actions/ActionRunner.d.ts +88 -16
- package/dist/actions/ActionRunner.js +164 -81
- package/dist/actions/actionKeys.d.ts +118 -0
- package/dist/actions/actionKeys.js +243 -0
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/adapters/ApiDataSource.d.ts +11 -1
- package/dist/adapters/ApiDataSource.js +21 -1
- package/dist/adapters/ValueDataSource.d.ts +10 -2
- package/dist/adapters/ValueDataSource.js +9 -0
- package/dist/adapters/batchTransaction.d.ts +49 -0
- package/dist/adapters/batchTransaction.js +118 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js +1 -0
- package/dist/evaluator/ExpressionEvaluator.d.ts +10 -0
- package/dist/evaluator/ExpressionEvaluator.js +48 -0
- package/dist/evaluator/SafeExpressionParser.js +5 -2
- package/dist/evaluator/fieldRules.d.ts +0 -2
- package/dist/evaluator/fieldRules.js +3 -2
- package/dist/evaluator/optionLint.js +5 -2
- package/dist/evaluator/optionRules.d.ts +35 -6
- package/dist/evaluator/optionRules.js +18 -23
- package/dist/index.d.ts +5 -0
- package/dist/index.js +8 -0
- package/dist/registry/Registry.d.ts +81 -2
- package/dist/registry/Registry.js +102 -2
- package/dist/registry/public-blocks.js +52 -1
- package/dist/theme/ThemeEngine.js +0 -12
- package/dist/utils/chart-series.d.ts +28 -0
- package/dist/utils/chart-series.js +55 -0
- package/dist/utils/column-identity.d.ts +110 -0
- package/dist/utils/column-identity.js +210 -0
- package/dist/utils/dashboard-filters.d.ts +2 -2
- package/dist/utils/dataset-format.d.ts +18 -1
- package/dist/utils/dataset-format.js +14 -1
- package/dist/utils/expand-fields.js +8 -8
- package/dist/utils/export-filename.js +4 -1
- package/dist/utils/filter-converter.d.ts +47 -0
- package/dist/utils/filter-converter.js +94 -7
- package/dist/utils/filter-tokens.d.ts +101 -0
- package/dist/utils/filter-tokens.js +173 -0
- package/dist/utils/managedBy.d.ts +119 -0
- package/dist/utils/managedBy.js +147 -0
- package/dist/utils/normalize-list-view.d.ts +99 -0
- package/dist/utils/normalize-list-view.js +255 -0
- package/dist/utils/record-title.js +0 -0
- package/dist/utils/sort-values.d.ts +52 -0
- package/dist/utils/sort-values.js +125 -0
- package/dist/validation/index.d.ts +9 -1
- package/dist/validation/index.js +9 -1
- package/dist/validation/validators/index.d.ts +7 -1
- package/dist/validation/validators/index.js +7 -1
- package/dist/validation/validators/object-validation-engine.d.ts +151 -31
- package/dist/validation/validators/object-validation-engine.js +275 -142
- package/package.json +4 -4
|
@@ -298,7 +298,10 @@ export class SafeExpressionParser {
|
|
|
298
298
|
this.skipWhitespace();
|
|
299
299
|
// eslint-disable-next-line no-constant-condition
|
|
300
300
|
while (true) {
|
|
301
|
-
|
|
301
|
+
// Every branch below either assigns `op` or `break`s out of the loop, so
|
|
302
|
+
// `op` is always set by the time the switch reads it (no null initializer
|
|
303
|
+
// needed).
|
|
304
|
+
let op;
|
|
302
305
|
if (this.peek() === '=' && this.peek(1) === '=' && this.peek(2) === '=') {
|
|
303
306
|
op = '===';
|
|
304
307
|
this.pos += 3;
|
|
@@ -648,7 +651,7 @@ export class SafeExpressionParser {
|
|
|
648
651
|
// Optional exponent: e+5, E-3
|
|
649
652
|
if (/[eE]/.test(this.source[this.pos] ?? '')) {
|
|
650
653
|
this.pos++; // consume 'e' or 'E'
|
|
651
|
-
if (/[
|
|
654
|
+
if (/[+-]/.test(this.source[this.pos] ?? ''))
|
|
652
655
|
this.pos++; // optional sign
|
|
653
656
|
let expDigits = 0;
|
|
654
657
|
while (this.pos < this.source.length && /\d/.test(this.source[this.pos])) {
|
|
@@ -37,8 +37,6 @@ export declare function resolveFieldRuleState(rules: {
|
|
|
37
37
|
visibleWhen?: FieldRulePredicate;
|
|
38
38
|
readonlyWhen?: FieldRulePredicate;
|
|
39
39
|
requiredWhen?: FieldRulePredicate;
|
|
40
|
-
/** Back-compat alias of `requiredWhen` (spec @deprecated). */
|
|
41
|
-
conditionalRequired?: FieldRulePredicate;
|
|
42
40
|
}, record: Record<string, unknown>, statics: {
|
|
43
41
|
required?: boolean;
|
|
44
42
|
readonly?: boolean;
|
|
@@ -79,8 +79,9 @@ export function resolveFieldRuleState(rules, record, statics, previous, scope) {
|
|
|
79
79
|
(rules.readonlyWhen != null
|
|
80
80
|
? evalFieldPredicate(rules.readonlyWhen, record, false, previous, scope)
|
|
81
81
|
: false);
|
|
82
|
-
const requiredPred = rules.requiredWhen ?? rules.conditionalRequired;
|
|
83
82
|
const required = statics.required === true ||
|
|
84
|
-
(
|
|
83
|
+
(rules.requiredWhen != null
|
|
84
|
+
? evalFieldPredicate(rules.requiredWhen, record, false, previous, scope)
|
|
85
|
+
: false);
|
|
85
86
|
return { visible, readonly, required };
|
|
86
87
|
}
|
|
@@ -109,7 +109,10 @@ export function lintOptionPredicates(fields) {
|
|
|
109
109
|
continue;
|
|
110
110
|
knownFields.add(f.name);
|
|
111
111
|
if (OPTION_FIELD_TYPES.has(fieldType(f)) && f.options && f.options.length > 0) {
|
|
112
|
-
|
|
112
|
+
// Values stringify at this boundary (#3090 widened OptionLike.value):
|
|
113
|
+
// the domain is compared against CEL literal TEXT, so the canonical
|
|
114
|
+
// string form is the right footing for numeric/boolean options too.
|
|
115
|
+
domainByField.set(f.name, new Set(f.options.map((o) => String(o.value))));
|
|
113
116
|
}
|
|
114
117
|
}
|
|
115
118
|
const issues = [];
|
|
@@ -123,7 +126,7 @@ export function lintOptionPredicates(fields) {
|
|
|
123
126
|
const source = predicateSource(opt.visibleWhen);
|
|
124
127
|
if (!source.trim())
|
|
125
128
|
continue;
|
|
126
|
-
const option = opt.value;
|
|
129
|
+
const option = String(opt.value); // report key — display form (#3090)
|
|
127
130
|
// 1. Syntax — canonical CEL parse (no schema hint = no scope false-positives).
|
|
128
131
|
const parsed = validateExpression('predicate', source);
|
|
129
132
|
if (!parsed.ok) {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* security boundary: when an option is gated for access-control reasons the
|
|
29
29
|
* server must also reject writes of its value.
|
|
30
30
|
*/
|
|
31
|
+
import type { DependsOnInput } from '@object-ui/types';
|
|
31
32
|
import { type FieldRulePredicate } from './fieldRules.js';
|
|
32
33
|
/**
|
|
33
34
|
* Minimal shape of a select/radio option this module reads. Deliberately has no
|
|
@@ -36,15 +37,21 @@ import { type FieldRulePredicate } from './fieldRules.js';
|
|
|
36
37
|
*/
|
|
37
38
|
export interface OptionLike {
|
|
38
39
|
label: string;
|
|
39
|
-
value:
|
|
40
|
+
/** Widened with `SelectOption.value` (#3090): the option engines treat the
|
|
41
|
+
* value opaquely (gate/filter by `visibleWhen`, compare by identity), so a
|
|
42
|
+
* numeric/boolean authored value flows through unchanged. */
|
|
43
|
+
value: string | number | boolean;
|
|
40
44
|
/** Per-option visibility predicate (CEL). Omit = always available. */
|
|
41
45
|
visibleWhen?: FieldRulePredicate;
|
|
42
46
|
}
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* A field's `dependsOn` as authored — imported from `@object-ui/types`, where
|
|
49
|
+
* `FormField.dependsOn` is declared with it (framework#4074; formerly a local
|
|
50
|
+
* declaration that only the reader knew about, while the public
|
|
51
|
+
* `FormField.dependsOn` said `string`). Re-exported for this module's existing
|
|
52
|
+
* consumers.
|
|
53
|
+
*/
|
|
54
|
+
export type { DependsOnInput } from '@object-ui/types';
|
|
48
55
|
/**
|
|
49
56
|
* Normalize a field's `dependsOn` to the list of sibling field names it reacts
|
|
50
57
|
* to. The lookup-only `{field,param}` form contributes just its `field`.
|
|
@@ -68,3 +75,25 @@ export declare function resolveVisibleOptions<T extends OptionLike>(options: rea
|
|
|
68
75
|
* no longer offered). An empty value is always "valid" (nothing to clear).
|
|
69
76
|
*/
|
|
70
77
|
export declare function isValueStillOffered(value: unknown, visibleOptions: readonly OptionLike[]): boolean;
|
|
78
|
+
/** The resolved state of a cascading / role-gated option list. */
|
|
79
|
+
export interface CascadingOptions<T extends OptionLike> {
|
|
80
|
+
/** Offered options after `visibleWhen` filtering. Empty while gated. */
|
|
81
|
+
options: T[];
|
|
82
|
+
/** True when a declared `dependsOn` field is empty — the list is gated. */
|
|
83
|
+
gated: boolean;
|
|
84
|
+
/** Normalized `dependsOn` field names (drives the "select the parent first" hint). */
|
|
85
|
+
dependsOnFields: string[];
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Resolve an option field's offered set in one shot: normalize `dependsOn`,
|
|
89
|
+
* decide whether the list is gated (a controlling field is still empty), and —
|
|
90
|
+
* unless gated — filter by each option's `visibleWhen`. The single source of
|
|
91
|
+
* truth behind the option widgets' `useCascadingOptions` hook and the form
|
|
92
|
+
* renderer's inline pre-filter / cascade-clear, so gating and filtering can
|
|
93
|
+
* never drift between them (#2715).
|
|
94
|
+
*
|
|
95
|
+
* `gated` is true only when there IS a `dependsOn` and one of its fields is
|
|
96
|
+
* empty; a field with no `dependsOn` is never gated (its options are filtered
|
|
97
|
+
* purely by `visibleWhen`). Callers format their own hint from `dependsOnFields`.
|
|
98
|
+
*/
|
|
99
|
+
export declare function resolveCascadingOptions<T extends OptionLike>(rawOptions: readonly T[] | undefined | null, record: Record<string, unknown>, dependsOn: DependsOnInput, scope?: Record<string, unknown>): CascadingOptions<T>;
|
|
@@ -5,29 +5,6 @@
|
|
|
5
5
|
* This source code is licensed under the MIT license found in the
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
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.positions`.
|
|
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
8
|
import { evalFieldPredicate } from './fieldRules.js';
|
|
32
9
|
/**
|
|
33
10
|
* Normalize a field's `dependsOn` to the list of sibling field names it reacts
|
|
@@ -78,3 +55,21 @@ export function isValueStillOffered(value, visibleOptions) {
|
|
|
78
55
|
}
|
|
79
56
|
return visibleOptions.some((o) => o.value === value);
|
|
80
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Resolve an option field's offered set in one shot: normalize `dependsOn`,
|
|
60
|
+
* decide whether the list is gated (a controlling field is still empty), and —
|
|
61
|
+
* unless gated — filter by each option's `visibleWhen`. The single source of
|
|
62
|
+
* truth behind the option widgets' `useCascadingOptions` hook and the form
|
|
63
|
+
* renderer's inline pre-filter / cascade-clear, so gating and filtering can
|
|
64
|
+
* never drift between them (#2715).
|
|
65
|
+
*
|
|
66
|
+
* `gated` is true only when there IS a `dependsOn` and one of its fields is
|
|
67
|
+
* empty; a field with no `dependsOn` is never gated (its options are filtered
|
|
68
|
+
* purely by `visibleWhen`). Callers format their own hint from `dependsOnFields`.
|
|
69
|
+
*/
|
|
70
|
+
export function resolveCascadingOptions(rawOptions, record, dependsOn, scope) {
|
|
71
|
+
const dependsOnFields = resolveDependsOnFields(dependsOn);
|
|
72
|
+
const gated = dependsOnFields.length > 0 && isOptionGroupGated(dependsOn, record);
|
|
73
|
+
const options = gated ? [] : resolveVisibleOptions(rawOptions, record, scope);
|
|
74
|
+
return { options, gated, dependsOnFields };
|
|
75
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,8 +14,11 @@ export * from './registry/WidgetRegistry.js';
|
|
|
14
14
|
export * from './validation/index.js';
|
|
15
15
|
export * from './builder/schema-builder.js';
|
|
16
16
|
export * from './utils/filter-converter.js';
|
|
17
|
+
export * from './utils/managedBy.js';
|
|
17
18
|
export * from './utils/extract-records.js';
|
|
18
19
|
export * from './utils/expand-fields.js';
|
|
20
|
+
export * from './utils/column-identity.js';
|
|
21
|
+
export * from './utils/sort-values.js';
|
|
19
22
|
export * from './evaluator/index.js';
|
|
20
23
|
export * from './actions/index.js';
|
|
21
24
|
export * from './query/index.js';
|
|
@@ -39,6 +42,7 @@ export * from './runtime/capabilities.js';
|
|
|
39
42
|
export { composeStacks } from '@objectstack/spec';
|
|
40
43
|
export * from './utils/drill-down.js';
|
|
41
44
|
export * from './utils/date-macros.js';
|
|
45
|
+
export * from './utils/filter-tokens.js';
|
|
42
46
|
export * from './utils/dashboard-filters.js';
|
|
43
47
|
export * from './utils/merge-filters.js';
|
|
44
48
|
export * from './utils/compare-to.js';
|
|
@@ -47,3 +51,4 @@ export * from './utils/dataset-format.js';
|
|
|
47
51
|
export * from './utils/record-title.js';
|
|
48
52
|
export * from './utils/export-filename.js';
|
|
49
53
|
export * from './utils/reference-keys.js';
|
|
54
|
+
export * from './utils/normalize-list-view.js';
|
package/dist/index.js
CHANGED
|
@@ -13,8 +13,11 @@ export * from './registry/WidgetRegistry.js';
|
|
|
13
13
|
export * from './validation/index.js';
|
|
14
14
|
export * from './builder/schema-builder.js';
|
|
15
15
|
export * from './utils/filter-converter.js';
|
|
16
|
+
export * from './utils/managedBy.js';
|
|
16
17
|
export * from './utils/extract-records.js';
|
|
17
18
|
export * from './utils/expand-fields.js';
|
|
19
|
+
export * from './utils/column-identity.js';
|
|
20
|
+
export * from './utils/sort-values.js';
|
|
18
21
|
export * from './evaluator/index.js';
|
|
19
22
|
export * from './actions/index.js';
|
|
20
23
|
export * from './query/index.js';
|
|
@@ -38,6 +41,10 @@ export * from './runtime/capabilities.js';
|
|
|
38
41
|
export { composeStacks } from '@objectstack/spec';
|
|
39
42
|
export * from './utils/drill-down.js';
|
|
40
43
|
export * from './utils/date-macros.js';
|
|
44
|
+
// Session-scoped filter placeholders ({current_user_id} / {current_org_id})
|
|
45
|
+
// plus `resolveFilterPlaceholders`, the single entry point every surface
|
|
46
|
+
// should call so no vocabulary is silently skipped (framework #3574).
|
|
47
|
+
export * from './utils/filter-tokens.js';
|
|
41
48
|
export * from './utils/dashboard-filters.js';
|
|
42
49
|
export * from './utils/merge-filters.js';
|
|
43
50
|
export * from './utils/compare-to.js';
|
|
@@ -46,3 +53,4 @@ export * from './utils/dataset-format.js';
|
|
|
46
53
|
export * from './utils/record-title.js';
|
|
47
54
|
export * from './utils/export-filename.js';
|
|
48
55
|
export * from './utils/reference-keys.js';
|
|
56
|
+
export * from './utils/normalize-list-view.js';
|
|
@@ -62,6 +62,22 @@ export type ComponentConfig<T = any> = ComponentMeta & {
|
|
|
62
62
|
type: string;
|
|
63
63
|
component: ComponentRenderer<T>;
|
|
64
64
|
};
|
|
65
|
+
/**
|
|
66
|
+
* A CONTRACT-surface entry (ADR-0080), as returned by
|
|
67
|
+
* {@link Registry.getPublicConfigs}.
|
|
68
|
+
*
|
|
69
|
+
* Same shape as {@link ComponentConfig} except `component` is absent while the
|
|
70
|
+
* entry is still a pending `registerLazy` stub: the plugin module has not been
|
|
71
|
+
* imported yet, so there is no renderer to hand out. Consumers render such an
|
|
72
|
+
* entry through `SchemaRenderer`, which triggers the loader and shows a
|
|
73
|
+
* placeholder in the meantime (objectui#2953).
|
|
74
|
+
*/
|
|
75
|
+
export type PublicComponentConfig<T = any> = ComponentMeta & {
|
|
76
|
+
type: string;
|
|
77
|
+
component?: ComponentRenderer<T>;
|
|
78
|
+
/** True while this entry is a `registerLazy` stub whose loader has not run. */
|
|
79
|
+
lazy?: boolean;
|
|
80
|
+
};
|
|
65
81
|
/**
|
|
66
82
|
* Lazy loader function used by `Registry.registerLazy`. The loader is invoked
|
|
67
83
|
* the first time a missing component type is requested through `getAsync`/the
|
|
@@ -79,6 +95,14 @@ export declare class Registry<T = any> {
|
|
|
79
95
|
* load completes.
|
|
80
96
|
*/
|
|
81
97
|
private listeners;
|
|
98
|
+
/**
|
|
99
|
+
* Bumped whenever the set of KNOWN types changes — a registration, an
|
|
100
|
+
* unregistration, or a new lazy stub. A cheap cache key for consumers that
|
|
101
|
+
* derive something from the whole registry (whitelists, manifests, palettes)
|
|
102
|
+
* and need to rebuild when it grows. Counting types is not a substitute: a
|
|
103
|
+
* registration paired with an unregistration leaves the count untouched.
|
|
104
|
+
*/
|
|
105
|
+
private version;
|
|
82
106
|
/**
|
|
83
107
|
* Register a component with optional namespace support.
|
|
84
108
|
* If namespace is provided in meta, the component will be registered as "namespace:type".
|
|
@@ -141,6 +165,11 @@ export declare class Registry<T = any> {
|
|
|
141
165
|
*/
|
|
142
166
|
subscribe(listener: () => void): () => void;
|
|
143
167
|
private notify;
|
|
168
|
+
/**
|
|
169
|
+
* Monotonic counter of changes to the set of known types. Rebuild anything
|
|
170
|
+
* derived from the whole registry when this moves — see {@link getKnownTypes}.
|
|
171
|
+
*/
|
|
172
|
+
getVersion(): number;
|
|
144
173
|
/**
|
|
145
174
|
* Get a component by type. Supports both namespaced and non-namespaced lookups.
|
|
146
175
|
*
|
|
@@ -164,6 +193,13 @@ export declare class Registry<T = any> {
|
|
|
164
193
|
/**
|
|
165
194
|
* Get component configuration by type with namespace support.
|
|
166
195
|
*
|
|
196
|
+
* LOADED registrations only — a type that exists solely as a `registerLazy`
|
|
197
|
+
* stub returns `undefined` here, because callers read `.component` off the
|
|
198
|
+
* result and a stub has no renderer until its loader runs. The *contract*
|
|
199
|
+
* question ("is this tag part of the public surface?") must not depend on
|
|
200
|
+
* load order, so {@link getPublicConfigs} resolves lazy stubs separately
|
|
201
|
+
* (objectui#2953). Use {@link hasLazy} / {@link loadLazy} to reach a stub.
|
|
202
|
+
*
|
|
167
203
|
* @param type - Component type (e.g., 'button' or 'ui:button')
|
|
168
204
|
* @param namespace - Optional namespace for lookup priority
|
|
169
205
|
* @returns Component configuration or undefined
|
|
@@ -178,23 +214,66 @@ export declare class Registry<T = any> {
|
|
|
178
214
|
*/
|
|
179
215
|
has(type: string, namespace?: string): boolean;
|
|
180
216
|
/**
|
|
181
|
-
* Get all
|
|
217
|
+
* Get all LOADED component types — what can be rendered right now.
|
|
218
|
+
*
|
|
219
|
+
* Pending `registerLazy` stubs are not included; use {@link getKnownTypes}
|
|
220
|
+
* for the question "is this a type this app knows about", which is what a
|
|
221
|
+
* whitelist or manifest wants (objectui#2953).
|
|
182
222
|
*
|
|
183
223
|
* @returns Array of all component type identifiers
|
|
184
224
|
*/
|
|
185
225
|
getAllTypes(): string[];
|
|
226
|
+
/**
|
|
227
|
+
* Every type the registry can resolve — loaded registrations PLUS pending
|
|
228
|
+
* lazy stubs, deduped. Both the bare and namespaced keys are included, as
|
|
229
|
+
* they are in {@link getAllTypes}.
|
|
230
|
+
*
|
|
231
|
+
* This is the set to build a whitelist or a manifest from. Keying one off
|
|
232
|
+
* `getAllTypes()` instead makes it depend on which plugin chunks happen to
|
|
233
|
+
* have loaded: a lazily-registered block gets rejected as unknown, and
|
|
234
|
+
* whether it does varies by session (objectui#2953).
|
|
235
|
+
*/
|
|
236
|
+
getKnownTypes(): string[];
|
|
237
|
+
/**
|
|
238
|
+
* Metadata for `type` — from the loaded registration when there is one,
|
|
239
|
+
* otherwise from a pending `registerLazy` stub.
|
|
240
|
+
*
|
|
241
|
+
* Use this when you need to know what a type IS (namespace, container-ness,
|
|
242
|
+
* declared inputs). {@link getConfig} answers a different question — "can I
|
|
243
|
+
* render this right now" — and is deliberately loaded-only, since callers
|
|
244
|
+
* read `.component` off it.
|
|
245
|
+
*
|
|
246
|
+
* A stub carries only what `registerLazy` was given, so `inputs` is usually
|
|
247
|
+
* absent until the chunk loads. Consumers should treat that as "not yet
|
|
248
|
+
* known", not as "declares no props".
|
|
249
|
+
*/
|
|
250
|
+
getMeta(type: string, namespace?: string): ComponentMeta | undefined;
|
|
186
251
|
/**
|
|
187
252
|
* Get all registered component configurations.
|
|
188
253
|
*
|
|
189
254
|
* @returns Array of all component configurations
|
|
190
255
|
*/
|
|
191
256
|
getAllConfigs(): ComponentConfig<T>[];
|
|
257
|
+
/**
|
|
258
|
+
* Resolve `tag` for the CONTRACT surface: the loaded registration when the
|
|
259
|
+
* component is already in the registry, otherwise the metadata of a pending
|
|
260
|
+
* `registerLazy` stub.
|
|
261
|
+
*
|
|
262
|
+
* A block registered lazily is a first-class member of the contract — the
|
|
263
|
+
* loader is recorded at boot and the plugin chunk is imported on first use —
|
|
264
|
+
* so contract membership must not hinge on whether that import happened to
|
|
265
|
+
* have run yet (objectui#2953).
|
|
266
|
+
*/
|
|
267
|
+
private getContractConfig;
|
|
192
268
|
/**
|
|
193
269
|
* Get the curated PUBLIC-tier component configs (ADR-0080) — those registered
|
|
194
270
|
* with `tier: 'public'`. This is the contract/AI-vocabulary surface, a subset
|
|
195
271
|
* of the full rendering capability returned by {@link getAllConfigs}.
|
|
272
|
+
*
|
|
273
|
+
* Includes blocks that are only lazily registered so far; those come back
|
|
274
|
+
* with `lazy: true` and no `component` (see {@link PublicComponentConfig}).
|
|
196
275
|
*/
|
|
197
|
-
getPublicConfigs():
|
|
276
|
+
getPublicConfigs(): PublicComponentConfig<T>[];
|
|
198
277
|
/**
|
|
199
278
|
* Get all components in a specific namespace.
|
|
200
279
|
*
|
|
@@ -31,6 +31,19 @@ export class Registry {
|
|
|
31
31
|
writable: true,
|
|
32
32
|
value: new Set()
|
|
33
33
|
});
|
|
34
|
+
/**
|
|
35
|
+
* Bumped whenever the set of KNOWN types changes — a registration, an
|
|
36
|
+
* unregistration, or a new lazy stub. A cheap cache key for consumers that
|
|
37
|
+
* derive something from the whole registry (whitelists, manifests, palettes)
|
|
38
|
+
* and need to rebuild when it grows. Counting types is not a substitute: a
|
|
39
|
+
* registration paired with an unregistration leaves the count untouched.
|
|
40
|
+
*/
|
|
41
|
+
Object.defineProperty(this, "version", {
|
|
42
|
+
enumerable: true,
|
|
43
|
+
configurable: true,
|
|
44
|
+
writable: true,
|
|
45
|
+
value: 0
|
|
46
|
+
});
|
|
34
47
|
}
|
|
35
48
|
/**
|
|
36
49
|
* Register a component with optional namespace support.
|
|
@@ -144,6 +157,10 @@ export class Registry {
|
|
|
144
157
|
if (meta?.namespace && !meta?.skipFallback) {
|
|
145
158
|
this.lazyEntries.set(type, entry);
|
|
146
159
|
}
|
|
160
|
+
// Bump the version but do NOT notify: the set of KNOWN types grew, so
|
|
161
|
+
// whitelists and manifests must rebuild, but nothing new can render yet —
|
|
162
|
+
// waking every subscriber for each stub at boot would be pure churn.
|
|
163
|
+
this.version++;
|
|
147
164
|
}
|
|
148
165
|
/**
|
|
149
166
|
* Returns true if `type` (or its namespaced form) has a registered lazy
|
|
@@ -186,6 +203,7 @@ export class Registry {
|
|
|
186
203
|
};
|
|
187
204
|
}
|
|
188
205
|
notify() {
|
|
206
|
+
this.version++;
|
|
189
207
|
for (const listener of this.listeners) {
|
|
190
208
|
try {
|
|
191
209
|
listener();
|
|
@@ -195,6 +213,13 @@ export class Registry {
|
|
|
195
213
|
}
|
|
196
214
|
}
|
|
197
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Monotonic counter of changes to the set of known types. Rebuild anything
|
|
218
|
+
* derived from the whole registry when this moves — see {@link getKnownTypes}.
|
|
219
|
+
*/
|
|
220
|
+
getVersion() {
|
|
221
|
+
return this.version;
|
|
222
|
+
}
|
|
198
223
|
/**
|
|
199
224
|
* Get a component by type. Supports both namespaced and non-namespaced lookups.
|
|
200
225
|
*
|
|
@@ -226,6 +251,13 @@ export class Registry {
|
|
|
226
251
|
/**
|
|
227
252
|
* Get component configuration by type with namespace support.
|
|
228
253
|
*
|
|
254
|
+
* LOADED registrations only — a type that exists solely as a `registerLazy`
|
|
255
|
+
* stub returns `undefined` here, because callers read `.component` off the
|
|
256
|
+
* result and a stub has no renderer until its loader runs. The *contract*
|
|
257
|
+
* question ("is this tag part of the public surface?") must not depend on
|
|
258
|
+
* load order, so {@link getPublicConfigs} resolves lazy stubs separately
|
|
259
|
+
* (objectui#2953). Use {@link hasLazy} / {@link loadLazy} to reach a stub.
|
|
260
|
+
*
|
|
229
261
|
* @param type - Component type (e.g., 'button' or 'ui:button')
|
|
230
262
|
* @param namespace - Optional namespace for lookup priority
|
|
231
263
|
* @returns Component configuration or undefined
|
|
@@ -256,13 +288,47 @@ export class Registry {
|
|
|
256
288
|
return this.components.has(type);
|
|
257
289
|
}
|
|
258
290
|
/**
|
|
259
|
-
* Get all
|
|
291
|
+
* Get all LOADED component types — what can be rendered right now.
|
|
292
|
+
*
|
|
293
|
+
* Pending `registerLazy` stubs are not included; use {@link getKnownTypes}
|
|
294
|
+
* for the question "is this a type this app knows about", which is what a
|
|
295
|
+
* whitelist or manifest wants (objectui#2953).
|
|
260
296
|
*
|
|
261
297
|
* @returns Array of all component type identifiers
|
|
262
298
|
*/
|
|
263
299
|
getAllTypes() {
|
|
264
300
|
return Array.from(this.components.keys());
|
|
265
301
|
}
|
|
302
|
+
/**
|
|
303
|
+
* Every type the registry can resolve — loaded registrations PLUS pending
|
|
304
|
+
* lazy stubs, deduped. Both the bare and namespaced keys are included, as
|
|
305
|
+
* they are in {@link getAllTypes}.
|
|
306
|
+
*
|
|
307
|
+
* This is the set to build a whitelist or a manifest from. Keying one off
|
|
308
|
+
* `getAllTypes()` instead makes it depend on which plugin chunks happen to
|
|
309
|
+
* have loaded: a lazily-registered block gets rejected as unknown, and
|
|
310
|
+
* whether it does varies by session (objectui#2953).
|
|
311
|
+
*/
|
|
312
|
+
getKnownTypes() {
|
|
313
|
+
return Array.from(new Set([...this.components.keys(), ...this.lazyEntries.keys()]));
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Metadata for `type` — from the loaded registration when there is one,
|
|
317
|
+
* otherwise from a pending `registerLazy` stub.
|
|
318
|
+
*
|
|
319
|
+
* Use this when you need to know what a type IS (namespace, container-ness,
|
|
320
|
+
* declared inputs). {@link getConfig} answers a different question — "can I
|
|
321
|
+
* render this right now" — and is deliberately loaded-only, since callers
|
|
322
|
+
* read `.component` off it.
|
|
323
|
+
*
|
|
324
|
+
* A stub carries only what `registerLazy` was given, so `inputs` is usually
|
|
325
|
+
* absent until the chunk loads. Consumers should treat that as "not yet
|
|
326
|
+
* known", not as "declares no props".
|
|
327
|
+
*/
|
|
328
|
+
getMeta(type, namespace) {
|
|
329
|
+
const key = namespace ? `${namespace}:${type}` : type;
|
|
330
|
+
return this.components.get(key) ?? this.lazyEntries.get(key)?.meta;
|
|
331
|
+
}
|
|
266
332
|
/**
|
|
267
333
|
* Get all registered component configurations.
|
|
268
334
|
*
|
|
@@ -271,10 +337,39 @@ export class Registry {
|
|
|
271
337
|
getAllConfigs() {
|
|
272
338
|
return Array.from(this.components.values());
|
|
273
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Resolve `tag` for the CONTRACT surface: the loaded registration when the
|
|
342
|
+
* component is already in the registry, otherwise the metadata of a pending
|
|
343
|
+
* `registerLazy` stub.
|
|
344
|
+
*
|
|
345
|
+
* A block registered lazily is a first-class member of the contract — the
|
|
346
|
+
* loader is recorded at boot and the plugin chunk is imported on first use —
|
|
347
|
+
* so contract membership must not hinge on whether that import happened to
|
|
348
|
+
* have run yet (objectui#2953).
|
|
349
|
+
*/
|
|
350
|
+
getContractConfig(tag) {
|
|
351
|
+
const loaded = this.components.get(tag);
|
|
352
|
+
if (loaded)
|
|
353
|
+
return loaded;
|
|
354
|
+
const entry = this.lazyEntries.get(tag);
|
|
355
|
+
if (!entry)
|
|
356
|
+
return undefined;
|
|
357
|
+
// Mirror registerLazy's key derivation so the canonical `type` matches the
|
|
358
|
+
// one `register()` will store once the loader runs — that keeps the dedupe
|
|
359
|
+
// below stable across the load. `tag` may already carry the namespace
|
|
360
|
+
// (lazyEntries holds both keys; curated tags like `record:details` are
|
|
361
|
+
// themselves colon-shaped), so don't prefix it twice.
|
|
362
|
+
const ns = entry.meta?.namespace;
|
|
363
|
+
const canonical = ns && !tag.startsWith(`${ns}:`) ? `${ns}:${tag}` : tag;
|
|
364
|
+
return { ...entry.meta, type: canonical, lazy: true };
|
|
365
|
+
}
|
|
274
366
|
/**
|
|
275
367
|
* Get the curated PUBLIC-tier component configs (ADR-0080) — those registered
|
|
276
368
|
* with `tier: 'public'`. This is the contract/AI-vocabulary surface, a subset
|
|
277
369
|
* of the full rendering capability returned by {@link getAllConfigs}.
|
|
370
|
+
*
|
|
371
|
+
* Includes blocks that are only lazily registered so far; those come back
|
|
372
|
+
* with `lazy: true` and no `component` (see {@link PublicComponentConfig}).
|
|
278
373
|
*/
|
|
279
374
|
getPublicConfigs() {
|
|
280
375
|
// Dedupe by the config's canonical (namespaced) `type` — a component is
|
|
@@ -292,12 +387,17 @@ export class Registry {
|
|
|
292
387
|
};
|
|
293
388
|
// Curated contract list first (stable, reviewable order) …
|
|
294
389
|
for (const tag of PUBLIC_BLOCKS)
|
|
295
|
-
add(tag, this.
|
|
390
|
+
add(tag, this.getContractConfig(tag));
|
|
296
391
|
// … plus any bare registration that opted in explicitly via `tier: 'public'`.
|
|
297
392
|
for (const [key, cfg] of this.components.entries()) {
|
|
298
393
|
if (cfg.tier === 'public' && !key.includes(':'))
|
|
299
394
|
add(key, cfg);
|
|
300
395
|
}
|
|
396
|
+
// … and the same opt-in for stubs whose loader has not run yet.
|
|
397
|
+
for (const [key, entry] of this.lazyEntries.entries()) {
|
|
398
|
+
if (entry.meta?.tier === 'public' && !key.includes(':'))
|
|
399
|
+
add(key, this.getContractConfig(key));
|
|
400
|
+
}
|
|
301
401
|
return out;
|
|
302
402
|
}
|
|
303
403
|
/**
|
|
@@ -42,7 +42,58 @@ export const PUBLIC_BLOCKS = [
|
|
|
42
42
|
'record:highlights',
|
|
43
43
|
'record:related_list',
|
|
44
44
|
'record:path',
|
|
45
|
-
|
|
45
|
+
// `record:`-prefixed, like its four siblings above. @object-ui/plugin-form
|
|
46
|
+
// registers this one with `skipFallback: true`, so the bare `line_items` key
|
|
47
|
+
// it was listed under here never existed — the block has shipped all along
|
|
48
|
+
// but could never resolve through the contract (objectui#2953 follow-up).
|
|
49
|
+
'record:line_items',
|
|
50
|
+
// Configurable as of objectui#3023 follow-up: these shipped with renderers
|
|
51
|
+
// but no declared `inputs`, so a model could only emit them bare — which is
|
|
52
|
+
// why they sat outside the contract. Their inputs now mirror what the
|
|
53
|
+
// renderers actually read, so they are authorable and belong here.
|
|
54
|
+
//
|
|
55
|
+
// `record:chatter` is deliberately NOT in this list: it is the same renderer
|
|
56
|
+
// as `record:discussion` under a Salesforce-familiar name, kept for schemas
|
|
57
|
+
// already in the wild. Two spellings of one block is ambiguity an authoring
|
|
58
|
+
// model has no way to resolve, so the vocabulary carries the spec's name.
|
|
59
|
+
'record:activity',
|
|
60
|
+
'record:discussion',
|
|
61
|
+
'record:history',
|
|
62
|
+
'record:quick_actions',
|
|
63
|
+
'record:reference_rail',
|
|
64
|
+
'record:alert',
|
|
65
|
+
// ── Tier A, continued — page-semantic blocks (the spec ui/page vocabulary) ─
|
|
66
|
+
// The `page:` / `element:` / `action:` families the Studio page designer
|
|
67
|
+
// offers and @objectstack/spec's page schema enumerates. Same standing as
|
|
68
|
+
// the `record:` family above: semantic page blocks, not primitives.
|
|
69
|
+
//
|
|
70
|
+
// Deliberately NOT here (each guarded, with its reason, by the console's
|
|
71
|
+
// reverse-coverage test):
|
|
72
|
+
// action:bar record:quick_actions covers the record action
|
|
73
|
+
// strip, and the spec blesses button/group/icon/
|
|
74
|
+
// menu but not bar
|
|
75
|
+
// element:image duplicates the curated `image` primitive below —
|
|
76
|
+
// one spelling per concept
|
|
77
|
+
// element:record_picker record picking is a field widget, not a page
|
|
78
|
+
// block (Studio palette exclusion)
|
|
79
|
+
// element:text_input bare inputs belong to a form, not a page block
|
|
80
|
+
// (Studio palette exclusion)
|
|
81
|
+
// element:metadata_viewer internal metadata debugging surface
|
|
82
|
+
'page:tabs',
|
|
83
|
+
'page:card',
|
|
84
|
+
'page:accordion',
|
|
85
|
+
'page:section',
|
|
86
|
+
'page:footer',
|
|
87
|
+
'page:sidebar',
|
|
88
|
+
'element:text',
|
|
89
|
+
'element:number',
|
|
90
|
+
'element:button',
|
|
91
|
+
'element:definition-list',
|
|
92
|
+
'element:repeater',
|
|
93
|
+
'action:button',
|
|
94
|
+
'action:group',
|
|
95
|
+
'action:menu',
|
|
96
|
+
'action:icon',
|
|
46
97
|
// ── Tier B — layout / content primitives ──────────────────────────────────
|
|
47
98
|
'flex',
|
|
48
99
|
'grid',
|
|
@@ -316,10 +316,6 @@ export function mergeThemes(parent, child) {
|
|
|
316
316
|
shadows: child.shadows || parent.shadows
|
|
317
317
|
? { ...parent.shadows, ...child.shadows }
|
|
318
318
|
: undefined,
|
|
319
|
-
// Deep-merge breakpoints
|
|
320
|
-
breakpoints: child.breakpoints || parent.breakpoints
|
|
321
|
-
? { ...parent.breakpoints, ...child.breakpoints }
|
|
322
|
-
: undefined,
|
|
323
319
|
// Deep-merge animation
|
|
324
320
|
animation: child.animation || parent.animation
|
|
325
321
|
? {
|
|
@@ -339,18 +335,10 @@ export function mergeThemes(parent, child) {
|
|
|
339
335
|
zIndex: child.zIndex || parent.zIndex
|
|
340
336
|
? { ...parent.zIndex, ...child.zIndex }
|
|
341
337
|
: undefined,
|
|
342
|
-
// Deep-merge spacing
|
|
343
|
-
spacing: child.spacing || parent.spacing
|
|
344
|
-
? { ...parent.spacing, ...child.spacing }
|
|
345
|
-
: undefined,
|
|
346
338
|
// Deep-merge customVars
|
|
347
339
|
customVars: child.customVars || parent.customVars
|
|
348
340
|
? { ...parent.customVars, ...child.customVars }
|
|
349
341
|
: undefined,
|
|
350
|
-
// Deep-merge logo
|
|
351
|
-
logo: child.logo || parent.logo
|
|
352
|
-
? { ...parent.logo, ...child.logo }
|
|
353
|
-
: undefined,
|
|
354
342
|
};
|
|
355
343
|
}
|
|
356
344
|
/**
|
|
@@ -89,6 +89,34 @@ export declare function findChartSeriesRow(rows: Array<Record<string, unknown>>
|
|
|
89
89
|
* paint identically across the chart view and dashboard widgets.
|
|
90
90
|
*/
|
|
91
91
|
export declare function buildOptionColorMap(options: unknown): Record<string, string> | null;
|
|
92
|
+
/**
|
|
93
|
+
* Build the DECLARED category order from a select field's `options` — the
|
|
94
|
+
* sequence the author wrote them in on the object (framework#3588).
|
|
95
|
+
*
|
|
96
|
+
* A picklist's option order is already the domain order: a sales `stage` field
|
|
97
|
+
* lists Qualification → Needs Analysis → Proposal → Negotiation because that IS
|
|
98
|
+
* the pipeline. Analytics groups by that field and returns buckets in whatever
|
|
99
|
+
* order the GROUP BY produced (usually alphabetical), which for an
|
|
100
|
+
* ordered-sequence chart — a funnel above all — draws a shape that reads as a
|
|
101
|
+
* pipeline but isn't one.
|
|
102
|
+
*
|
|
103
|
+
* Emits BOTH the stored `value` and its display `label` per option, adjacent
|
|
104
|
+
* and in declared order, for the same reason {@link buildOptionColorMap} keys
|
|
105
|
+
* by both: a chart row's category may carry either, depending on whether the
|
|
106
|
+
* server resolved the dimension's labels. Rank is "index of first match", so
|
|
107
|
+
* either key ranks the option identically.
|
|
108
|
+
*
|
|
109
|
+
* Returns `null` for a field with no options, so callers keep their existing
|
|
110
|
+
* ordering (a funnel falls back to descending by value).
|
|
111
|
+
*/
|
|
112
|
+
export declare function buildCategoryOrder(options: unknown): string[] | null;
|
|
113
|
+
/**
|
|
114
|
+
* Rank map for {@link buildCategoryOrder} keys — `category → position`, first
|
|
115
|
+
* occurrence wins. Categories absent from the declared order get no entry; the
|
|
116
|
+
* caller decides where those sort (see `AdvancedChartImpl`, which keeps them
|
|
117
|
+
* after the declared ones rather than dropping them).
|
|
118
|
+
*/
|
|
119
|
+
export declare function buildCategoryRank(order: string[] | null | undefined): Map<string, number> | null;
|
|
92
120
|
/**
|
|
93
121
|
* Build a `{ value → label }` map from a select/enum field's `options`, for
|
|
94
122
|
* resolving a grouped dimension's stored value to its display label (fed to
|