@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.
Files changed (66) hide show
  1. package/CHANGELOG.md +738 -0
  2. package/README.md +29 -0
  3. package/dist/actions/ActionEngine.d.ts +22 -7
  4. package/dist/actions/ActionEngine.js +40 -22
  5. package/dist/actions/ActionRunner.d.ts +133 -13
  6. package/dist/actions/ActionRunner.js +27 -4
  7. package/dist/actions/actionErrorDetail.d.ts +26 -0
  8. package/dist/actions/actionErrorDetail.js +37 -0
  9. package/dist/actions/actionKeys.d.ts +29 -12
  10. package/dist/actions/actionKeys.js +61 -10
  11. package/dist/actions/actionResponse.d.ts +42 -0
  12. package/dist/actions/actionResponse.js +104 -0
  13. package/dist/actions/index.d.ts +3 -0
  14. package/dist/actions/index.js +3 -0
  15. package/dist/actions/serverActionHandler.d.ts +143 -0
  16. package/dist/actions/serverActionHandler.js +137 -0
  17. package/dist/evaluator/ExpressionEvaluator.js +4 -2
  18. package/dist/evaluator/fieldRules.d.ts +26 -2
  19. package/dist/evaluator/fieldRules.js +63 -8
  20. package/dist/evaluator/index.d.ts +1 -0
  21. package/dist/evaluator/index.js +1 -0
  22. package/dist/evaluator/listConditional.d.ts +13 -1
  23. package/dist/evaluator/listConditional.js +0 -0
  24. package/dist/evaluator/optionRules.js +5 -1
  25. package/dist/evaluator/predicateInput.d.ts +84 -0
  26. package/dist/evaluator/predicateInput.js +72 -0
  27. package/dist/index.d.ts +3 -0
  28. package/dist/index.js +12 -0
  29. package/dist/protocols/index.d.ts +0 -1
  30. package/dist/protocols/index.js +8 -1
  31. package/dist/query/query-ast.d.ts +6 -6
  32. package/dist/query/query-ast.js +2 -2
  33. package/dist/registry/PluginSystem.d.ts +15 -5
  34. package/dist/registry/PluginSystem.js +1 -1
  35. package/dist/registry/WidgetRegistry.d.ts +7 -7
  36. package/dist/registry/WidgetRegistry.js +1 -1
  37. package/dist/styling/scoped-styles.d.ts +14 -8
  38. package/dist/utils/chart-series.d.ts +14 -2
  39. package/dist/utils/dashboard-filters.d.ts +9 -0
  40. package/dist/utils/dashboard-filters.js +99 -4
  41. package/dist/utils/dataset-format.d.ts +18 -1
  42. package/dist/utils/dataset-format.js +10 -2
  43. package/dist/utils/dataset-pivot.d.ts +57 -0
  44. package/dist/utils/dataset-pivot.js +57 -0
  45. package/dist/utils/filter-converter.d.ts +39 -11
  46. package/dist/utils/filter-converter.js +87 -7
  47. package/dist/utils/filter-tokens.d.ts +13 -6
  48. package/dist/utils/filter-tokens.js +13 -6
  49. package/dist/utils/freeze-schema.d.ts +14 -6
  50. package/dist/utils/freeze-schema.js +14 -6
  51. package/dist/utils/managedBy.d.ts +78 -50
  52. package/dist/utils/managedBy.js +75 -52
  53. package/dist/utils/normalize-list-view.d.ts +12 -2
  54. package/dist/utils/predicate-fields.d.ts +96 -0
  55. package/dist/utils/predicate-fields.js +163 -0
  56. package/dist/utils/predicate-record.d.ts +32 -0
  57. package/dist/utils/predicate-record.js +120 -0
  58. package/dist/utils/record-title.js +0 -0
  59. package/dist/validation/index.d.ts +1 -0
  60. package/dist/validation/index.js +1 -0
  61. package/dist/validation/required-presence.d.ts +31 -0
  62. package/dist/validation/required-presence.js +36 -0
  63. package/dist/validation/schema-validator.d.ts +24 -8
  64. package/package.json +4 -4
  65. package/dist/protocols/NotificationProtocol.d.ts +0 -71
  66. 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
- /** Register multiple actions from an ActionSchema array */
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 matches `@object-ui/react`'s `toPredicateInput`:
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. Spec-style
104
- * `{ dialect, source }` envelopes are unwrapped to their `source`.
105
- * Without this, every per-action renderer (which already calls
106
- * `toPredicateInput`) and the engine path would disagree on the same
107
- * `visible:` predicate.
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
- /** Register multiple actions from an ActionSchema array */
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 matches `@object-ui/react`'s `toPredicateInput`:
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. Spec-style
154
- * `{ dialect, source }` envelopes are unwrapped to their `source`.
155
- * Without this, every per-action renderer (which already calls
156
- * `toPredicateInput`) and the engine path would disagree on the same
157
- * `visible:` predicate.
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
- let expr;
185
- if (typeof raw === 'string') {
186
- expr = `\${${raw}}`;
187
- }
188
- else if (typeof raw === 'object' && typeof raw.source === 'string') {
189
- const src = raw.source;
190
- if (!src)
191
- return true;
192
- expr = `\${${src}}`;
193
- }
194
- else {
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
- /** Legacy action type field */
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
- /** API endpoint (string URL or complex config) */
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
- /** API endpoint URL (spec v2.0.1 alias) */
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
- /** Navigation target */
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}` (see app-shell's
151
- * `useConsoleActionRuntime`); the server runs the body through its sandbox.
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
- export type ActionHandler = (action: ActionDef, context: ActionContext) => Promise<ActionResult> | ActionResult;
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: ActionHandler): void;
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: ActionHandler): void;
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. Consumers can
471
- * extend by stuffing extra keys under `context.ctx = {...}` before
472
- * calling `runner.execute()`.
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. Consumers can
806
- * extend by stuffing extra keys under `context.ctx = {...}` before
807
- * calling `runner.execute()`.
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, i.e. step 2's worklist ─────────────────────────
36
- * `ActionDef` declares 34 keys, the spec's `ActionSchema` declares 36, and they
37
- * overlap on 17. The two halves of the difference are different problems:
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 signature.
44
- * `ActionEngine` reads two of them (`visible` at the location filter, `locations`
45
- * at registration) through an `as any` cast — a cast that exists ONLY because the
46
- * field is undeclared, and that goes away when step 2 promotes them.
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`. Several are already marked legacy at their declaration. These
52
- * are objectui's own dialect and need the #4115 treatment: kept and documented as
53
- * deliberate, or retired — but not silently carried.
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`).