@object-ui/core 6.2.2 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,210 @@
1
1
  # @object-ui/core
2
2
 
3
+ ## 7.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f7f325d: feat: action progress state + Undo affordance
8
+
9
+ - **core**: `ActionResult.undo` (an `UndoableOperation`) and `ActionDef.undoable`.
10
+ On success the `ActionRunner` pushes the operation onto the global UndoManager
11
+ and the success toast carries an "Undo" affordance (`ToastHandler` gains an
12
+ `undo` option).
13
+ - **app-shell**: the console action runtime mounts `useGlobalUndo` (Ctrl+Z /
14
+ Ctrl+Shift+Z) and renders the toast's "Undo" button; its `apiHandler` resolves
15
+ the row id from the list row record and, for `undoable` actions, captures the
16
+ changed fields' prior values so the update can be reverted.
17
+ - **plugin-detail**: record-header quick-action buttons show a spinner + disable
18
+ while the action runs (a visible progress state for slow/flow actions).
19
+
20
+ - c12986e: Add resultDialog + target interpolation for one-shot action reveals
21
+
22
+ Some platform actions return values the user MUST copy now because the
23
+ server will not surface them again — 2FA TOTP URI + backup codes, freshly
24
+ minted OAuth client_secret, regenerated recovery codes. Previously these
25
+ had to ship as bespoke pages in `apps/account` because actions only
26
+ emitted a fire-and-forget toast.
27
+
28
+ **`@object-ui/core` — ActionRunner**
29
+
30
+ - New `ActionDef.resultDialog: ResultDialogSpec` field. When set on a
31
+ successful action, the runner suppresses the `successMessage` toast and
32
+ awaits the registered `ResultDialogHandler` instead. Missing handler is
33
+ non-fatal (logs a warning); rejected handler is treated as acknowledged.
34
+ - New `setResultDialogHandler(handler)` setter.
35
+ - New types: `ResultDialogSpec`, `ResultDialogFieldSpec`,
36
+ `ResultDialogHandler`.
37
+ - `executeUrl` and `executeAPI` now run `${param.X}` and `${ctx.X}`
38
+ interpolation against `target` before fetching / navigating. Values are
39
+ `encodeURIComponent`'d, missing keys resolve to empty string. `ctx`
40
+ exposes `origin`, `user`, `org`, `recordId` by default; consumers can
41
+ inject more via `context.ctx`.
42
+
43
+ **`@object-ui/react`**
44
+
45
+ - `ActionProvider` and `useActionRunner` both gained an `onResultDialog`
46
+ option that wires straight through to the runner.
47
+
48
+ **`@object-ui/app-shell`**
49
+
50
+ - New `ActionResultDialog` component — promise-based, blocks click-outside
51
+ and Escape (the user MUST click acknowledge), renders five field
52
+ formats: `qrcode` (client-side via the `qrcode` package — never sent
53
+ off-device, so 2FA URIs stay secret), `code-list`, `secret`, `text`,
54
+ `json`. Falls back to `json` when a value's shape doesn't match its
55
+ declared format.
56
+ - `ObjectView` and `RecordDetailView` install the handler and mount the
57
+ dialog automatically, so any action with `resultDialog` declared in
58
+ metadata now works without code changes.
59
+ - New dependency: `qrcode@^1.5.x` for client-side QR rendering.
60
+
61
+ Pairs with the framework-side `Action.resultDialog` schema added in
62
+ `@objectstack/spec` and the `sys_two_factor` / `sys_oauth_application` /
63
+ `sys_account` updates in `@objectstack/platform-objects`.
64
+
65
+ - 053c948: feat(app-shell): zero-roundtrip `newTabUrl` fast path for `opensInNewTab` actions
66
+
67
+ Actions that declare `newTabUrl` (a path template with a `{recordId}` placeholder
68
+ whose target endpoint performs all auth/authz itself) now drive the pre-opened
69
+ popup straight to that URL on click, skipping the action POST entirely — applied
70
+ to both server-action paths (list rows via `useConsoleActionRuntime`, record
71
+ header via `RecordDetailView`). The popup paints the existing spinner page until
72
+ the (possibly slow) endpoint commits its redirect; the URL is resolved absolute
73
+ because `about:blank` gives a bare-relative href no reliable base. The
74
+ popup-blocked toast fallback is unchanged. Removes one full round trip of
75
+ white-screen latency from every such Open click.
76
+
77
+ - ddbe4a2: B2 step 3: client-side field-level conditional rules (`visibleWhen` / `readonlyWhen` / `requiredWhen`). The form renderer now evaluates these CEL predicates reactively against the live record and gates each field's visibility, read-only state, and required-ness accordingly. Evaluation delegates to the canonical `@objectstack/formula` `ExpressionEngine` — the _same_ dialect the server enforces (`requiredWhen` in the rule-validator, `readonlyWhen` in `stripReadonlyWhenFields`) — so the UX and the persisted verdict always agree. New core helpers `evalFieldPredicate` / `resolveFieldRuleState` (zero-React, fail-open). `FormField` gains `visibleWhen` / `readonlyWhen` / `requiredWhen` (+ deprecated `conditionalRequired` alias), and `ObjectForm` carries them through from object metadata.
78
+ - d54346c: feat: action/flow completion messaging
79
+
80
+ - **core**: `ActionResult.silent` — a handler sets it when the action only
81
+ HANDED OFF to a follow-up UI (rather than completing), so `ActionRunner`
82
+ skips the automatic success toast. Fixes the misleading "Action completed
83
+ successfully" toast that fired the moment a `flow` action opened its wizard.
84
+ - **app-shell**: both flow handlers now return `silent: true` when the flow
85
+ pauses at a screen (the wizard only opened — it hasn't completed). `FlowRunner`
86
+ renders the flow's declared `successMessage` / `errorMessage` (from the
87
+ terminal `AutomationResult`) instead of a generic "Done" / the raw error.
88
+
89
+ - 2270239: feat: scoped style-object rendering (ADR-0065)
90
+
91
+ A metadata node may carry `responsiveStyles` (per-breakpoint CSS-property maps);
92
+ `SchemaRenderer` compiles it to **id-scoped CSS** injected as a `<style>` tag and
93
+ appends a scope class to the node. Build-independent (arbitrary values + design
94
+ tokens pass through verbatim — no Tailwind JIT), collision-free (per-node scope,
95
+ unlayered so it beats base utilities), responsive-correct (model breakpoint maps
96
+ → generated `@media`, never `md:` variant classes). Adds `compileScopedStyles`/
97
+ `scopeClassFor`/`hasResponsiveStyles` to `@object-ui/core` and an SDUI design-token
98
+ palette (`--space-*`, `--surface`, `--brand`, …) to the theme. Mirrors Builder.io.
99
+
100
+ ### Patch Changes
101
+
102
+ - 5976ba3: fix(core): evaluate bare CEL predicates in `evaluateCondition`
103
+
104
+ `ExpressionEvaluator.evaluateCondition` delegated to `evaluate`, which only
105
+ processes `${...}` templates and returns any other string verbatim. A bare
106
+ predicate such as `record.status == "converted"` (the shape `objectstack build`
107
+ emits for `disabled`/`visible`/`condition`) was therefore returned as a
108
+ non-empty string and coerced to `true` — so every bare-expression predicate was
109
+ silently always-truthy.
110
+
111
+ The most visible symptom: a param-collecting `api` action invoked from the
112
+ record header (e.g. CRM "Reassign Lead") was treated as permanently `disabled`,
113
+ so `ActionRunner.execute` bailed before opening the param dialog. The renderer
114
+ (`page:header`) was unaffected because it evaluates via `evaluateExpression`
115
+ directly.
116
+
117
+ `evaluateCondition` now treats a non-`${}` condition as a single expression
118
+ (via `evaluateExpression`), keeps the `${...}` template path, and preserves the
119
+ "empty/undefined ⇒ visible/enabled" and "unparseable ⇒ default visible/enabled"
120
+ fallbacks. Also hardens `ActionRunner`'s `disabled` gate to evaluate the
121
+ boolean/string/envelope form rather than treating any object as truthy, and
122
+ unifies the grid row-action predicate scope so `record.*` and bare-field
123
+ predicates resolve identically on every surface.
124
+
125
+ - eaccefd: fix(actions): warn when an action is hidden by a throwing `visible` predicate
126
+
127
+ `ActionEngine.getActionsForLocation` is fail-closed: a `visible` predicate that
128
+ throws hides the action. The most common cause is an authoring bug — a BARE
129
+ field reference (`done` instead of `record.done`), which is undeclared in the
130
+ `{ record, recordId, objectName, user }` eval scope. Hiding it silently made
131
+ that bug invisible (a long debugging hunt). The catch now emits a one-time
132
+ `console.warn` naming the action + predicate + error, with the `record.<field>`
133
+ tip. Deduped per predicate so re-renders don't spam.
134
+
135
+ - 71d7ce0: fix(actions): handle `type: 'form'` in ActionRunner
136
+
137
+ A `form` action had no `case` in `ActionRunner`'s execution switch, so it fell
138
+ through to `executeActionSchema` and silently no-opped — clicking a Log-Time /
139
+ "open form" action did nothing. Add `executeForm`, which opens the FormView as a
140
+ routed page (`/forms/:name`, per the action spec) via the navigation handler,
141
+ forwarding the current record id as `?recordId=` for hosts that support it.
142
+ Covered by ActionRunner unit tests.
143
+
144
+ - 2d47e94: B2 follow-ups (A): field conditional rules in inline grids + submit-time enforcement.
145
+
146
+ - **Grids**: a line-item column's `readonlyWhen` / `requiredWhen` CEL rule is now honored per row — `deriveMasterDetail` carries the props onto the `GridColumn` and `GridField` evaluates them against each row via `resolveFieldRuleState` (a `readonlyWhen`-TRUE cell locks; a `requiredWhen`-TRUE empty cell flags inline-invalid). Rules are row-scoped (`record.*`); the core helpers gained an optional `scope` (and `GridField` a `contextRecord` prop) so a future header-driven lock can bind `parent.*` — that wiring is deferred (it needs the master-detail header's re-renders isolated).
147
+ - **Submit enforcement**: `requiredWhen` already drove react-hook-form's `required` rule, so submit is blocked with a field error when the predicate is TRUE and the value is empty. Added a reactive cleanup so a stale _required_ error clears when the predicate flips FALSE (and all errors clear when a field is hidden by `visibleWhen`).
148
+
149
+ - c3749eb: feat(dashboard): dataset chart widgets drill through to records
150
+
151
+ Dataset-bound **chart** widgets (bar/line/pie/area/donut/funnel/…) are now
152
+ click-drillable, matching table/pivot. Clicking a segment maps it back to its
153
+ dataset row and opens the same governed drill drawer (raw group keys preserved),
154
+ so a chart-only dashboard is no longer an exploration dead-end. This closes the
155
+ "object-backed chart drills but dataset chart doesn't" inconsistency and aligns
156
+ with mainstream BI (click a chart → see records).
157
+
158
+ - `@object-ui/core`: `findChartSeriesRow` — inverse of `buildChartSeries`,
159
+ maps a clicked `{category, series}` back to the source dataset row index
160
+ (matches both dims when a 2nd dimension is pivoted into series).
161
+ - `ObjectChart`: optional `onSegmentClick` lets a host own the chart click
162
+ (and suppress the widget's own object-drill).
163
+ - `DatasetWidget`: lifts the drill machinery to cover both table/pivot and
164
+ chart, and wires the chart's segment click to the precise dataset drill.
165
+
166
+ - 1394e34: feat(chart): visualise the second dataset dimension as grouped series
167
+
168
+ A dataset chart with two dimensions (e.g. `['status','priority']`) previously
169
+ only rendered the first dimension — the second was invisible (repeated x-axis
170
+ labels, no grouping). New shared `buildChartSeries` helper (`@object-ui/core`)
171
+ pivots the second dimension into one series per value; `ObjectChart`
172
+ (plugin-charts) and `DatasetWidget` (plugin-dashboard) both use it, so
173
+ multi-dimension charts render consistently as grouped/coloured bars.
174
+
175
+ Refs objectstack-ai/objectui#1759, objectstack-ai/framework#1890
176
+
177
+ - 7c239fd: Add `ComponentRegistry.unregister(type, namespace?)` — the inverse of
178
+ `register()`. Clears the namespaced key and the bare-name fallback (when it
179
+ still resolves to that registration) plus any matching lazy stub, and notifies
180
+ subscribers only when something was removed. Lets callers (and tests) restore
181
+ prior registry state cleanly.
182
+ - 8d1195d: Fix `type: 'url'` actions so they actually reach the backend in split-origin dev setups, and so reveal-once result dialogs render.
183
+
184
+ - `ActionRunner.executeUrl`: when context provides `apiBase`, relative `/api/...`, `/_auth/...`, and `/_account/...` URLs are now promoted to absolute (`${apiBase}${path}`) before navigation. Same-origin API paths (with or without `apiBase`) trigger a full-page `window.location.href` rather than React-Router push — this is required for server-side OAuth redirect dances (e.g. better-auth `/sign-in/social`) that React Router would otherwise swallow into the SPA's fallback route.
185
+ - `ActionRunner.buildInterpolationContext`: surfaces `ctx.apiBase` for action targets that want to template it explicitly.
186
+ - `ObjectView`: passes `apiBase: import.meta.env.VITE_SERVER_URL` into the toolbar `ActionProvider` context so the above resolves.
187
+ - `action-button` and `action-menu` renderers now forward `resultDialog` when invoking the runner. Previously this field was silently dropped by an explicit whitelist, breaking every "show once, then hide" flow (2FA QR/backup codes, OAuth client_secret, regenerated tokens).
188
+
189
+ - Updated dependencies [ddbe4a2]
190
+ - Updated dependencies [9049bbe]
191
+ - Updated dependencies [cb2fdb1]
192
+ - Updated dependencies [6cfa330]
193
+ - Updated dependencies [ad8ade6]
194
+ - Updated dependencies [3870c20]
195
+ - Updated dependencies [b88c560]
196
+ - Updated dependencies [d16566f]
197
+ - Updated dependencies [300d755]
198
+ - Updated dependencies [4eb9cb6]
199
+ - Updated dependencies [858ad94]
200
+ - @object-ui/types@7.0.0
201
+
202
+ ## 6.2.3
203
+
204
+ ### Patch Changes
205
+
206
+ - @object-ui/types@6.2.3
207
+
3
208
  ## 6.2.2
4
209
 
5
210
  ### Patch Changes
@@ -16,8 +16,16 @@
16
16
  * ActionSchema[] → ActionEngine → ActionRunner → ActionResult
17
17
  */
18
18
  import { ActionRunner, type ActionDef, type ActionContext, type ActionResult } from './ActionRunner';
19
- /** Action location types (from @objectstack/spec) */
20
- export type ActionLocation = 'list_toolbar' | 'list_item' | 'record_header' | 'record_more' | 'record_related' | 'global_nav';
19
+ /**
20
+ * Action location types — re-exported from `@objectstack/spec/ui` so the
21
+ * platform has one source of truth (`ACTION_LOCATIONS`). Do NOT redeclare
22
+ * this enum locally; add new values in
23
+ * `framework/packages/spec/src/ui/action.zod.ts` and they propagate here
24
+ * automatically.
25
+ */
26
+ export type { ActionLocation } from '@object-ui/types';
27
+ export { ACTION_LOCATIONS } from '@object-ui/types';
28
+ import type { ActionLocation } from '@object-ui/types';
21
29
  /** Registered action with metadata */
22
30
  export interface RegisteredAction {
23
31
  action: ActionDef;
@@ -51,7 +59,16 @@ export declare class ActionEngine {
51
59
  private shortcuts;
52
60
  private normalizedShortcutMap;
53
61
  private runner;
54
- constructor(context?: ActionContext);
62
+ /**
63
+ * @param contextOrRunner Either an initial `ActionContext` (constructs a
64
+ * new local `ActionRunner`) or an existing `ActionRunner` to share.
65
+ * Sharing is how nested consumers (e.g. `record:quick_actions` inside
66
+ * an `<ActionProvider>`) inherit the provider's confirm/param/modal/
67
+ * toast/navigate handlers without redeclaring them — without this,
68
+ * any action declaring `params: [...]` or `confirmText` would silently
69
+ * no-op in the nested engine.
70
+ */
71
+ constructor(contextOrRunner?: ActionContext | ActionRunner);
55
72
  /** Get the underlying ActionRunner for handler configuration */
56
73
  getRunner(): ActionRunner;
57
74
  /** Register a single action */
@@ -67,7 +84,28 @@ export declare class ActionEngine {
67
84
  unregisterAction(name: string): void;
68
85
  /** Add an event-to-action mapping */
69
86
  addMapping(mapping: ActionMapping): void;
70
- /** Get actions available at a specific location, sorted by priority */
87
+ /**
88
+ * Get actions available at a specific location, sorted by priority.
89
+ *
90
+ * Filtering applied (in order):
91
+ * 1. `locations.includes(location)` — location/region match
92
+ * 2. `action.visible` — evaluated against the runner's current context
93
+ * ({ record, recordId, objectName, user, … }). Missing or `true`
94
+ * passes; any other value is coerced to boolean. Evaluator errors
95
+ * hide the action (fail-closed) rather than throwing — this matches
96
+ * the contract used by every individual action renderer
97
+ * (`action-button`, `action-menu`, `action-bar`, …) so the same
98
+ * action behaves identically whether surfaced via the engine or
99
+ * consumed standalone.
100
+ *
101
+ * Predicate normalization matches `@object-ui/react`'s `toPredicateInput`:
102
+ * 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.
108
+ */
71
109
  getActionsForLocation(location: ActionLocation): ActionDef[];
72
110
  /** Get actions that support bulk operations */
73
111
  getBulkActions(): ActionDef[];
@@ -16,12 +16,43 @@
16
16
  * ActionSchema[] → ActionEngine → ActionRunner → ActionResult
17
17
  */
18
18
  import { ActionRunner } from './ActionRunner';
19
+ export { ACTION_LOCATIONS } from '@object-ui/types';
19
20
  /** Normalize a shortcut string for comparison (lowercase, trimmed, sorted parts) */
20
21
  function normalizeShortcut(keys) {
21
22
  return keys.toLowerCase().split('+').map(k => k.trim()).sort().join('+');
22
23
  }
24
+ /** De-dupe key set so a broken predicate warns once, not every re-render. */
25
+ const _warnedPredicates = new Set();
26
+ /**
27
+ * Surface an action whose `visible` predicate threw (and was therefore
28
+ * fail-closed hidden). Almost always an authoring bug — typically a bare field
29
+ * reference that should be `record.<field>`. Warned once per predicate.
30
+ */
31
+ function warnHiddenPredicate(name, raw, err) {
32
+ const src = typeof raw === 'string'
33
+ ? raw
34
+ : (raw && typeof raw === 'object' && typeof raw.source === 'string' ? raw.source : String(raw));
35
+ const key = `${String(name)}::${src}`;
36
+ if (_warnedPredicates.has(key))
37
+ return;
38
+ _warnedPredicates.add(key);
39
+ const msg = err instanceof Error ? err.message : String(err);
40
+ // eslint-disable-next-line no-console
41
+ console.warn(`[ActionEngine] action "${String(name)}" hidden: its \`visible\` predicate threw — ${msg}. ` +
42
+ `Predicate: ${src}. Action predicates evaluate against { record, recordId, objectName, user, … }; ` +
43
+ `use \`record.<field>\` (not a bare field).`);
44
+ }
23
45
  export class ActionEngine {
24
- constructor(context = {}) {
46
+ /**
47
+ * @param contextOrRunner Either an initial `ActionContext` (constructs a
48
+ * new local `ActionRunner`) or an existing `ActionRunner` to share.
49
+ * Sharing is how nested consumers (e.g. `record:quick_actions` inside
50
+ * an `<ActionProvider>`) inherit the provider's confirm/param/modal/
51
+ * toast/navigate handlers without redeclaring them — without this,
52
+ * any action declaring `params: [...]` or `confirmText` would silently
53
+ * no-op in the nested engine.
54
+ */
55
+ constructor(contextOrRunner = {}) {
25
56
  Object.defineProperty(this, "actions", {
26
57
  enumerable: true,
27
58
  configurable: true,
@@ -52,7 +83,10 @@ export class ActionEngine {
52
83
  writable: true,
53
84
  value: void 0
54
85
  });
55
- this.runner = new ActionRunner(context);
86
+ this.runner =
87
+ contextOrRunner instanceof ActionRunner
88
+ ? contextOrRunner
89
+ : new ActionRunner(contextOrRunner);
56
90
  }
57
91
  /** Get the underlying ActionRunner for handler configuration */
58
92
  getRunner() {
@@ -100,10 +134,72 @@ export class ActionEngine {
100
134
  addMapping(mapping) {
101
135
  this.mappings.push(mapping);
102
136
  }
103
- /** Get actions available at a specific location, sorted by priority */
137
+ /**
138
+ * Get actions available at a specific location, sorted by priority.
139
+ *
140
+ * Filtering applied (in order):
141
+ * 1. `locations.includes(location)` — location/region match
142
+ * 2. `action.visible` — evaluated against the runner's current context
143
+ * ({ record, recordId, objectName, user, … }). Missing or `true`
144
+ * passes; any other value is coerced to boolean. Evaluator errors
145
+ * hide the action (fail-closed) rather than throwing — this matches
146
+ * the contract used by every individual action renderer
147
+ * (`action-button`, `action-menu`, `action-bar`, …) so the same
148
+ * action behaves identically whether surfaced via the engine or
149
+ * consumed standalone.
150
+ *
151
+ * Predicate normalization matches `@object-ui/react`'s `toPredicateInput`:
152
+ * 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.
158
+ */
104
159
  getActionsForLocation(location) {
160
+ const evaluator = this.runner.getEvaluator();
105
161
  return Array.from(this.actions.values())
106
162
  .filter(ra => ra.locations.includes(location))
163
+ .filter(ra => {
164
+ const raw = ra.action.visible;
165
+ if (raw == null || raw === '' || raw === true)
166
+ return true;
167
+ if (raw === false)
168
+ return false;
169
+ let expr;
170
+ if (typeof raw === 'string') {
171
+ expr = `\${${raw}}`;
172
+ }
173
+ else if (typeof raw === 'object' && typeof raw.source === 'string') {
174
+ const src = raw.source;
175
+ if (!src)
176
+ return true;
177
+ expr = `\${${src}}`;
178
+ }
179
+ else {
180
+ expr = Boolean(raw);
181
+ }
182
+ try {
183
+ // `throwOnError: true` is required for fail-closed semantics: the
184
+ // evaluator's default behavior swallows expression errors and
185
+ // returns the raw template string, which `Boolean(string)` then
186
+ // coerces to `true` — silently leaking actions whose preconditions
187
+ // failed (e.g. `ctx` undefined). Surface the error so our catch
188
+ // below can hide the action.
189
+ return evaluator.evaluateCondition(expr, { throwOnError: true });
190
+ }
191
+ catch (err) {
192
+ // Fail-closed: hide the action. But a *thrown* predicate is almost
193
+ // always an authoring bug, not a real precondition — most often a
194
+ // BARE field reference (`done` instead of `record.done`), which is
195
+ // an undeclared variable in the `{ record, recordId, objectName,
196
+ // user }` eval scope. Silently hiding it makes that bug invisible
197
+ // (the #2183 hunt). Warn once per predicate so it is diagnosable
198
+ // without spamming re-renders.
199
+ warnHiddenPredicate(ra.action.name, raw, err);
200
+ return false;
201
+ }
202
+ })
107
203
  .sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100))
108
204
  .map(ra => ra.action);
109
205
  }
@@ -14,6 +14,7 @@
14
14
  * redirect handling, action chaining, custom handler registration.
15
15
  */
16
16
  import { ExpressionEvaluator } from '../evaluator/ExpressionEvaluator';
17
+ import { type UndoableOperation } from './UndoManager';
17
18
  export interface ActionResult {
18
19
  success: boolean;
19
20
  data?: any;
@@ -23,6 +24,20 @@ export interface ActionResult {
23
24
  redirect?: string;
24
25
  /** Modal schema to render (for type: 'modal') */
25
26
  modal?: any;
27
+ /**
28
+ * Suppress the automatic success toast for this result. A handler sets this
29
+ * when the action only HANDED OFF to a follow-up UI rather than completing —
30
+ * e.g. a `flow` action that paused at a screen and opened the flow-runner. The
31
+ * action hasn't "completed yet", so a "success" toast on open would be
32
+ * misleading; the follow-up surface owns its own completion messaging.
33
+ */
34
+ silent?: boolean;
35
+ /**
36
+ * An undoable operation captured by the handler (e.g. an `undoable` update
37
+ * action's prior field values). When present, the runner pushes it onto the
38
+ * global UndoManager and the success toast offers an "Undo" affordance.
39
+ */
40
+ undo?: UndoableOperation;
26
41
  }
27
42
  export interface ActionContext {
28
43
  data?: Record<string, any>;
@@ -102,10 +117,18 @@ export interface ActionDef {
102
117
  errorMessage?: string;
103
118
  /** Whether to refresh data after execution (from UIActionSchema) */
104
119
  refreshAfter?: boolean;
120
+ /** Single-record update actions: offer an Undo affordance on success. */
121
+ undoable?: boolean;
105
122
  /** Params object (for custom handlers) */
106
123
  params?: Record<string, any>;
107
124
  /** ActionParam definitions to collect from user before execution (from spec ActionSchema.params) */
108
125
  actionParams?: ActionParamDef[];
126
+ /**
127
+ * Result dialog spec — when present and the action succeeds, the runner
128
+ * suppresses the successMessage toast and asks the consumer to open a
129
+ * reveal dialog rendering `result.data` (see ResultDialogSpec).
130
+ */
131
+ resultDialog?: ResultDialogSpec;
109
132
  /** Script/expression to execute (for type: 'script') */
110
133
  execute?: string;
111
134
  /** Target URL or identifier (for type: 'url', 'modal', 'flow') */
@@ -124,6 +147,12 @@ export interface ActionDef {
124
147
  * handler can drive it to a returned `redirectUrl` after an awaited fetch
125
148
  * without tripping popup blockers. Used by actions like `sso_as_owner`. */
126
149
  opensInNewTab?: boolean;
150
+ /** Zero-roundtrip new-tab target: a path template (`{recordId}` placeholder)
151
+ * the handler navigates the pre-opened tab to IMMEDIATELY on click, skipping
152
+ * the action POST entirely. Only valid with `opensInNewTab`; the endpoint
153
+ * must perform all auth/authz itself (e.g. the cloud `/sso-open` endpoint,
154
+ * which re-runs every check the POST half would have done). */
155
+ newTabUrl?: string;
127
156
  /** Any additional properties */
128
157
  [key: string]: any;
129
158
  }
@@ -143,6 +172,11 @@ export type ConfirmationHandler = (message: string, options?: {
143
172
  export type ToastHandler = (message: string, options?: {
144
173
  type?: 'success' | 'error' | 'info' | 'warning';
145
174
  duration?: number;
175
+ /** When set, the toast offers an "Undo" affordance (the runner has already
176
+ * pushed the operation onto the global UndoManager). */
177
+ undo?: {
178
+ label?: string;
179
+ };
146
180
  }) => void;
147
181
  /**
148
182
  * Modal handler — consumers provide to render modal dialogs.
@@ -162,6 +196,35 @@ export type NavigationHandler = (url: string, options?: {
162
196
  * Returns collected values, or null if cancelled.
163
197
  */
164
198
  export type ParamCollectionHandler = (params: ActionParamDef[], action?: ActionDef) => Promise<Record<string, any> | null>;
199
+ /**
200
+ * Result dialog spec — declarative description of how to render a
201
+ * one-shot reveal of an action's API response. Mirrors
202
+ * `Action.resultDialog` in @objectstack/spec.
203
+ *
204
+ * When set on an action and the action succeeds, the runner suppresses
205
+ * the success toast and awaits a ResultDialogHandler instead. Used for
206
+ * values the user must copy NOW (2FA secret, OAuth client_secret, backup
207
+ * codes).
208
+ */
209
+ export interface ResultDialogFieldSpec {
210
+ path: string;
211
+ label?: string;
212
+ format?: 'qrcode' | 'code-list' | 'secret' | 'text' | 'json';
213
+ }
214
+ export interface ResultDialogSpec {
215
+ title?: string;
216
+ description?: string;
217
+ acknowledge?: string;
218
+ format?: 'qrcode' | 'code-list' | 'secret' | 'text' | 'json';
219
+ fields?: ResultDialogFieldSpec[];
220
+ }
221
+ /**
222
+ * Result dialog handler — consumers provide to render the post-success
223
+ * reveal dialog. Returns a promise that resolves when the user
224
+ * acknowledges. Errors / rejections are treated as acknowledge for
225
+ * cleanup purposes (the action already succeeded).
226
+ */
227
+ export type ResultDialogHandler = (spec: ResultDialogSpec, data: unknown, action?: ActionDef) => Promise<void>;
165
228
  /**
166
229
  * ActionParam definition accepted by the runner.
167
230
  * Compatible with @objectstack/spec ActionParam.
@@ -210,6 +273,7 @@ export declare class ActionRunner {
210
273
  private modalHandler;
211
274
  private navigationHandler;
212
275
  private paramCollectionHandler;
276
+ private resultDialogHandler;
213
277
  constructor(context?: ActionContext);
214
278
  /**
215
279
  * Set a custom confirmation handler (e.g., Shadcn AlertDialog).
@@ -232,6 +296,12 @@ export declare class ActionRunner {
232
296
  * ActionParam values before action execution.
233
297
  */
234
298
  setParamCollectionHandler(handler: ParamCollectionHandler): void;
299
+ /**
300
+ * Set a result dialog handler — shows a one-shot reveal dialog after
301
+ * a successful action whose spec declares `resultDialog`. Used for
302
+ * values the user must copy now (2FA codes, OAuth client_secret).
303
+ */
304
+ setResultDialogHandler(handler: ResultDialogHandler): void;
235
305
  registerHandler(actionName: string, handler: ActionHandler): void;
236
306
  unregisterHandler(actionName: string): void;
237
307
  /**
@@ -275,6 +345,14 @@ export declare class ActionRunner {
275
345
  /**
276
346
  * Execute navigation action
277
347
  */
348
+ /**
349
+ * `form` action — open a FormView as a routed page (`/forms/:name`, per the
350
+ * spec). `target` is the FormView name. Without this case the action fell
351
+ * through to `executeActionSchema` and silently no-opped (the "Log Time does
352
+ * nothing" report). The current record id is forwarded as `?recordId=` for
353
+ * hosts that support it; the form route ignores unknown query params.
354
+ */
355
+ private executeForm;
278
356
  private executeNavigation;
279
357
  /**
280
358
  * Execute API action — supports both simple string endpoint and complex ApiConfig.
@@ -284,6 +362,34 @@ export declare class ActionRunner {
284
362
  * Validate URL to prevent javascript: or data: protocol injection.
285
363
  */
286
364
  private isValidUrl;
365
+ /**
366
+ * Substitute `${param.X}` and `${ctx.X}` tokens in an action target.
367
+ *
368
+ * Used by executeUrl and executeAPI so metadata can produce dynamic
369
+ * URLs without coupling to the wider ExpressionEvaluator surface
370
+ * (which would also evaluate operators, member access, etc. — overkill
371
+ * and unsafe for URL-position values).
372
+ *
373
+ * Every substituted value is `encodeURIComponent`'d, since the only
374
+ * legitimate use case so far (better-auth `/sign-in/social?provider=…`)
375
+ * places interpolated values in URL query positions. Callers MUST
376
+ * therefore put tokens after a `?` or `&` — using `${param.x}` inside a
377
+ * path segment will produce percent-encoded slashes, which is the
378
+ * correct behaviour for opaque IDs but would break a literal path
379
+ * fragment.
380
+ *
381
+ * `ctx.origin`, `ctx.user.*`, `ctx.org.*`, `ctx.recordId` are surfaced
382
+ * implicitly from the runner's ActionContext + window. Consumers can
383
+ * extend by stuffing extra keys under `context.ctx = {...}` before
384
+ * calling `runner.execute()`.
385
+ */
386
+ private interpolateTarget;
387
+ /**
388
+ * Assemble the `ctx` namespace exposed to target interpolation.
389
+ * Built-in keys: origin, user, org, recordId. Anything the consumer
390
+ * placed under context.ctx is merged on top.
391
+ */
392
+ private buildInterpolationContext;
287
393
  updateContext(newContext: Partial<ActionContext>): void;
288
394
  getContext(): ActionContext;
289
395
  /**