@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 +205 -0
- package/dist/actions/ActionEngine.d.ts +42 -4
- package/dist/actions/ActionEngine.js +99 -3
- package/dist/actions/ActionRunner.d.ts +106 -0
- package/dist/actions/ActionRunner.js +213 -10
- package/dist/evaluator/ExpressionEvaluator.js +26 -4
- package/dist/evaluator/fieldRules.d.ts +49 -0
- package/dist/evaluator/fieldRules.js +86 -0
- package/dist/evaluator/index.d.ts +1 -0
- package/dist/evaluator/index.js +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/registry/Registry.d.ts +11 -0
- package/dist/registry/Registry.js +36 -0
- package/dist/styling/scoped-styles.d.ts +41 -0
- package/dist/styling/scoped-styles.js +68 -0
- package/dist/utils/chart-series.d.ts +52 -0
- package/dist/utils/chart-series.js +72 -0
- package/dist/utils/dataset-format.d.ts +85 -0
- package/dist/utils/dataset-format.js +117 -0
- package/dist/validation/validators/object-validation-engine.js +20 -21
- package/package.json +5 -4
- package/dist/utils/normalize-quick-filter.d.ts +0 -29
- package/dist/utils/normalize-quick-filter.js +0 -66
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
|
-
/**
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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 =
|
|
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
|
-
/**
|
|
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
|
/**
|