@object-ui/core 17.1.0 → 17.3.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 +738 -0
- package/README.md +29 -0
- package/dist/actions/ActionEngine.d.ts +22 -7
- package/dist/actions/ActionEngine.js +40 -22
- package/dist/actions/ActionRunner.d.ts +133 -13
- package/dist/actions/ActionRunner.js +27 -4
- package/dist/actions/actionErrorDetail.d.ts +26 -0
- package/dist/actions/actionErrorDetail.js +37 -0
- package/dist/actions/actionKeys.d.ts +29 -12
- package/dist/actions/actionKeys.js +61 -10
- 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/evaluator/ExpressionEvaluator.js +4 -2
- package/dist/evaluator/fieldRules.d.ts +26 -2
- package/dist/evaluator/fieldRules.js +63 -8
- package/dist/evaluator/index.d.ts +1 -0
- package/dist/evaluator/index.js +1 -0
- package/dist/evaluator/listConditional.d.ts +13 -1
- package/dist/evaluator/listConditional.js +0 -0
- package/dist/evaluator/optionRules.js +5 -1
- package/dist/evaluator/predicateInput.d.ts +84 -0
- package/dist/evaluator/predicateInput.js +72 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +12 -0
- package/dist/protocols/index.d.ts +0 -1
- package/dist/protocols/index.js +8 -1
- package/dist/query/query-ast.d.ts +6 -6
- package/dist/query/query-ast.js +2 -2
- package/dist/registry/PluginSystem.d.ts +15 -5
- package/dist/registry/PluginSystem.js +1 -1
- package/dist/registry/WidgetRegistry.d.ts +7 -7
- package/dist/registry/WidgetRegistry.js +1 -1
- package/dist/styling/scoped-styles.d.ts +14 -8
- package/dist/utils/chart-series.d.ts +14 -2
- package/dist/utils/dashboard-filters.d.ts +9 -0
- package/dist/utils/dashboard-filters.js +99 -4
- package/dist/utils/dataset-format.d.ts +18 -1
- package/dist/utils/dataset-format.js +10 -2
- package/dist/utils/dataset-pivot.d.ts +57 -0
- package/dist/utils/dataset-pivot.js +57 -0
- package/dist/utils/filter-converter.d.ts +39 -11
- package/dist/utils/filter-converter.js +87 -7
- package/dist/utils/filter-tokens.d.ts +13 -6
- package/dist/utils/filter-tokens.js +13 -6
- package/dist/utils/freeze-schema.d.ts +14 -6
- package/dist/utils/freeze-schema.js +14 -6
- package/dist/utils/managedBy.d.ts +78 -50
- package/dist/utils/managedBy.js +75 -52
- package/dist/utils/normalize-list-view.d.ts +12 -2
- 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/validation/index.d.ts +1 -0
- package/dist/validation/index.js +1 -0
- package/dist/validation/required-presence.d.ts +31 -0
- package/dist/validation/required-presence.js +36 -0
- package/dist/validation/schema-validator.d.ts +24 -8
- package/package.json +4 -4
- package/dist/protocols/NotificationProtocol.d.ts +0 -71
- package/dist/protocols/NotificationProtocol.js +0 -89
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* System View immutability layer.
|
|
10
10
|
*
|
|
11
11
|
* A "System View" is any UI schema authored in source code (imported `.ts`/`.json`,
|
|
12
|
-
* `as const` literals, or returned from a `
|
|
12
|
+
* `as const` literals, or returned from a `defineSystemView()` call). Such schemas are
|
|
13
13
|
* part of the product contract and MUST NOT be mutated at runtime. Mutation
|
|
14
14
|
* would cause behavior drift, break TypeScript inference, and bypass code review.
|
|
15
15
|
*
|
|
@@ -86,11 +86,19 @@ export function deepFreeze(value, seen = new WeakSet()) {
|
|
|
86
86
|
/**
|
|
87
87
|
* Mark and freeze a code-loaded schema as a System View.
|
|
88
88
|
*
|
|
89
|
+
* Named `defineView` until objectstack#4115, which is a name `@objectstack/spec`
|
|
90
|
+
* owns for something else entirely: the spec's `defineView(config)` is the
|
|
91
|
+
* VIEW-DOCUMENT factory — it parses a `ViewSchema` (`{ list, form, listViews,
|
|
92
|
+
* formViews }`) and returns a validated `View`. This one takes an arbitrary
|
|
93
|
+
* object, stamps the System-View marker and deep-freezes it; it validates
|
|
94
|
+
* nothing. An app-shell comment already conflated the two, which is precisely
|
|
95
|
+
* the planted-premise failure the guard exists to stop.
|
|
96
|
+
*
|
|
89
97
|
* @example
|
|
90
98
|
* ```ts
|
|
91
|
-
* import {
|
|
99
|
+
* import { defineSystemView } from '@object-ui/core';
|
|
92
100
|
*
|
|
93
|
-
* export const userListView =
|
|
101
|
+
* export const userListView = defineSystemView({
|
|
94
102
|
* type: 'list',
|
|
95
103
|
* data: { object: 'User' },
|
|
96
104
|
* columns: [{ name: 'email' }],
|
|
@@ -103,9 +111,9 @@ export function deepFreeze(value, seen = new WeakSet()) {
|
|
|
103
111
|
* To produce a mutable derivative (Tenant or User View), call
|
|
104
112
|
* `cloneAsOverride(userListView)`.
|
|
105
113
|
*/
|
|
106
|
-
export function
|
|
114
|
+
export function defineSystemView(schema) {
|
|
107
115
|
if (schema == null || typeof schema !== 'object') {
|
|
108
|
-
throw new TypeError('[ObjectUI]
|
|
116
|
+
throw new TypeError('[ObjectUI] defineSystemView() expects a non-null object schema.');
|
|
109
117
|
}
|
|
110
118
|
// Stamp the marker on the root only — nested nodes share origin via lineage.
|
|
111
119
|
// Non-enumerable keeps it out of JSON, spreads, and Object.keys.
|
|
@@ -120,7 +128,7 @@ export function defineView(schema) {
|
|
|
120
128
|
return deepFreeze(schema);
|
|
121
129
|
}
|
|
122
130
|
/**
|
|
123
|
-
* Runtime check: was this object produced by `
|
|
131
|
+
* Runtime check: was this object produced by `defineSystemView()` (or loaded from a
|
|
124
132
|
* source that forwards the marker)?
|
|
125
133
|
*/
|
|
126
134
|
export function isSystemView(value) {
|
|
@@ -1,51 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* managedBy — the
|
|
3
|
-
*
|
|
2
|
+
* managedBy — the UI end of the platform's `managedBy` → CRUD affordance
|
|
3
|
+
* contract (ADR-0103).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* (`@objectstack/spec/data/object.zod.ts`, ADR-0103). Lives in `@object-ui/core`
|
|
7
|
-
* — React-free and reachable by every UI package (app-shell, plugin-detail,
|
|
8
|
-
* plugin-form, plugin-grid) — so the bucket logic is defined ONCE instead of
|
|
9
|
-
* hand-mirrored in each. Components ask "should I show New / Import / Edit /
|
|
10
|
-
* Delete / Export?" and get an answer that tracks the object's lifecycle without
|
|
11
|
-
* special-casing `sys_*` names.
|
|
5
|
+
* ## What is spec-owned here, and what is not (objectstack#4115)
|
|
12
6
|
*
|
|
13
|
-
* The bucket
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
7
|
+
* The bucket logic is **the spec's**, and this module now says so in code
|
|
8
|
+
* rather than in a comment. `CrudAffordances` / `RowCrudPredicates` are
|
|
9
|
+
* re-exported from `@objectstack/spec/data`, and the bucket resolution itself
|
|
10
|
+
* delegates to the spec's `resolveCrudAffordances()`.
|
|
11
|
+
*
|
|
12
|
+
* This module used to describe itself as a "UI-side mirror" of that function
|
|
13
|
+
* and carry its own `DEFAULTS` table — a line-for-line copy of the spec's
|
|
14
|
+
* `CRUD_AFFORDANCE_DEFAULTS`, plus a copy of its override parser. A faithful
|
|
15
|
+
* copy passes every value comparison and every behavioural test, so nothing
|
|
16
|
+
* would have caught the release in which the spec re-buckets an object class;
|
|
17
|
+
* reference identity is the only check that distinguishes a re-export from a
|
|
18
|
+
* fork (objectui#3003).
|
|
19
|
+
*
|
|
20
|
+
* What is genuinely objectui's and keeps a local name:
|
|
21
|
+
* {@link resolveEffectiveCrudAffordances} — the spec's object-level matrix
|
|
22
|
+
* INTERSECTED with the server-resolved effective API operation set (#3391), so
|
|
23
|
+
* the UI never offers a button the server would 405. The spec's function has
|
|
24
|
+
* no such notion; naming ours after it was the misdescription this burn-down
|
|
25
|
+
* removes.
|
|
26
|
+
*
|
|
27
|
+
* Lives in `@object-ui/core` — React-free and reachable by every UI package
|
|
28
|
+
* (app-shell, plugin-detail, plugin-form, plugin-grid) — so the intersection is
|
|
29
|
+
* defined ONCE instead of hand-mirrored in each. The bucket UNION type lives in
|
|
30
|
+
* `@object-ui/types` (`ManagedByBucket`) and is re-exported here for
|
|
31
|
+
* convenience. All translated copy (badge variants, empty-state messages) stays
|
|
32
|
+
* in `@object-ui/app-shell`.
|
|
17
33
|
*/
|
|
34
|
+
import { type CrudAffordances, type RowCrudPredicates } from '@objectstack/spec/data';
|
|
18
35
|
export type { ManagedByBucket } from '@object-ui/types';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Per-record CEL predicates for the built-in row Edit/Delete actions,
|
|
32
|
-
* present only when `userActions.edit` / `delete` used the object form
|
|
33
|
-
* (objectui#2614). Carried through as authored for row renderers to evaluate;
|
|
34
|
-
* they never affect the object-level booleans above.
|
|
35
|
-
*/
|
|
36
|
-
editPredicates?: RowCrudPredicates;
|
|
37
|
-
deletePredicates?: RowCrudPredicates;
|
|
38
|
-
}
|
|
39
|
-
/** Per-record predicates from the #2614 object form of a userActions flag. */
|
|
40
|
-
export interface RowCrudPredicates {
|
|
41
|
-
visibleWhen?: unknown;
|
|
42
|
-
disabledWhen?: unknown;
|
|
43
|
-
}
|
|
36
|
+
/**
|
|
37
|
+
* The resolved CRUD affordance matrix and its per-record predicate envelope
|
|
38
|
+
* are **spec-owned** (`@objectstack/spec/data`, ADR-0103) and re-exported here,
|
|
39
|
+
* not re-declared.
|
|
40
|
+
*
|
|
41
|
+
* Note this also TIGHTENS the predicate types: the local copies typed
|
|
42
|
+
* `visibleWhen` / `disabledWhen` as `unknown`, while the spec types them as
|
|
43
|
+
* `Expression | ExpressionInput` — the authored CEL shorthand or its envelope.
|
|
44
|
+
* The local `unknown` was imprecision, not a deliberate dialect.
|
|
45
|
+
*/
|
|
46
|
+
export type { CrudAffordances, RowCrudPredicates };
|
|
44
47
|
/** `edit`/`delete` accept a bare boolean or the #2614 object form. */
|
|
45
48
|
export type UserActionOverride = boolean | {
|
|
46
49
|
enabled?: boolean;
|
|
47
|
-
visibleWhen?:
|
|
48
|
-
disabledWhen?:
|
|
50
|
+
visibleWhen?: RowCrudPredicates['visibleWhen'];
|
|
51
|
+
disabledWhen?: RowCrudPredicates['disabledWhen'];
|
|
49
52
|
};
|
|
50
53
|
export interface UserActionsOverride {
|
|
51
54
|
create?: boolean;
|
|
@@ -63,11 +66,14 @@ export interface SchemaLike {
|
|
|
63
66
|
* the given bucket default, surfacing any per-record predicates alongside.
|
|
64
67
|
*
|
|
65
68
|
* THE single parser for the `userActions.{edit,delete}` override shape — every
|
|
66
|
-
* UI package (grid row affordances, related-list row predicates
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
69
|
+
* UI package (grid row affordances, related-list row predicates) routes through
|
|
70
|
+
* here instead of re-implementing the boolean/object-form parse locally. The
|
|
71
|
+
* spec keeps its equivalent (`normalizeRowCrudOverride`) module-private, so
|
|
72
|
+
* this stays a local declaration — under a name the spec does not own.
|
|
73
|
+
*
|
|
74
|
+
* `base` is the bucket default an omitted `enabled` falls back to (callers that
|
|
75
|
+
* only gate on an explicit opt-out pass `true`; predicate extraction is
|
|
76
|
+
* independent of `base`).
|
|
71
77
|
*/
|
|
72
78
|
export declare function normalizeUserAction(v: UserActionOverride | undefined | null, base: boolean): {
|
|
73
79
|
enabled: boolean;
|
|
@@ -82,7 +88,15 @@ export declare function normalizeUserAction(v: UserActionOverride | undefined |
|
|
|
82
88
|
*/
|
|
83
89
|
export declare function userActionPredicates(v: UserActionOverride | undefined | null): RowCrudPredicates | undefined;
|
|
84
90
|
/**
|
|
85
|
-
* Resolve the
|
|
91
|
+
* Resolve the CRUD affordances a UI surface should expose for an object —
|
|
92
|
+
* the spec's object-level matrix, INTERSECTED with what the server will
|
|
93
|
+
* actually accept.
|
|
94
|
+
*
|
|
95
|
+
* The bucket/`userActions` half is **not computed here**: it is delegated to
|
|
96
|
+
* `resolveCrudAffordances()` from `@objectstack/spec/data` (ADR-0103), so the
|
|
97
|
+
* bucket table has exactly one definition on the platform. Only the
|
|
98
|
+
* intersection below is objectui's, which is why this function no longer
|
|
99
|
+
* carries the spec's name (objectstack#4115).
|
|
86
100
|
*
|
|
87
101
|
* @param obj The object schema (managedBy bucket + userActions overrides).
|
|
88
102
|
* @param effectiveApiOperations OPTIONAL server-resolved effective API operation
|
|
@@ -93,13 +107,27 @@ export declare function userActionPredicates(v: UserActionOverride | undefined |
|
|
|
93
107
|
* (old backend / no effective set) leaves the affordances untouched —
|
|
94
108
|
* backward-compatible. An empty array means "expose nothing" → all bits off.
|
|
95
109
|
*/
|
|
96
|
-
export declare function
|
|
110
|
+
export declare function resolveEffectiveCrudAffordances(obj: SchemaLike | null | undefined, effectiveApiOperations?: readonly string[] | null): CrudAffordances;
|
|
97
111
|
/** True only when a `userActions` flag (bare boolean or #2614 object form) opts the write in. */
|
|
98
112
|
export declare function isWriteOptedIn(v: UserActionOverride | undefined | null): boolean;
|
|
99
113
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
114
|
+
* True for a platform-defined schema whose DATA is admin/user-writable — the
|
|
115
|
+
* `system-data` bucket. Used to pick the writable-system badge/empty-state copy
|
|
116
|
+
* instead of the engine-owned "read-only monitoring surface" copy.
|
|
117
|
+
*
|
|
118
|
+
* **Simplified in protocol 17 (objectstack#3355).** Under ADR-0103's v16 split
|
|
119
|
+
* this had to RECOVER the distinction from `userActions`: `system` doubled as
|
|
120
|
+
* both the engine-owned default and the writable set, so "did it open any
|
|
121
|
+
* write?" was the only way to tell a Notification Preferences grid from an
|
|
122
|
+
* automation-run log. v17 retired that overload — the writable half is now the
|
|
123
|
+
* `system-data` bucket (writable by DEFAULT) and the engine-owned half is
|
|
124
|
+
* `engine-owned` — so the bucket answers the question directly and the
|
|
125
|
+
* `userActions` probe would only re-derive what the value already states.
|
|
126
|
+
*
|
|
127
|
+
* The narrowing case is deliberately still `true`: a `system-data` grid that
|
|
128
|
+
* narrows to edit-only is still platform-schema/user-data and should read as
|
|
129
|
+
* such. The spec refuses `system-data` on an object that narrows away every
|
|
130
|
+
* write, so there is no "writable bucket with no writes" shape to worry about.
|
|
103
131
|
*/
|
|
104
132
|
export declare function isSystemWritable(obj: SchemaLike | null | undefined): boolean;
|
|
105
133
|
/**
|
package/dist/utils/managedBy.js
CHANGED
|
@@ -1,38 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* managedBy — the
|
|
3
|
-
*
|
|
2
|
+
* managedBy — the UI end of the platform's `managedBy` → CRUD affordance
|
|
3
|
+
* contract (ADR-0103).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* (`@objectstack/spec/data/object.zod.ts`, ADR-0103). Lives in `@object-ui/core`
|
|
7
|
-
* — React-free and reachable by every UI package (app-shell, plugin-detail,
|
|
8
|
-
* plugin-form, plugin-grid) — so the bucket logic is defined ONCE instead of
|
|
9
|
-
* hand-mirrored in each. Components ask "should I show New / Import / Edit /
|
|
10
|
-
* Delete / Export?" and get an answer that tracks the object's lifecycle without
|
|
11
|
-
* special-casing `sys_*` names.
|
|
5
|
+
* ## What is spec-owned here, and what is not (objectstack#4115)
|
|
12
6
|
*
|
|
13
|
-
* The bucket
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
7
|
+
* The bucket logic is **the spec's**, and this module now says so in code
|
|
8
|
+
* rather than in a comment. `CrudAffordances` / `RowCrudPredicates` are
|
|
9
|
+
* re-exported from `@objectstack/spec/data`, and the bucket resolution itself
|
|
10
|
+
* delegates to the spec's `resolveCrudAffordances()`.
|
|
11
|
+
*
|
|
12
|
+
* This module used to describe itself as a "UI-side mirror" of that function
|
|
13
|
+
* and carry its own `DEFAULTS` table — a line-for-line copy of the spec's
|
|
14
|
+
* `CRUD_AFFORDANCE_DEFAULTS`, plus a copy of its override parser. A faithful
|
|
15
|
+
* copy passes every value comparison and every behavioural test, so nothing
|
|
16
|
+
* would have caught the release in which the spec re-buckets an object class;
|
|
17
|
+
* reference identity is the only check that distinguishes a re-export from a
|
|
18
|
+
* fork (objectui#3003).
|
|
19
|
+
*
|
|
20
|
+
* What is genuinely objectui's and keeps a local name:
|
|
21
|
+
* {@link resolveEffectiveCrudAffordances} — the spec's object-level matrix
|
|
22
|
+
* INTERSECTED with the server-resolved effective API operation set (#3391), so
|
|
23
|
+
* the UI never offers a button the server would 405. The spec's function has
|
|
24
|
+
* no such notion; naming ours after it was the misdescription this burn-down
|
|
25
|
+
* removes.
|
|
26
|
+
*
|
|
27
|
+
* Lives in `@object-ui/core` — React-free and reachable by every UI package
|
|
28
|
+
* (app-shell, plugin-detail, plugin-form, plugin-grid) — so the intersection is
|
|
29
|
+
* defined ONCE instead of hand-mirrored in each. The bucket UNION type lives in
|
|
30
|
+
* `@object-ui/types` (`ManagedByBucket`) and is re-exported here for
|
|
31
|
+
* convenience. All translated copy (badge variants, empty-state messages) stays
|
|
32
|
+
* in `@object-ui/app-shell`.
|
|
17
33
|
*/
|
|
18
|
-
|
|
19
|
-
platform: { create: true, import: true, edit: true, delete: true, exportCsv: true },
|
|
20
|
-
config: { create: true, import: false, edit: true, delete: true, exportCsv: true },
|
|
21
|
-
system: { create: false, import: false, edit: false, delete: false, exportCsv: true },
|
|
22
|
-
'engine-owned': { create: false, import: false, edit: false, delete: false, exportCsv: true },
|
|
23
|
-
'append-only': { create: false, import: false, edit: false, delete: false, exportCsv: true },
|
|
24
|
-
'better-auth': { create: false, import: false, edit: false, delete: false, exportCsv: true },
|
|
25
|
-
};
|
|
34
|
+
import { resolveCrudAffordances as resolveSpecCrudAffordances, } from '@objectstack/spec/data';
|
|
26
35
|
/**
|
|
27
36
|
* Collapse an `edit`/`delete` override (boolean or #2614 object form) onto
|
|
28
37
|
* the given bucket default, surfacing any per-record predicates alongside.
|
|
29
38
|
*
|
|
30
39
|
* THE single parser for the `userActions.{edit,delete}` override shape — every
|
|
31
|
-
* UI package (grid row affordances, related-list row predicates
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
40
|
+
* UI package (grid row affordances, related-list row predicates) routes through
|
|
41
|
+
* here instead of re-implementing the boolean/object-form parse locally. The
|
|
42
|
+
* spec keeps its equivalent (`normalizeRowCrudOverride`) module-private, so
|
|
43
|
+
* this stays a local declaration — under a name the spec does not own.
|
|
44
|
+
*
|
|
45
|
+
* `base` is the bucket default an omitted `enabled` falls back to (callers that
|
|
46
|
+
* only gate on an explicit opt-out pass `true`; predicate extraction is
|
|
47
|
+
* independent of `base`).
|
|
36
48
|
*/
|
|
37
49
|
export function normalizeUserAction(v, base) {
|
|
38
50
|
if (v == null)
|
|
@@ -73,7 +85,15 @@ const AFFORDANCE_TO_API_OPERATION = {
|
|
|
73
85
|
exportCsv: 'export',
|
|
74
86
|
};
|
|
75
87
|
/**
|
|
76
|
-
* Resolve the
|
|
88
|
+
* Resolve the CRUD affordances a UI surface should expose for an object —
|
|
89
|
+
* the spec's object-level matrix, INTERSECTED with what the server will
|
|
90
|
+
* actually accept.
|
|
91
|
+
*
|
|
92
|
+
* The bucket/`userActions` half is **not computed here**: it is delegated to
|
|
93
|
+
* `resolveCrudAffordances()` from `@objectstack/spec/data` (ADR-0103), so the
|
|
94
|
+
* bucket table has exactly one definition on the platform. Only the
|
|
95
|
+
* intersection below is objectui's, which is why this function no longer
|
|
96
|
+
* carries the spec's name (objectstack#4115).
|
|
77
97
|
*
|
|
78
98
|
* @param obj The object schema (managedBy bucket + userActions overrides).
|
|
79
99
|
* @param effectiveApiOperations OPTIONAL server-resolved effective API operation
|
|
@@ -84,23 +104,15 @@ const AFFORDANCE_TO_API_OPERATION = {
|
|
|
84
104
|
* (old backend / no effective set) leaves the affordances untouched —
|
|
85
105
|
* backward-compatible. An empty array means "expose nothing" → all bits off.
|
|
86
106
|
*/
|
|
87
|
-
export function
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
edit: edit.enabled,
|
|
97
|
-
delete: del.enabled,
|
|
98
|
-
exportCsv: o.exportCsv ?? base.exportCsv,
|
|
99
|
-
};
|
|
100
|
-
if (edit.predicates)
|
|
101
|
-
out.editPredicates = edit.predicates;
|
|
102
|
-
if (del.predicates)
|
|
103
|
-
out.deletePredicates = del.predicates;
|
|
107
|
+
export function resolveEffectiveCrudAffordances(obj, effectiveApiOperations) {
|
|
108
|
+
// The spec's resolver is the single definition of the bucket defaults and the
|
|
109
|
+
// `userActions` override parse (including the #2614 object form). Its input
|
|
110
|
+
// type does not model the `null`s objectui's loosely-typed schemas carry, so
|
|
111
|
+
// they are normalised away at this one seam rather than re-deriving anything.
|
|
112
|
+
const out = resolveSpecCrudAffordances({
|
|
113
|
+
managedBy: obj?.managedBy ?? undefined,
|
|
114
|
+
userActions: (obj?.userActions ?? undefined),
|
|
115
|
+
});
|
|
104
116
|
// [#3391] Intersect with the server's effective API operations when present.
|
|
105
117
|
// The frontend consumes the effective set the server hands down; it never
|
|
106
118
|
// reads the raw `apiMethods` nor re-derives.
|
|
@@ -118,15 +130,26 @@ export function isWriteOptedIn(v) {
|
|
|
118
130
|
return v === true || (typeof v === 'object' && v !== null && v.enabled === true);
|
|
119
131
|
}
|
|
120
132
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
133
|
+
* True for a platform-defined schema whose DATA is admin/user-writable — the
|
|
134
|
+
* `system-data` bucket. Used to pick the writable-system badge/empty-state copy
|
|
135
|
+
* instead of the engine-owned "read-only monitoring surface" copy.
|
|
136
|
+
*
|
|
137
|
+
* **Simplified in protocol 17 (objectstack#3355).** Under ADR-0103's v16 split
|
|
138
|
+
* this had to RECOVER the distinction from `userActions`: `system` doubled as
|
|
139
|
+
* both the engine-owned default and the writable set, so "did it open any
|
|
140
|
+
* write?" was the only way to tell a Notification Preferences grid from an
|
|
141
|
+
* automation-run log. v17 retired that overload — the writable half is now the
|
|
142
|
+
* `system-data` bucket (writable by DEFAULT) and the engine-owned half is
|
|
143
|
+
* `engine-owned` — so the bucket answers the question directly and the
|
|
144
|
+
* `userActions` probe would only re-derive what the value already states.
|
|
145
|
+
*
|
|
146
|
+
* The narrowing case is deliberately still `true`: a `system-data` grid that
|
|
147
|
+
* narrows to edit-only is still platform-schema/user-data and should read as
|
|
148
|
+
* such. The spec refuses `system-data` on an object that narrows away every
|
|
149
|
+
* write, so there is no "writable bucket with no writes" shape to worry about.
|
|
124
150
|
*/
|
|
125
151
|
export function isSystemWritable(obj) {
|
|
126
|
-
|
|
127
|
-
return false;
|
|
128
|
-
const o = obj?.userActions ?? {};
|
|
129
|
-
return o.create === true || isWriteOptedIn(o.edit) || isWriteOptedIn(o.delete);
|
|
152
|
+
return obj?.managedBy === 'system-data';
|
|
130
153
|
}
|
|
131
154
|
/**
|
|
132
155
|
* Whether an object's rows may be edited in place (detail inline-edit / form
|
|
@@ -143,5 +166,5 @@ export function isSystemWritable(obj) {
|
|
|
143
166
|
* bucket affordance untouched.
|
|
144
167
|
*/
|
|
145
168
|
export function isObjectInlineEditable(obj, effectiveApiOperations) {
|
|
146
|
-
return
|
|
169
|
+
return resolveEffectiveCrudAffordances(obj, effectiveApiOperations).edit;
|
|
147
170
|
}
|
|
@@ -5,10 +5,20 @@
|
|
|
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
|
+
import type { RowHeight } from '@objectstack/spec/ui';
|
|
8
9
|
/** ListView's toolbar density vocabulary — three steps, not the spec's five. */
|
|
9
10
|
export type DensityMode = 'compact' | 'comfortable' | 'spacious';
|
|
10
|
-
/**
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* The spec's `RowHeightSchema` vocabulary — **re-exported, not re-declared**.
|
|
13
|
+
*
|
|
14
|
+
* The hand-written union that used to sit here already advertised itself as
|
|
15
|
+
* "the spec's `RowHeightSchema` vocabulary" in its doc comment, which is the
|
|
16
|
+
* failure class objectstack#4115 is about: the claim was true when written and
|
|
17
|
+
* nothing would have caught the day it stopped being true. The maps below are
|
|
18
|
+
* `Record<RowHeight, …>`, so a spec-side addition now fails the build here
|
|
19
|
+
* instead of silently leaving a row height with no density mapping.
|
|
20
|
+
*/
|
|
21
|
+
export type { RowHeight };
|
|
12
22
|
/**
|
|
13
23
|
* `densityMode` → `rowHeight`. The two vocabularies are not the same size: the
|
|
14
24
|
* spec has five row heights, ListView's toolbar offers three. Widening maps
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — predicate field harvesting
|
|
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
|
+
* Which record fields a view's PREDICATES read — the half of a view's field
|
|
10
|
+
* appetite that is not its columns.
|
|
11
|
+
*
|
|
12
|
+
* A list view projects what it DISPLAYS: `$select` is built from the column
|
|
13
|
+
* list (plus `id` and the expanded relation roots). That is correct for
|
|
14
|
+
* rendering and wrong for gating, because a `visible` / `disabled` /
|
|
15
|
+
* conditional-formatting predicate reads fields nobody put on screen — gating a
|
|
16
|
+
* row action on `record.owner` while showing Title / Status is the ordinary
|
|
17
|
+
* case, not an exotic one.
|
|
18
|
+
*
|
|
19
|
+
* The field then simply is not in the payload, and CEL does not treat an absent
|
|
20
|
+
* key as null: `record.owner == os.user.id` FAULTS with `No such key: owner`.
|
|
21
|
+
* Fault handling differs per surface and both answers are wrong — fail-CLOSED
|
|
22
|
+
* on the row kebab and the selection bar (the button silently disappears for
|
|
23
|
+
* everyone), fail-OPEN on the lenient paths (it appears for everyone). Nothing
|
|
24
|
+
* in either outcome points at a projection.
|
|
25
|
+
*
|
|
26
|
+
* Harvesting the names a predicate mentions and adding them to `$select` is
|
|
27
|
+
* what closes that. Note what it deliberately does NOT add: `$expand`. A
|
|
28
|
+
* predicate wants the stored foreign key, which is exactly what an unexpanded
|
|
29
|
+
* relation column already is — so a predicate-only relation field costs one
|
|
30
|
+
* more column and no extra round trip. (A relation that IS displayed still gets
|
|
31
|
+
* expanded for its label, and `toPredicateRecord` collapses it back for the
|
|
32
|
+
* predicate.)
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* The columns the framework provisions on EVERY business object
|
|
36
|
+
* (`applySystemFields`) but does not publish in the object's `fields` metadata.
|
|
37
|
+
*
|
|
38
|
+
* They matter here because the most ordinary ownership predicate there is —
|
|
39
|
+
* `record.owner_id == os.user.id` — names one of them. Checking a harvested
|
|
40
|
+
* name against the declared fields alone would drop it, leaving the predicate
|
|
41
|
+
* with the same missing key the harvest exists to prevent.
|
|
42
|
+
*
|
|
43
|
+
* Deliberately the columns OBSERVED on a live payload rather than
|
|
44
|
+
* `SYSTEM_MANAGED_FIELD_NAMES` (`@object-ui/types`), which is the broader
|
|
45
|
+
* DISPLAY-oriented set and carries legacy aliases (`_id`, `createdAt`,
|
|
46
|
+
* `modified`, `space`) that are not columns anywhere. An alias in `$select` is
|
|
47
|
+
* exactly the unknown key the guard exists to keep out.
|
|
48
|
+
*/
|
|
49
|
+
export declare const PLATFORM_RECORD_COLUMNS: ReadonlySet<string>;
|
|
50
|
+
/**
|
|
51
|
+
* Whether a harvested name is safe to put in a `$select` projection: either the
|
|
52
|
+
* object declares it, or it is one of the platform columns every object has.
|
|
53
|
+
*
|
|
54
|
+
* The check is not pedantry — an unknown key is not ignored by every backend
|
|
55
|
+
* (the cloud multi-tenant runtime answers with an EMPTY result set), so without
|
|
56
|
+
* it one typo'd predicate could blank a whole list.
|
|
57
|
+
*/
|
|
58
|
+
export declare function isProjectableField(name: string, declaredFields: Record<string, unknown> | null | undefined): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Collect the record field names referenced by a set of predicates.
|
|
61
|
+
*
|
|
62
|
+
* Accepts predicates in every authored shape (bare CEL string, the
|
|
63
|
+
* `{ dialect, source }` envelope, `null`/`undefined`/boolean — the last three
|
|
64
|
+
* contribute nothing). Returns names in first-seen order, deduplicated.
|
|
65
|
+
*
|
|
66
|
+
* The result is a set of CANDIDATES, not verified fields: a typo'd predicate
|
|
67
|
+
* yields a name this object does not have. Callers must intersect it with the
|
|
68
|
+
* object's declared fields before putting it in a query — an unknown key in
|
|
69
|
+
* `$select` is not ignored by every backend (the cloud runtime answers with an
|
|
70
|
+
* empty result set), which would turn a typo into a blank list.
|
|
71
|
+
*/
|
|
72
|
+
export declare function collectPredicateFieldRefs(predicates: readonly unknown[]): string[];
|
|
73
|
+
/**
|
|
74
|
+
* Every predicate a list-ish view carries, flattened for
|
|
75
|
+
* {@link collectPredicateFieldRefs}.
|
|
76
|
+
*
|
|
77
|
+
* One reader so the projection cannot fall behind the surfaces: each entry here
|
|
78
|
+
* is a place a predicate is actually evaluated against a row — conditional
|
|
79
|
+
* formatting, the row kebab (custom defs AND the built-in Edit/Delete
|
|
80
|
+
* overrides), the selection bar, and the object's declared actions.
|
|
81
|
+
*
|
|
82
|
+
* `objectActions` is deliberately the object's WHOLE action set rather than
|
|
83
|
+
* only the ones this view promotes. Narrowing it would mean re-running the
|
|
84
|
+
* name resolution `resolveLegacyRowActions` / `resolveBulkActions` already do,
|
|
85
|
+
* at a point in the fetch where the view's `rowActions` / `bulkActions` lists
|
|
86
|
+
* have not been folded yet — and the two directions of error are not
|
|
87
|
+
* symmetric: an extra column costs bytes, a missing one silently breaks the
|
|
88
|
+
* predicate that needed it.
|
|
89
|
+
*/
|
|
90
|
+
export declare function listViewPredicates(view: {
|
|
91
|
+
conditionalFormatting?: readonly unknown[] | null;
|
|
92
|
+
rowActionDefs?: readonly unknown[] | null;
|
|
93
|
+
bulkActionDefs?: readonly unknown[] | null;
|
|
94
|
+
objectActions?: readonly unknown[] | null;
|
|
95
|
+
userActions?: Record<string, unknown> | null;
|
|
96
|
+
}): unknown[];
|