@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
package/README.md
CHANGED
|
@@ -58,6 +58,35 @@ const userName = scope.get('user.name') // 'John'
|
|
|
58
58
|
const isAdmin = scope.evaluate('${user.role === "admin"}') // true
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
### Server Action Dispatch (`createServerActionHandler`)
|
|
62
|
+
|
|
63
|
+
`ActionSchema.body` (L1 expression / L2 sandboxed JS) executes **server-side**
|
|
64
|
+
— `POST /api/v1/actions/{object}/{action}` → the runtime sandbox. The client
|
|
65
|
+
dispatches; it never interprets a body. Build the dispatch handler with the
|
|
66
|
+
factory and register it — core stays opinion-free about auth, origin and
|
|
67
|
+
object scope, which are injected:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { createServerActionHandler } from '@object-ui/core'
|
|
71
|
+
|
|
72
|
+
const script = createServerActionHandler({
|
|
73
|
+
fetch: myAuthenticatedFetch, // your auth wrapper (Bearer/cookies/...)
|
|
74
|
+
baseUrl: 'https://api.example.com', // '' or omitted = same-origin
|
|
75
|
+
resolveObject: () => currentObject, // fallback when the action has no objectName
|
|
76
|
+
onRefresh: () => refetchData(), // called per the action's refreshAfter
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// Registered handlers beat the built-in executors:
|
|
80
|
+
runner.registerHandler('script', script)
|
|
81
|
+
// (React hosts: <ActionProvider handlers={{ script }} ... />)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The factory owns the protocol so consumers cannot drift on it: name-based
|
|
85
|
+
action identity (ADR-0110), the record-id resolution dance (`_rowRecord`,
|
|
86
|
+
`recordIdField`, selection fallback, aggregate `_selectedIds`), a re-entrancy
|
|
87
|
+
guard, and the `/actions` response-envelope rule (`interpretActionResponse` /
|
|
88
|
+
`readActionPayload`, also exported).
|
|
89
|
+
|
|
61
90
|
### System Views (`defineView`)
|
|
62
91
|
|
|
63
92
|
Schemas authored in source code are part of the product contract and must
|
|
@@ -78,7 +78,17 @@ export declare class ActionEngine {
|
|
|
78
78
|
bulkEnabled?: boolean;
|
|
79
79
|
priority?: number;
|
|
80
80
|
}): void;
|
|
81
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Register multiple actions from an ActionSchema array.
|
|
83
|
+
*
|
|
84
|
+
* Only `locations` is harvested from the metadata. `shortcut` and
|
|
85
|
+
* `bulkEnabled` used to be read here too, but spec 17 retired both as
|
|
86
|
+
* `retiredKey()` tombstones — a declaration carrying either one no longer
|
|
87
|
+
* parses, so harvesting them read a key that cannot exist and made two dead
|
|
88
|
+
* options look load-bearing. Both remain accepted on the single-action
|
|
89
|
+
* `registerAction(action, options)` overload, where a HOST passes them
|
|
90
|
+
* explicitly; they are simply no longer sourced from authored metadata.
|
|
91
|
+
*/
|
|
82
92
|
registerActions(actions: ActionDef[]): void;
|
|
83
93
|
/** Unregister an action by name */
|
|
84
94
|
unregisterAction(name: string): void;
|
|
@@ -98,13 +108,18 @@ export declare class ActionEngine {
|
|
|
98
108
|
* action behaves identically whether surfaced via the engine or
|
|
99
109
|
* consumed standalone.
|
|
100
110
|
*
|
|
101
|
-
* Predicate normalization
|
|
111
|
+
* Predicate normalization goes through the shared `toPredicateInput`
|
|
112
|
+
* (`@object-ui/core`'s canonical helper — semantically identical to
|
|
113
|
+
* `@object-ui/react`'s renderer-side twin, pinned by a parity test):
|
|
102
114
|
* raw strings (`'record.foo == true'`) are wrapped as `${...}` so they
|
|
103
|
-
* evaluate as expressions, not as truthy string literals
|
|
104
|
-
* `{ dialect, source }`
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
115
|
+
* evaluate as expressions, not as truthy string literals, and a spec-style
|
|
116
|
+
* `{ dialect: 'cel', source }` envelope is passed through INTACT so
|
|
117
|
+
* `evaluateCondition` routes it to the canonical `@objectstack/formula`
|
|
118
|
+
* engine (#2661). Unwrapping the envelope here — as this method used to —
|
|
119
|
+
* dropped the dialect and silently demoted every CEL predicate to the
|
|
120
|
+
* legacy JS evaluator, so the same `visible:` reached a different verdict
|
|
121
|
+
* via the engine than via a renderer (#3314): CEL faults on `null < null`
|
|
122
|
+
* and fail-closed hides, JS quietly returns `false`.
|
|
108
123
|
*/
|
|
109
124
|
getActionsForLocation(location: ActionLocation): ActionDef[];
|
|
110
125
|
/** Get actions that support bulk operations */
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* ActionSchema[] → ActionEngine → ActionRunner → ActionResult
|
|
17
17
|
*/
|
|
18
18
|
import { ActionRunner } from './ActionRunner';
|
|
19
|
+
import { toPredicateInput } from '../evaluator/predicateInput.js';
|
|
19
20
|
export { ACTION_LOCATIONS } from '@object-ui/types';
|
|
20
21
|
/** Normalize a shortcut string for comparison (lowercase, trimmed, sorted parts) */
|
|
21
22
|
function normalizeShortcut(keys) {
|
|
@@ -110,13 +111,24 @@ export class ActionEngine {
|
|
|
110
111
|
this.normalizedShortcutMap.set(normalizeShortcut(options.shortcut), name);
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* Register multiple actions from an ActionSchema array.
|
|
116
|
+
*
|
|
117
|
+
* Only `locations` is harvested from the metadata. `shortcut` and
|
|
118
|
+
* `bulkEnabled` used to be read here too, but spec 17 retired both as
|
|
119
|
+
* `retiredKey()` tombstones — a declaration carrying either one no longer
|
|
120
|
+
* parses, so harvesting them read a key that cannot exist and made two dead
|
|
121
|
+
* options look load-bearing. Both remain accepted on the single-action
|
|
122
|
+
* `registerAction(action, options)` overload, where a HOST passes them
|
|
123
|
+
* explicitly; they are simply no longer sourced from authored metadata.
|
|
124
|
+
*/
|
|
114
125
|
registerActions(actions) {
|
|
115
126
|
for (const action of actions) {
|
|
116
127
|
this.registerAction(action, {
|
|
128
|
+
// No cast: `locations` is a declared `ActionDef` field as of
|
|
129
|
+
// objectstack#4075 step 2. The `as any` it replaces existed only
|
|
130
|
+
// because the key reached this reader through the index signature.
|
|
117
131
|
locations: action.locations,
|
|
118
|
-
shortcut: action.shortcut,
|
|
119
|
-
bulkEnabled: action.bulkEnabled,
|
|
120
132
|
});
|
|
121
133
|
}
|
|
122
134
|
}
|
|
@@ -148,13 +160,18 @@ export class ActionEngine {
|
|
|
148
160
|
* action behaves identically whether surfaced via the engine or
|
|
149
161
|
* consumed standalone.
|
|
150
162
|
*
|
|
151
|
-
* Predicate normalization
|
|
163
|
+
* Predicate normalization goes through the shared `toPredicateInput`
|
|
164
|
+
* (`@object-ui/core`'s canonical helper — semantically identical to
|
|
165
|
+
* `@object-ui/react`'s renderer-side twin, pinned by a parity test):
|
|
152
166
|
* raw strings (`'record.foo == true'`) are wrapped as `${...}` so they
|
|
153
|
-
* evaluate as expressions, not as truthy string literals
|
|
154
|
-
* `{ dialect, source }`
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
167
|
+
* evaluate as expressions, not as truthy string literals, and a spec-style
|
|
168
|
+
* `{ dialect: 'cel', source }` envelope is passed through INTACT so
|
|
169
|
+
* `evaluateCondition` routes it to the canonical `@objectstack/formula`
|
|
170
|
+
* engine (#2661). Unwrapping the envelope here — as this method used to —
|
|
171
|
+
* dropped the dialect and silently demoted every CEL predicate to the
|
|
172
|
+
* legacy JS evaluator, so the same `visible:` reached a different verdict
|
|
173
|
+
* via the engine than via a renderer (#3314): CEL faults on `null < null`
|
|
174
|
+
* and fail-closed hides, JS quietly returns `false`.
|
|
158
175
|
*/
|
|
159
176
|
getActionsForLocation(location) {
|
|
160
177
|
const evaluator = this.runner.getEvaluator();
|
|
@@ -181,18 +198,17 @@ export class ActionEngine {
|
|
|
181
198
|
return true;
|
|
182
199
|
if (raw === false)
|
|
183
200
|
return false;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
expr = Boolean(raw);
|
|
201
|
+
// #3314 — one shared normalization, not two. Hand-rolling the envelope
|
|
202
|
+
// unwrap here is what dropped `dialect: 'cel'` and demoted the
|
|
203
|
+
// predicate to the legacy JS engine; `toPredicateInput` keeps the
|
|
204
|
+
// envelope so `evaluateCondition` can route it to `@objectstack/formula`
|
|
205
|
+
// (the engine the server enforces with), exactly as the renderers do.
|
|
206
|
+
const expr = toPredicateInput(raw);
|
|
207
|
+
if (expr === undefined) {
|
|
208
|
+
// Not an evaluable predicate: an envelope with an empty `source`
|
|
209
|
+
// (→ nothing declared → visible), or a stray non-predicate value,
|
|
210
|
+
// which keeps the historical `Boolean(raw)` coercion.
|
|
211
|
+
return typeof raw === 'object' ? true : Boolean(raw);
|
|
196
212
|
}
|
|
197
213
|
try {
|
|
198
214
|
// `throwOnError: true` is required for fail-closed semantics: the
|
|
@@ -200,7 +216,9 @@ export class ActionEngine {
|
|
|
200
216
|
// returns the raw template string, which `Boolean(string)` then
|
|
201
217
|
// coerces to `true` — silently leaking actions whose preconditions
|
|
202
218
|
// failed (e.g. `ctx` undefined). Surface the error so our catch
|
|
203
|
-
// below can hide the action.
|
|
219
|
+
// below can hide the action. It is equally load-bearing on the CEL
|
|
220
|
+
// path, where the canonical helper fails SOFT to visible/enabled
|
|
221
|
+
// unless the caller opts in.
|
|
204
222
|
return evaluator.evaluateCondition(expr, { throwOnError: true });
|
|
205
223
|
}
|
|
206
224
|
catch (err) {
|
|
@@ -5,6 +5,7 @@
|
|
|
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 { ActionInput as SpecActionInput } from '@objectstack/spec/ui';
|
|
8
9
|
import { ExpressionEvaluator } from '../evaluator/ExpressionEvaluator';
|
|
9
10
|
import { type UndoableOperation } from './UndoManager';
|
|
10
11
|
export interface ActionResult {
|
|
@@ -65,7 +66,12 @@ export interface ActionDef {
|
|
|
65
66
|
/** Action type identifier — a `RunnableActionType` (the spec's six plus the
|
|
66
67
|
* `navigation` alias of `url`) or a custom type with a registered handler. */
|
|
67
68
|
type?: string;
|
|
68
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Legacy action type field.
|
|
71
|
+
*
|
|
72
|
+
* @deprecated objectui dialect — use the spec spelling `type`. Both are read,
|
|
73
|
+
* `type` first (objectstack#4075 step 2).
|
|
74
|
+
*/
|
|
69
75
|
actionType?: string;
|
|
70
76
|
/** Action name (from UIActionSchema) */
|
|
71
77
|
name?: string;
|
|
@@ -84,13 +90,36 @@ export interface ActionDef {
|
|
|
84
90
|
condition?: string;
|
|
85
91
|
/** Disabled expression — if truthy, skip action */
|
|
86
92
|
disabled?: string | boolean;
|
|
87
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* API endpoint (string URL or complex config).
|
|
95
|
+
*
|
|
96
|
+
* @deprecated objectui dialect — the spec spells an `api` action's endpoint
|
|
97
|
+
* `target` (with the verb in `method`). `executeAPI` resolves
|
|
98
|
+
* `api || endpoint || target`, so `target` alone is sufficient. The
|
|
99
|
+
* `ApiConfig` object form has no spec counterpart and is objectui-only
|
|
100
|
+
* (objectstack#4075 step 2).
|
|
101
|
+
*/
|
|
88
102
|
api?: string | ApiConfig;
|
|
89
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* API endpoint URL.
|
|
105
|
+
*
|
|
106
|
+
* @deprecated objectui dialect — this was the "spec v2.0.1 alias", but spec 17
|
|
107
|
+
* spells it `target`. `executeAPI` resolves `api || endpoint || target`
|
|
108
|
+
* (objectstack#4075 step 2).
|
|
109
|
+
*/
|
|
90
110
|
endpoint?: string;
|
|
91
111
|
/** HTTP method */
|
|
92
112
|
method?: string;
|
|
93
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* Navigation target — a nested envelope carrying the `navigation` alias's own
|
|
115
|
+
* spelling (`to` / `external` / `newTab` / `replace`).
|
|
116
|
+
*
|
|
117
|
+
* @deprecated objectui dialect — the spec has no nested navigation envelope;
|
|
118
|
+
* it spells a navigation action `{ type: 'url', target, openIn }`.
|
|
119
|
+
* `executeNavigation` reads `navigate.to || navigate.target || navigate.redirect`,
|
|
120
|
+
* falling back to the same keys on the action itself, so a flat `target` works
|
|
121
|
+
* today (objectstack#4075 step 2).
|
|
122
|
+
*/
|
|
94
123
|
navigate?: any;
|
|
95
124
|
/** onClick callback (legacy) */
|
|
96
125
|
onClick?: () => void | Promise<void>;
|
|
@@ -147,8 +176,9 @@ export interface ActionDef {
|
|
|
147
176
|
* would diverge silently rather than fail.
|
|
148
177
|
*
|
|
149
178
|
* Consumers dispatch bodies by registering a `script` handler that POSTs to
|
|
150
|
-
* `/api/v1/actions/{object}/{action}
|
|
151
|
-
*
|
|
179
|
+
* `/api/v1/actions/{object}/{action}`; the server runs the body through its
|
|
180
|
+
* sandbox. Build that handler with `createServerActionHandler` (#2904) —
|
|
181
|
+
* app-shell's `useConsoleActionRuntime` and `RecordDetailView` both do.
|
|
152
182
|
*/
|
|
153
183
|
body?: unknown;
|
|
154
184
|
/** For type: 'url' — where to open `target`. `'new-tab'` forces a new
|
|
@@ -177,10 +207,82 @@ export interface ActionDef {
|
|
|
177
207
|
* must perform all auth/authz itself (e.g. the cloud `/sso-open` endpoint,
|
|
178
208
|
* which re-runs every check the POST half would have done). */
|
|
179
209
|
newTabUrl?: string;
|
|
210
|
+
/** Where this action renders (`record_header`, `list_toolbar`, …). Read by
|
|
211
|
+
* `ActionEngine.registerActions` to seed the location filter. */
|
|
212
|
+
locations?: SpecActionInput['locations'];
|
|
213
|
+
/**
|
|
214
|
+
* Visibility predicate, evaluated against the runner's context.
|
|
215
|
+
*
|
|
216
|
+
* The `boolean` arm is objectui's own, deliberately wider than the spec:
|
|
217
|
+
* `ActionSchema.visible` admits only a CEL string or a `{ dialect, source }`
|
|
218
|
+
* envelope, while `ActionEngine.getActionsForLocation` also honours a literal
|
|
219
|
+
* `visible: true` / `false` (pinned by `ActionEngine.visibility.test.ts`).
|
|
220
|
+
* Declared rather than left to the index signature so the tolerance is
|
|
221
|
+
* auditable instead of invisible — but it IS a dialect, and objectstack#4075
|
|
222
|
+
* step 3 has to decide whether the spec adopts the boolean or objectui drops
|
|
223
|
+
* it. Do not widen this further.
|
|
224
|
+
*/
|
|
225
|
+
visible?: SpecActionInput['visible'] | boolean;
|
|
226
|
+
/** System permissions the caller must ALL hold for the action to be offered
|
|
227
|
+
* (ADR-0066 D4 UI half — the server enforces the source of truth). */
|
|
228
|
+
requiredPermissions?: SpecActionInput['requiredPermissions'];
|
|
229
|
+
/** Icon name for the rendered control. */
|
|
230
|
+
icon?: SpecActionInput['icon'];
|
|
231
|
+
/** Visual emphasis of the rendered control (`primary`, `danger`, …). */
|
|
232
|
+
variant?: SpecActionInput['variant'];
|
|
233
|
+
/** Sort weight among sibling actions. */
|
|
234
|
+
order?: SpecActionInput['order'];
|
|
235
|
+
/** Which renderer surfaces the action (`action:button`, `action:menu`, …). */
|
|
236
|
+
component?: SpecActionInput['component'];
|
|
237
|
+
/** Object this action is declared against. */
|
|
238
|
+
objectName?: SpecActionInput['objectName'];
|
|
239
|
+
/** AI affordance metadata (tool exposure, prompts). */
|
|
240
|
+
ai?: SpecActionInput['ai'];
|
|
241
|
+
/** Accessibility overrides for the rendered control. */
|
|
242
|
+
aria?: SpecActionInput['aria'];
|
|
243
|
+
/** Extra properties merged into the request body alongside the collected params. */
|
|
244
|
+
bodyExtra?: SpecActionInput['bodyExtra'];
|
|
245
|
+
/** How collected params are shaped into the request body (`flat` or nested). */
|
|
246
|
+
bodyShape?: SpecActionInput['bodyShape'];
|
|
247
|
+
/** Execution mode of the action. */
|
|
248
|
+
mode?: SpecActionInput['mode'];
|
|
249
|
+
/** Field on the record supplying the record id sent to the endpoint. */
|
|
250
|
+
recordIdField?: SpecActionInput['recordIdField'];
|
|
251
|
+
/** Request-parameter name carrying the record id. */
|
|
252
|
+
recordIdParam?: SpecActionInput['recordIdParam'];
|
|
253
|
+
/** Auth/tenancy feature the action requires before it is offered. */
|
|
254
|
+
requiresFeature?: SpecActionInput['requiresFeature'];
|
|
255
|
+
/**
|
|
256
|
+
* @deprecated Retired in `@objectstack/spec` 17 as a `retiredKey()` tombstone —
|
|
257
|
+
* authoring it is a hard parse rejection, so this resolves to `undefined` and
|
|
258
|
+
* assigning a value is a compile error. A HOST may still pass a shortcut
|
|
259
|
+
* explicitly via `ActionEngine.registerAction(action, { shortcut })`; it is
|
|
260
|
+
* only no longer sourced from authored metadata. No replacement key.
|
|
261
|
+
*/
|
|
262
|
+
shortcut?: SpecActionInput['shortcut'];
|
|
263
|
+
/**
|
|
264
|
+
* @deprecated Retired in `@objectstack/spec` 17 as a `retiredKey()` tombstone —
|
|
265
|
+
* authoring it is a hard parse rejection, so this resolves to `undefined` and
|
|
266
|
+
* assigning a value is a compile error. Use the list view's `bulkActions` /
|
|
267
|
+
* `bulkActionDefs` instead; `ActionEngine.registerAction(action, { bulkEnabled })`
|
|
268
|
+
* remains available to a HOST passing it explicitly.
|
|
269
|
+
*/
|
|
270
|
+
bulkEnabled?: SpecActionInput['bulkEnabled'];
|
|
180
271
|
/** Any additional properties */
|
|
181
272
|
[key: string]: any;
|
|
182
273
|
}
|
|
183
|
-
|
|
274
|
+
/**
|
|
275
|
+
* A handler registered with {@link ActionRunner} — the client-side action
|
|
276
|
+
* dispatch seam: given an authored `ActionDef` and the invocation context,
|
|
277
|
+
* produce an `ActionResult`.
|
|
278
|
+
*
|
|
279
|
+
* NOT the spec's `ActionHandler` (`@objectstack/spec/ui`), whose name this type
|
|
280
|
+
* carried until objectstack#4115. That one is the SERVER-side objectql action
|
|
281
|
+
* handler — a different calling convention entirely (`(ctx) => unknown`, with
|
|
282
|
+
* validated params and a trusted engine facade on `ctx`) and a different
|
|
283
|
+
* execution site.
|
|
284
|
+
*/
|
|
285
|
+
export type ActionRunnerHandler = (action: ActionDef, context: ActionContext) => Promise<ActionResult> | ActionResult;
|
|
184
286
|
/**
|
|
185
287
|
* Confirmation handler — replaces window.confirm.
|
|
186
288
|
* Consumers can provide an async implementation (e.g., Shadcn AlertDialog).
|
|
@@ -252,6 +354,19 @@ export type ResultDialogHandler = (spec: ResultDialogSpec, data: unknown, action
|
|
|
252
354
|
/**
|
|
253
355
|
* ActionParam definition accepted by the runner.
|
|
254
356
|
* Compatible with @objectstack/spec ActionParam.
|
|
357
|
+
*
|
|
358
|
+
* The RESOLVED shape — what `resolveActionParams()` emits and `ActionParamDialog`
|
|
359
|
+
* reads, after a field-backed param's `{ field }` reference has been inlined.
|
|
360
|
+
* The AUTHORING counterpart is `@object-ui/types`' `ActionParam` (objectui#3174).
|
|
361
|
+
*
|
|
362
|
+
* `validation?: string` used to sit here too, and was removed by objectui#3201:
|
|
363
|
+
* no producer ever wrote it (it was never a key of `resolveActionParams()`'s
|
|
364
|
+
* `RawActionParam`, and the runtime field metadata a field-backed param inherits
|
|
365
|
+
* from carries no `validation` either) and no consumer ever read it —
|
|
366
|
+
* `paramToField()` did not map it, so it never reached the field widgets, whose
|
|
367
|
+
* rules `buildValidationRules()` builds from `required` / `minLength` /
|
|
368
|
+
* `maxLength` / `pattern`. Declaring it on the resolved side only invited the
|
|
369
|
+
* authoring side to declare it back.
|
|
255
370
|
*/
|
|
256
371
|
export interface ActionParamDef {
|
|
257
372
|
name: string;
|
|
@@ -265,7 +380,6 @@ export interface ActionParamDef {
|
|
|
265
380
|
defaultValue?: unknown;
|
|
266
381
|
helpText?: string;
|
|
267
382
|
placeholder?: string;
|
|
268
|
-
validation?: string;
|
|
269
383
|
/**
|
|
270
384
|
* Visibility predicate (CEL) evaluated against the same scope as action
|
|
271
385
|
* `visible` (`current_user` / `app` / `data` / `features`). When it evaluates
|
|
@@ -353,7 +467,7 @@ export declare class ActionRunner {
|
|
|
353
467
|
* values the user must copy now (2FA codes, OAuth client_secret).
|
|
354
468
|
*/
|
|
355
469
|
setResultDialogHandler(handler: ResultDialogHandler): void;
|
|
356
|
-
registerHandler(actionName: string, handler:
|
|
470
|
+
registerHandler(actionName: string, handler: ActionRunnerHandler): void;
|
|
357
471
|
unregisterHandler(actionName: string): void;
|
|
358
472
|
/**
|
|
359
473
|
* Register a named script handler. When a `script` action's
|
|
@@ -361,7 +475,7 @@ export declare class ActionRunner {
|
|
|
361
475
|
* instead of the expression evaluator. Lets dashboards/views wire
|
|
362
476
|
* symbolic action names (e.g. 'export_dashboard_pdf') to JS callbacks.
|
|
363
477
|
*/
|
|
364
|
-
registerScript(scriptName: string, handler:
|
|
478
|
+
registerScript(scriptName: string, handler: ActionRunnerHandler): void;
|
|
365
479
|
unregisterScript(scriptName: string): void;
|
|
366
480
|
execute(action: ActionDef): Promise<ActionResult>;
|
|
367
481
|
/**
|
|
@@ -467,9 +581,15 @@ export declare class ActionRunner {
|
|
|
467
581
|
* fragment.
|
|
468
582
|
*
|
|
469
583
|
* `ctx.origin`, `ctx.user.*`, `ctx.org.*`, `ctx.recordId` are surfaced
|
|
470
|
-
* implicitly from the runner's ActionContext + window
|
|
471
|
-
*
|
|
472
|
-
*
|
|
584
|
+
* implicitly from the runner's ActionContext + window, and
|
|
585
|
+
* `ctx.selection.ids` / `ctx.selection.count` reflect the grid's current
|
|
586
|
+
* checkbox selection (`context.selectedRecords`) — `${ctx.selection.ids}`
|
|
587
|
+
* comma-joins the ids via the standard `String(array)` behaviour, so a
|
|
588
|
+
* list_toolbar url/api action can carry the selection without any bulk
|
|
589
|
+
* plumbing (objectui#3139). In an aggregate bulk dispatch,
|
|
590
|
+
* `${param._selectedIds}` interpolates the same ids from the injected
|
|
591
|
+
* param. Consumers can extend by stuffing extra keys under
|
|
592
|
+
* `context.ctx = {...}` before calling `runner.execute()`.
|
|
473
593
|
*/
|
|
474
594
|
private interpolateTarget;
|
|
475
595
|
/**
|
|
@@ -448,7 +448,8 @@ export class ActionRunner {
|
|
|
448
448
|
success: false,
|
|
449
449
|
error: 'Action body must be executed server-side — this client runner does not interpret ' +
|
|
450
450
|
'`body` (sandboxed JS needs an isolated VM; expression bodies use the formula engine). ' +
|
|
451
|
-
'Register a `script` handler that POSTs to /api/v1/actions/{object}/{action}
|
|
451
|
+
'Register a `script` handler that POSTs to /api/v1/actions/{object}/{action} — ' +
|
|
452
|
+
'build one with createServerActionHandler from @object-ui/core.',
|
|
452
453
|
};
|
|
453
454
|
}
|
|
454
455
|
// ActionDef is open-ended (`[key: string]: any`), so hand-authored
|
|
@@ -802,9 +803,15 @@ export class ActionRunner {
|
|
|
802
803
|
* fragment.
|
|
803
804
|
*
|
|
804
805
|
* `ctx.origin`, `ctx.user.*`, `ctx.org.*`, `ctx.recordId` are surfaced
|
|
805
|
-
* implicitly from the runner's ActionContext + window
|
|
806
|
-
*
|
|
807
|
-
*
|
|
806
|
+
* implicitly from the runner's ActionContext + window, and
|
|
807
|
+
* `ctx.selection.ids` / `ctx.selection.count` reflect the grid's current
|
|
808
|
+
* checkbox selection (`context.selectedRecords`) — `${ctx.selection.ids}`
|
|
809
|
+
* comma-joins the ids via the standard `String(array)` behaviour, so a
|
|
810
|
+
* list_toolbar url/api action can carry the selection without any bulk
|
|
811
|
+
* plumbing (objectui#3139). In an aggregate bulk dispatch,
|
|
812
|
+
* `${param._selectedIds}` interpolates the same ids from the injected
|
|
813
|
+
* param. Consumers can extend by stuffing extra keys under
|
|
814
|
+
* `context.ctx = {...}` before calling `runner.execute()`.
|
|
808
815
|
*/
|
|
809
816
|
interpolateTarget(target, action) {
|
|
810
817
|
if (typeof target !== 'string' || target.indexOf('${') === -1)
|
|
@@ -839,6 +846,22 @@ export class ActionRunner {
|
|
|
839
846
|
user: this.context.user ?? {},
|
|
840
847
|
org: this.context.org ?? this.context.organization ?? {},
|
|
841
848
|
recordId: this.context.record?.id ?? this.context.recordId,
|
|
849
|
+
// Current list selection (published by the grid as `selectedRecords`).
|
|
850
|
+
// `${ctx.selection.ids}` in a url/api target comma-joins the ids —
|
|
851
|
+
// String(array) — giving toolbar actions selection access without any
|
|
852
|
+
// bulk-executor involvement (objectui#3139). Empty when nothing is
|
|
853
|
+
// selected, so authors can treat "" as "no selection".
|
|
854
|
+
selection: {
|
|
855
|
+
ids: Array.isArray(this.context.selectedRecords)
|
|
856
|
+
? this.context.selectedRecords
|
|
857
|
+
.map((r) => r?.id)
|
|
858
|
+
.filter((v) => v != null)
|
|
859
|
+
.map(String)
|
|
860
|
+
: [],
|
|
861
|
+
count: Array.isArray(this.context.selectedRecords)
|
|
862
|
+
? this.context.selectedRecords.length
|
|
863
|
+
: 0,
|
|
864
|
+
},
|
|
842
865
|
};
|
|
843
866
|
if (this.context.ctx && typeof this.context.ctx === 'object') {
|
|
844
867
|
Object.assign(ctx, this.context.ctx);
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
* Resolve a human-readable message out of an ObjectStack error payload.
|
|
10
|
+
*
|
|
11
|
+
* Always returns a STRING. The envelope nests the message as
|
|
12
|
+
* `{ error: { code, message } }`, and passing that object through as
|
|
13
|
+
* `ActionResult.error` reaches `toast.error()` as a React child and crashes the
|
|
14
|
+
* page (React #31). That shape used to be rare on the `/actions` route —
|
|
15
|
+
* failures came back as HTTP 200 with a plain-string `error` — but since
|
|
16
|
+
* objectstack#3913 a failed action answers with a real status and the nested
|
|
17
|
+
* object, so every `/actions` caller has to go through this.
|
|
18
|
+
*
|
|
19
|
+
* Handles, in order: `{error: 'msg'}`, `{error: {message: 'msg'}}`,
|
|
20
|
+
* `{message: 'msg'}`, else the caller's fallback.
|
|
21
|
+
*
|
|
22
|
+
* Moved here from `@object-ui/app-shell` (`utils/actionErrorDetail`) by
|
|
23
|
+
* objectui#2904 so `createServerActionHandler` — the core dispatch every
|
|
24
|
+
* consumer registers — can own the rule.
|
|
25
|
+
*/
|
|
26
|
+
export declare function actionErrorDetail(body: unknown, fallback: string): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
* Resolve a human-readable message out of an ObjectStack error payload.
|
|
10
|
+
*
|
|
11
|
+
* Always returns a STRING. The envelope nests the message as
|
|
12
|
+
* `{ error: { code, message } }`, and passing that object through as
|
|
13
|
+
* `ActionResult.error` reaches `toast.error()` as a React child and crashes the
|
|
14
|
+
* page (React #31). That shape used to be rare on the `/actions` route —
|
|
15
|
+
* failures came back as HTTP 200 with a plain-string `error` — but since
|
|
16
|
+
* objectstack#3913 a failed action answers with a real status and the nested
|
|
17
|
+
* object, so every `/actions` caller has to go through this.
|
|
18
|
+
*
|
|
19
|
+
* Handles, in order: `{error: 'msg'}`, `{error: {message: 'msg'}}`,
|
|
20
|
+
* `{message: 'msg'}`, else the caller's fallback.
|
|
21
|
+
*
|
|
22
|
+
* Moved here from `@object-ui/app-shell` (`utils/actionErrorDetail`) by
|
|
23
|
+
* objectui#2904 so `createServerActionHandler` — the core dispatch every
|
|
24
|
+
* consumer registers — can own the rule.
|
|
25
|
+
*/
|
|
26
|
+
export function actionErrorDetail(body, fallback) {
|
|
27
|
+
const b = body;
|
|
28
|
+
const err = b?.error;
|
|
29
|
+
if (typeof err === 'string' && err.length > 0)
|
|
30
|
+
return err;
|
|
31
|
+
const nested = err?.message;
|
|
32
|
+
if (typeof nested === 'string' && nested.length > 0)
|
|
33
|
+
return nested;
|
|
34
|
+
if (typeof b?.message === 'string' && b.message.length > 0)
|
|
35
|
+
return b.message;
|
|
36
|
+
return fallback;
|
|
37
|
+
}
|
|
@@ -32,25 +32,42 @@
|
|
|
32
32
|
* interface it mirrors would be the same "declared ≠ enforced" trap this whole
|
|
33
33
|
* thread is about.
|
|
34
34
|
*
|
|
35
|
-
* ── What the inventory found,
|
|
36
|
-
* `ActionDef`
|
|
37
|
-
*
|
|
35
|
+
* ── What the inventory found, and what step 2 did with it ────────────────────
|
|
36
|
+
* Step 1 found `ActionDef` declaring 34 keys against the spec `ActionSchema`'s
|
|
37
|
+
* 36, overlapping on 17. The two halves of the difference were different
|
|
38
|
+
* problems, and step 2 (objectstack#4075) treated them differently:
|
|
38
39
|
*
|
|
39
40
|
* 18 keys the SPEC owns that `ActionDef` never declared — `ai`, `aria`,
|
|
40
41
|
* `bodyExtra`, `bodyShape`, `bulkEnabled`, `component`, `icon`, `locations`,
|
|
41
42
|
* `mode`, `objectName`, `order`, `recordIdField`, `recordIdParam`,
|
|
42
43
|
* `requiredPermissions`, `requiresFeature`, `shortcut`, `variant`, `visible`.
|
|
43
|
-
* These are authored today and reach readers through the index
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
* These are authored today and used to reach readers through the index
|
|
45
|
+
* signature. **Step 2 promoted all 18 to explicit optional fields**, typed by
|
|
46
|
+
* DERIVATION from `@objectstack/spec`'s `ActionInput` rather than by hand-copied
|
|
47
|
+
* shapes. The `as any` casts `ActionEngine` needed to read `visible`,
|
|
48
|
+
* `locations` and `requiredPermissions` are gone with them — those casts existed
|
|
49
|
+
* only because the fields were undeclared.
|
|
50
|
+
*
|
|
51
|
+
* Two of the 18 landed as `undefined` rather than as usable types, which is the
|
|
52
|
+
* correct outcome: spec 17 retired `shortcut` and `bulkEnabled` as `retiredKey()`
|
|
53
|
+
* tombstones (`z.never()`), so deriving turns "authoring this is a parse
|
|
54
|
+
* rejection" into a compile error at no cost. Hand-copying would have quietly
|
|
55
|
+
* re-legitimized two dead keys — which is why the types are derived.
|
|
47
56
|
*
|
|
48
57
|
* 17 keys `ActionDef` declares that the spec does not own — `actionType`, `api`,
|
|
49
58
|
* `chain`, `chainMode`, `close`, `condition`, `confirm`, `endpoint`, `modal`,
|
|
50
59
|
* `navigate`, `onClick`, `onFailure`, `onSuccess`, `redirect`, `reload`, `toast`,
|
|
51
|
-
* `actionParams`.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
60
|
+
* `actionParams`. Step 2 marked `@deprecated`, with the spec spelling to use
|
|
61
|
+
* instead, ONLY the four the runner itself proves are aliases: `actionType` (→
|
|
62
|
+
* `type`), `api` and `endpoint` (→ `target`; `executeAPI` resolves
|
|
63
|
+
* `api || endpoint || target`), and `navigate` (→ flat `target`/`openIn`;
|
|
64
|
+
* `executeNavigation` already falls back to them). The rest are runner mechanics
|
|
65
|
+
* with no spec counterpart — chaining, toasts, post-execution reload/close — and
|
|
66
|
+
* deprecating them toward a spelling that does not exist would be worse than
|
|
67
|
+
* leaving them declared.
|
|
68
|
+
*
|
|
69
|
+
* Step 3 remains: remove the index signature, at which point `tsc` catches both
|
|
70
|
+
* typos and retired keys and the dev-mode warning below can retire with it.
|
|
54
71
|
*/
|
|
55
72
|
/**
|
|
56
73
|
* Every property `ActionDef` declares, in declaration order.
|
|
@@ -59,7 +76,7 @@
|
|
|
59
76
|
* `keyof ActionDef` cannot substitute for this list: the index signature widens
|
|
60
77
|
* it to `string | number`, which is precisely the problem being worked around.
|
|
61
78
|
*/
|
|
62
|
-
export declare const ACTION_DEF_KEYS: readonly ["type", "actionType", "name", "label", "confirmText", "confirm", "condition", "disabled", "api", "endpoint", "method", "navigate", "onClick", "reload", "close", "redirect", "toast", "successMessage", "errorMessage", "refreshAfter", "undoable", "params", "actionParams", "resultDialog", "target", "body", "openIn", "modal", "chain", "chainMode", "onSuccess", "onFailure", "opensInNewTab", "newTabUrl"];
|
|
79
|
+
export declare const ACTION_DEF_KEYS: readonly ["type", "actionType", "name", "label", "confirmText", "confirm", "condition", "disabled", "api", "endpoint", "method", "navigate", "onClick", "reload", "close", "redirect", "toast", "successMessage", "errorMessage", "refreshAfter", "undoable", "params", "actionParams", "resultDialog", "target", "body", "openIn", "modal", "chain", "chainMode", "onSuccess", "onFailure", "opensInNewTab", "newTabUrl", "locations", "visible", "requiredPermissions", "icon", "variant", "order", "component", "objectName", "ai", "aria", "bodyExtra", "bodyShape", "mode", "recordIdField", "recordIdParam", "requiresFeature", "shortcut", "bulkEnabled"];
|
|
63
80
|
/**
|
|
64
81
|
* Every property `@objectstack/spec`'s `ActionSchema` declares.
|
|
65
82
|
*
|
|
@@ -74,7 +91,7 @@ export declare const ACTION_DEF_KEYS: readonly ["type", "actionType", "name", "l
|
|
|
74
91
|
* test and wrong in shipped code — `@object-ui/types` made the same call for
|
|
75
92
|
* `ActionComponent`. The pin test does the walk and fails the day this drifts.
|
|
76
93
|
*/
|
|
77
|
-
export declare const SPEC_ACTION_KEYS: readonly ["ai", "aria", "body", "bodyExtra", "bodyShape", "bulkEnabled", "component", "confirmText", "disabled", "errorMessage", "execute", "icon", "label", "locations", "method", "mode", "name", "newTabUrl", "objectName", "openIn", "opensInNewTab", "order", "params", "recordIdField", "recordIdParam", "refreshAfter", "requiredPermissions", "requiresFeature", "resultDialog", "shortcut", "successMessage", "target", "type", "undoable", "variant", "visible"];
|
|
94
|
+
export declare const SPEC_ACTION_KEYS: readonly ["_lock", "_lockDocsUrl", "_lockReason", "_lockSource", "_packageId", "_packageVersion", "_provenance", "ai", "aria", "body", "bodyExtra", "bodyShape", "bulkEnabled", "component", "confirmText", "disabled", "errorMessage", "execute", "icon", "label", "locations", "method", "mode", "name", "newTabUrl", "objectName", "openIn", "opensInNewTab", "order", "params", "recordIdField", "recordIdParam", "refreshAfter", "requiredPermissions", "requiresFeature", "resultDialog", "shortcut", "successMessage", "target", "type", "undoable", "variant", "visible"];
|
|
78
95
|
/**
|
|
79
96
|
* The `navigation` alias's own spelling of a target, read off the action itself
|
|
80
97
|
* when no nested `navigate` object is present (`ActionRunner.executeNavigation`).
|