@object-ui/core 17.2.0 → 17.5.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 (79) hide show
  1. package/CHANGELOG.md +1436 -0
  2. package/README.md +31 -12
  3. package/dist/actions/ActionEngine.d.ts +53 -31
  4. package/dist/actions/ActionEngine.js +87 -66
  5. package/dist/actions/ActionRunner.d.ts +215 -24
  6. package/dist/actions/ActionRunner.js +334 -17
  7. package/dist/actions/actionErrorDetail.d.ts +26 -0
  8. package/dist/actions/actionErrorDetail.js +37 -0
  9. package/dist/actions/actionKeys.d.ts +96 -20
  10. package/dist/actions/actionKeys.js +168 -18
  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/data-scope/ViewDataProvider.d.ts +37 -12
  18. package/dist/data-scope/ViewDataProvider.js +83 -5
  19. package/dist/data-scope/element-data-source.d.ts +113 -0
  20. package/dist/data-scope/element-data-source.js +216 -0
  21. package/dist/data-scope/index.d.ts +2 -1
  22. package/dist/data-scope/index.js +1 -0
  23. package/dist/evaluator/ExpressionEvaluator.js +4 -2
  24. package/dist/evaluator/declaredPredicate.d.ts +119 -0
  25. package/dist/evaluator/declaredPredicate.js +152 -0
  26. package/dist/evaluator/fieldRules.d.ts +42 -2
  27. package/dist/evaluator/fieldRules.js +101 -11
  28. package/dist/evaluator/index.d.ts +2 -0
  29. package/dist/evaluator/index.js +2 -0
  30. package/dist/evaluator/listConditional.d.ts +18 -3
  31. package/dist/evaluator/listConditional.js +0 -0
  32. package/dist/evaluator/optionRules.js +5 -1
  33. package/dist/evaluator/predicateInput.d.ts +132 -0
  34. package/dist/evaluator/predicateInput.js +129 -0
  35. package/dist/index.d.ts +7 -1
  36. package/dist/index.js +26 -1
  37. package/dist/protocols/DndProtocol.d.ts +42 -1
  38. package/dist/protocols/KeyboardProtocol.d.ts +31 -1
  39. package/dist/protocols/SharingProtocol.d.ts +11 -1
  40. package/dist/protocols/index.d.ts +0 -1
  41. package/dist/protocols/index.js +8 -1
  42. package/dist/registry/Registry.d.ts +24 -0
  43. package/dist/theme/ThemeEngine.d.ts +0 -8
  44. package/dist/theme/ThemeEngine.js +22 -103
  45. package/dist/theme/index.d.ts +1 -1
  46. package/dist/theme/index.js +7 -1
  47. package/dist/types/index.d.ts +42 -8
  48. package/dist/utils/chart-series.d.ts +258 -5
  49. package/dist/utils/chart-series.js +423 -20
  50. package/dist/utils/compare-to.d.ts +40 -13
  51. package/dist/utils/compare-to.js +4 -32
  52. package/dist/utils/dashboard-filters.d.ts +54 -8
  53. package/dist/utils/dashboard-filters.js +261 -12
  54. package/dist/utils/dataset-format.d.ts +79 -15
  55. package/dist/utils/dataset-format.js +130 -9
  56. package/dist/utils/dataset-pivot.d.ts +78 -0
  57. package/dist/utils/dataset-pivot.js +78 -0
  58. package/dist/utils/dom-props.d.ts +137 -0
  59. package/dist/utils/dom-props.js +153 -0
  60. package/dist/utils/filter-converter.d.ts +39 -11
  61. package/dist/utils/filter-converter.js +87 -7
  62. package/dist/utils/normalize-list-view.d.ts +26 -6
  63. package/dist/utils/normalize-list-view.js +30 -8
  64. package/dist/utils/number-display.d.ts +151 -0
  65. package/dist/utils/number-display.js +71 -0
  66. package/dist/utils/predicate-fields.d.ts +96 -0
  67. package/dist/utils/predicate-fields.js +163 -0
  68. package/dist/utils/predicate-record.d.ts +32 -0
  69. package/dist/utils/predicate-record.js +120 -0
  70. package/dist/utils/record-title.js +0 -0
  71. package/dist/utils/resolve-view-id.d.ts +31 -0
  72. package/dist/utils/resolve-view-id.js +45 -0
  73. package/dist/utils/sort-query.d.ts +55 -0
  74. package/dist/utils/sort-query.js +35 -0
  75. package/package.json +5 -5
  76. package/dist/protocols/NotificationProtocol.d.ts +0 -71
  77. package/dist/protocols/NotificationProtocol.js +0 -89
  78. package/dist/utils/merge-views-into-objects.d.ts +0 -19
  79. package/dist/utils/merge-views-into-objects.js +0 -39
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
@@ -112,21 +141,11 @@ This allows the core types and logic to be used in:
112
141
 
113
142
  ## API Reference
114
143
 
115
- See [full documentation](https://objectui.org/api/core) for detailed API reference.
116
-
117
- <!-- release-metadata:v3.3.0 -->
118
-
119
- ## Compatibility
120
-
121
- - **Node.js:** ≥ 18
122
- - **TypeScript:** ≥ 5.0 (strict mode)
123
- - **`@objectstack/spec`:** ^3.3.0
124
- - **`@objectstack/client`:** ^3.3.0
125
- - **Tailwind CSS:** ≥ 3.4 (for packages with UI)
144
+ See [full documentation](https://objectui.org/docs/api) for detailed API reference.
126
145
 
127
146
  ## Links
128
147
 
129
- - 📚 [Documentation](https://www.objectui.org/docs/core)
148
+ - 📚 [Documentation](https://www.objectui.org/docs/guide/architecture)
130
149
  - 📦 [npm package](https://www.npmjs.com/package/@object-ui/core)
131
150
  - 📝 [Changelog](./CHANGELOG.md)
132
151
  - 🐛 [Report an issue](https://github.com/objectstack-ai/objectui/issues)
@@ -8,9 +8,8 @@
8
8
  /**
9
9
  * @object-ui/core - Action Engine
10
10
  *
11
- * Declarative action dispatch engine. Manages action registration,
12
- * event-to-action mapping, location-based filtering, keyboard shortcuts,
13
- * and bulk operation support.
11
+ * Declarative action engine. Manages action registration, location-based
12
+ * filtering, keyboard shortcuts, and bulk operation support.
14
13
  *
15
14
  * Replaces callback-based patterns with a schema-driven pipeline:
16
15
  * ActionSchema[] → ActionEngine → ActionRunner → ActionResult
@@ -37,15 +36,6 @@ export interface RegisteredAction {
37
36
  /** Priority for ordering (lower = first) */
38
37
  priority?: number;
39
38
  }
40
- /** Event-to-action mapping */
41
- export interface ActionMapping {
42
- /** Event name (e.g., 'row:click', 'toolbar:save', 'keyboard:ctrl+s') */
43
- event: string;
44
- /** Action name to execute */
45
- actionName: string;
46
- /** Optional condition expression */
47
- condition?: string;
48
- }
49
39
  /** Keyboard shortcut handler */
50
40
  export interface ShortcutBinding {
51
41
  /** Key combination (e.g., 'ctrl+s', 'meta+k', 'shift+n') */
@@ -53,9 +43,35 @@ export interface ShortcutBinding {
53
43
  /** Action name to trigger */
54
44
  actionName: string;
55
45
  }
46
+ /**
47
+ * Registration + filtering + execution for declared actions.
48
+ *
49
+ * Entry points are by NAME (`executeAction`), by LOCATION
50
+ * (`getActionsForLocation`), by SHORTCUT (`handleShortcut`) and in BULK
51
+ * (`executeBulk`). There is deliberately no entry point by arbitrary EVENT
52
+ * name: this class used to carry a second, parallel one — `addMapping()`
53
+ * registered `{ event, actionName, condition }` triples into a private
54
+ * `mappings` array and `dispatch(event)` ran every triple matching an event
55
+ * string. It was retired under enforce-or-remove (objectui#3368) because it
56
+ * was a public export of `@object-ui/core` with zero production callers: in
57
+ * its whole lifetime nothing ever registered a mapping, so `dispatch()` had
58
+ * no reachable caller either, and its four call sites were all in this
59
+ * class's own test file.
60
+ *
61
+ * Its condition gate had drifted three ways from the `visible` contract
62
+ * `getActionsForLocation` below implements — it entered on a raw truthy check
63
+ * (so `condition: false` dispatched anyway), typed `condition` as `string`
64
+ * only (so a `{ dialect: 'cel', source }` envelope could not reach the
65
+ * canonical engine), and evaluated without `throwOnError` (so a throwing
66
+ * predicate failed OPEN, the opposite of `visible`'s fail-closed posture).
67
+ * All three were retired WITH the path rather than fixed on it: aligning the
68
+ * contract of an API nobody calls only widens behaviour nobody uses. If an
69
+ * event-keyed entry point is ever genuinely needed, it must be built on the
70
+ * shared predicate definitions (`hasDeclaredPredicate` + `toPredicateInput`)
71
+ * that the surviving gate below already uses, not on a fourth spelling.
72
+ */
56
73
  export declare class ActionEngine {
57
74
  private actions;
58
- private mappings;
59
75
  private shortcuts;
60
76
  private normalizedShortcutMap;
61
77
  private runner;
@@ -92,29 +108,37 @@ export declare class ActionEngine {
92
108
  registerActions(actions: ActionDef[]): void;
93
109
  /** Unregister an action by name */
94
110
  unregisterAction(name: string): void;
95
- /** Add an event-to-action mapping */
96
- addMapping(mapping: ActionMapping): void;
97
111
  /**
98
112
  * Get actions available at a specific location, sorted by priority.
99
113
  *
100
114
  * Filtering applied (in order):
101
115
  * 1. `locations.includes(location)` — location/region match
102
116
  * 2. `action.visible` — evaluated against the runner's current context
103
- * ({ record, recordId, objectName, user, … }). Missing or `true`
104
- * passes; any other value is coerced to boolean. Evaluator errors
105
- * hide the action (fail-closed) rather than throwing — this matches
106
- * the contract used by every individual action renderer
107
- * (`action-button`, `action-menu`, `action-bar`, …) so the same
108
- * action behaves identically whether surfaced via the engine or
109
- * consumed standalone.
117
+ * ({ record, recordId, objectName, user, … }) whenever a gate is
118
+ * DECLARED, which is asked through core's one definition
119
+ * `hasDeclaredPredicate` (objectui#3850's ruling, adopted here by
120
+ * objectui#3957). No gate — absent, `''`, blank predicate text in either
121
+ * spelling, an envelope with no evaluable `source`, or a value that is
122
+ * not a predicate at all — means visible. Evaluator errors hide the
123
+ * action (fail-closed) rather than throwing — this matches the contract
124
+ * used by every individual action renderer (`action-button`,
125
+ * `action-menu`, `action-bar`, …) so the same action behaves identically
126
+ * whether surfaced via the engine or consumed standalone, which is now
127
+ * true of the gate in front of the verdict as well as the verdict
128
+ * (objectui#3314: one value, one answer, whichever entry reads it).
110
129
  *
111
- * Predicate normalization matches `@object-ui/react`'s `toPredicateInput`:
130
+ * Predicate normalization goes through the shared `toPredicateInput`
131
+ * (`@object-ui/core`'s canonical helper — semantically identical to
132
+ * `@object-ui/react`'s renderer-side twin, pinned by a parity test):
112
133
  * raw strings (`'record.foo == true'`) are wrapped as `${...}` so they
113
- * evaluate as expressions, not as truthy string literals. Spec-style
114
- * `{ dialect, source }` envelopes are unwrapped to their `source`.
115
- * Without this, every per-action renderer (which already calls
116
- * `toPredicateInput`) and the engine path would disagree on the same
117
- * `visible:` predicate.
134
+ * evaluate as expressions, not as truthy string literals, and a spec-style
135
+ * `{ dialect: 'cel', source }` envelope is passed through INTACT so
136
+ * `evaluateCondition` routes it to the canonical `@objectstack/formula`
137
+ * engine (#2661). Unwrapping the envelope here — as this method used to —
138
+ * dropped the dialect and silently demoted every CEL predicate to the
139
+ * legacy JS evaluator, so the same `visible:` reached a different verdict
140
+ * via the engine than via a renderer (#3314): CEL faults on `null < null`
141
+ * and fail-closed hides, JS quietly returns `false`.
118
142
  */
119
143
  getActionsForLocation(location: ActionLocation): ActionDef[];
120
144
  /** Get actions that support bulk operations */
@@ -125,8 +149,6 @@ export declare class ActionEngine {
125
149
  getAction(name: string): ActionDef | undefined;
126
150
  /** Execute an action by name */
127
151
  executeAction(name: string, contextOverride?: Partial<ActionContext>): Promise<ActionResult>;
128
- /** Dispatch an event — finds mapped actions and executes them */
129
- dispatch(event: string, contextOverride?: Partial<ActionContext>): Promise<ActionResult[]>;
130
152
  /** Handle a keyboard shortcut event — returns true if handled */
131
153
  handleShortcut(keys: string, contextOverride?: Partial<ActionContext>): Promise<ActionResult | null>;
132
154
  /** Execute a bulk operation on multiple records */
@@ -141,6 +163,6 @@ export declare class ActionEngine {
141
163
  }>;
142
164
  /** Update the action context */
143
165
  updateContext(context: Partial<ActionContext>): void;
144
- /** Clear all registered actions and mappings */
166
+ /** Clear all registered actions and shortcuts */
145
167
  clear(): void;
146
168
  }
@@ -8,14 +8,15 @@
8
8
  /**
9
9
  * @object-ui/core - Action Engine
10
10
  *
11
- * Declarative action dispatch engine. Manages action registration,
12
- * event-to-action mapping, location-based filtering, keyboard shortcuts,
13
- * and bulk operation support.
11
+ * Declarative action engine. Manages action registration, location-based
12
+ * filtering, keyboard shortcuts, and bulk operation support.
14
13
  *
15
14
  * Replaces callback-based patterns with a schema-driven pipeline:
16
15
  * ActionSchema[] → ActionEngine → ActionRunner → ActionResult
17
16
  */
18
17
  import { ActionRunner } from './ActionRunner';
18
+ import { toPredicateInput } from '../evaluator/predicateInput.js';
19
+ import { hasDeclaredPredicate } from '../evaluator/declaredPredicate.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) {
@@ -42,6 +43,33 @@ function warnHiddenPredicate(name, raw, err) {
42
43
  `Predicate: ${src}. Action predicates evaluate against { record, recordId, objectName, user, os.user, … }; ` +
43
44
  `use \`record.<field>\` (not a bare field).`);
44
45
  }
46
+ /**
47
+ * Registration + filtering + execution for declared actions.
48
+ *
49
+ * Entry points are by NAME (`executeAction`), by LOCATION
50
+ * (`getActionsForLocation`), by SHORTCUT (`handleShortcut`) and in BULK
51
+ * (`executeBulk`). There is deliberately no entry point by arbitrary EVENT
52
+ * name: this class used to carry a second, parallel one — `addMapping()`
53
+ * registered `{ event, actionName, condition }` triples into a private
54
+ * `mappings` array and `dispatch(event)` ran every triple matching an event
55
+ * string. It was retired under enforce-or-remove (objectui#3368) because it
56
+ * was a public export of `@object-ui/core` with zero production callers: in
57
+ * its whole lifetime nothing ever registered a mapping, so `dispatch()` had
58
+ * no reachable caller either, and its four call sites were all in this
59
+ * class's own test file.
60
+ *
61
+ * Its condition gate had drifted three ways from the `visible` contract
62
+ * `getActionsForLocation` below implements — it entered on a raw truthy check
63
+ * (so `condition: false` dispatched anyway), typed `condition` as `string`
64
+ * only (so a `{ dialect: 'cel', source }` envelope could not reach the
65
+ * canonical engine), and evaluated without `throwOnError` (so a throwing
66
+ * predicate failed OPEN, the opposite of `visible`'s fail-closed posture).
67
+ * All three were retired WITH the path rather than fixed on it: aligning the
68
+ * contract of an API nobody calls only widens behaviour nobody uses. If an
69
+ * event-keyed entry point is ever genuinely needed, it must be built on the
70
+ * shared predicate definitions (`hasDeclaredPredicate` + `toPredicateInput`)
71
+ * that the surviving gate below already uses, not on a fourth spelling.
72
+ */
45
73
  export class ActionEngine {
46
74
  /**
47
75
  * @param contextOrRunner Either an initial `ActionContext` (constructs a
@@ -59,12 +87,6 @@ export class ActionEngine {
59
87
  writable: true,
60
88
  value: new Map()
61
89
  });
62
- Object.defineProperty(this, "mappings", {
63
- enumerable: true,
64
- configurable: true,
65
- writable: true,
66
- value: []
67
- });
68
90
  Object.defineProperty(this, "shortcuts", {
69
91
  enumerable: true,
70
92
  configurable: true,
@@ -139,11 +161,6 @@ export class ActionEngine {
139
161
  }
140
162
  this.actions.delete(name);
141
163
  this.shortcuts = this.shortcuts.filter(s => s.actionName !== name);
142
- this.mappings = this.mappings.filter(m => m.actionName !== name);
143
- }
144
- /** Add an event-to-action mapping */
145
- addMapping(mapping) {
146
- this.mappings.push(mapping);
147
164
  }
148
165
  /**
149
166
  * Get actions available at a specific location, sorted by priority.
@@ -151,21 +168,31 @@ export class ActionEngine {
151
168
  * Filtering applied (in order):
152
169
  * 1. `locations.includes(location)` — location/region match
153
170
  * 2. `action.visible` — evaluated against the runner's current context
154
- * ({ record, recordId, objectName, user, … }). Missing or `true`
155
- * passes; any other value is coerced to boolean. Evaluator errors
156
- * hide the action (fail-closed) rather than throwing — this matches
157
- * the contract used by every individual action renderer
158
- * (`action-button`, `action-menu`, `action-bar`, …) so the same
159
- * action behaves identically whether surfaced via the engine or
160
- * consumed standalone.
171
+ * ({ record, recordId, objectName, user, … }) whenever a gate is
172
+ * DECLARED, which is asked through core's one definition
173
+ * `hasDeclaredPredicate` (objectui#3850's ruling, adopted here by
174
+ * objectui#3957). No gate — absent, `''`, blank predicate text in either
175
+ * spelling, an envelope with no evaluable `source`, or a value that is
176
+ * not a predicate at all — means visible. Evaluator errors hide the
177
+ * action (fail-closed) rather than throwing — this matches the contract
178
+ * used by every individual action renderer (`action-button`,
179
+ * `action-menu`, `action-bar`, …) so the same action behaves identically
180
+ * whether surfaced via the engine or consumed standalone, which is now
181
+ * true of the gate in front of the verdict as well as the verdict
182
+ * (objectui#3314: one value, one answer, whichever entry reads it).
161
183
  *
162
- * Predicate normalization matches `@object-ui/react`'s `toPredicateInput`:
184
+ * Predicate normalization goes through the shared `toPredicateInput`
185
+ * (`@object-ui/core`'s canonical helper — semantically identical to
186
+ * `@object-ui/react`'s renderer-side twin, pinned by a parity test):
163
187
  * raw strings (`'record.foo == true'`) are wrapped as `${...}` so they
164
- * evaluate as expressions, not as truthy string literals. Spec-style
165
- * `{ dialect, source }` envelopes are unwrapped to their `source`.
166
- * Without this, every per-action renderer (which already calls
167
- * `toPredicateInput`) and the engine path would disagree on the same
168
- * `visible:` predicate.
188
+ * evaluate as expressions, not as truthy string literals, and a spec-style
189
+ * `{ dialect: 'cel', source }` envelope is passed through INTACT so
190
+ * `evaluateCondition` routes it to the canonical `@objectstack/formula`
191
+ * engine (#2661). Unwrapping the envelope here — as this method used to —
192
+ * dropped the dialect and silently demoted every CEL predicate to the
193
+ * legacy JS evaluator, so the same `visible:` reached a different verdict
194
+ * via the engine than via a renderer (#3314): CEL faults on `null < null`
195
+ * and fail-closed hides, JS quietly returns `false`.
169
196
  */
170
197
  getActionsForLocation(location) {
171
198
  const evaluator = this.runner.getEvaluator();
@@ -188,30 +215,45 @@ export class ActionEngine {
188
215
  })
189
216
  .filter(ra => {
190
217
  const raw = ra.action.visible;
191
- if (raw == null || raw === '' || raw === true)
218
+ // Ask "is a `visible` gate DECLARED?" through the repo's ONE definition
219
+ // (`evaluator/declaredPredicate.ts`, objectui#3850's ruling), which is
220
+ // what the renderers, `SchemaRenderer` and `ActionRunner`'s execution
221
+ // gates ask. This filter used to answer it with a range of its own —
222
+ // three empty spellings folded by hand (`null` / `''` / an envelope with
223
+ // an empty `source`) and everything else coerced with `Boolean(raw)` —
224
+ // and it was the LAST consumer to do so, so the same value got two
225
+ // answers depending on which entry read it: `visible: 0` / `NaN` hid the
226
+ // action here and showed it on the renderer face, and `visible: ' '`
227
+ // hid it here because the normalizer wraps a blank string into `'${ }'`
228
+ // whose verdict is falsy. That is the shape objectui#3314's invariant
229
+ // forbids, and it is fixed by deleting a range, not by adding one
230
+ // (objectui#3957). A value that is not a predicate, or a predicate that
231
+ // says nothing, must not be the reason an action is hidden from
232
+ // everyone — the same fail-open posture `ActionRunner` already committed
233
+ // to for `disabled` (`catch { isDisabled = false }`).
234
+ //
235
+ // Booleans need no branch of their own: they are DECLARED (`visible:
236
+ // false` is a verdict, objectui#3812) and `evaluateCondition`
237
+ // short-circuits them.
238
+ if (!hasDeclaredPredicate(raw))
192
239
  return true;
193
- if (raw === false)
194
- return false;
195
- let expr;
196
- if (typeof raw === 'string') {
197
- expr = `\${${raw}}`;
198
- }
199
- else if (typeof raw === 'object' && typeof raw.source === 'string') {
200
- const src = raw.source;
201
- if (!src)
202
- return true;
203
- expr = `\${${src}}`;
204
- }
205
- else {
206
- expr = Boolean(raw);
207
- }
240
+ // #3314 — one shared normalization, not two. Hand-rolling the envelope
241
+ // unwrap here is what dropped `dialect: 'cel'` and demoted the
242
+ // predicate to the legacy JS engine; `toPredicateInput` keeps the
243
+ // envelope so `evaluateCondition` can route it to `@objectstack/formula`
244
+ // (the engine the server enforces with), exactly as the renderers do.
245
+ // Declared ⇒ the normalizer left something to evaluate, so there is no
246
+ // `undefined` case left to handle here.
247
+ const expr = toPredicateInput(raw);
208
248
  try {
209
249
  // `throwOnError: true` is required for fail-closed semantics: the
210
250
  // evaluator's default behavior swallows expression errors and
211
251
  // returns the raw template string, which `Boolean(string)` then
212
252
  // coerces to `true` — silently leaking actions whose preconditions
213
253
  // failed (e.g. `ctx` undefined). Surface the error so our catch
214
- // below can hide the action.
254
+ // below can hide the action. It is equally load-bearing on the CEL
255
+ // path, where the canonical helper fails SOFT to visible/enabled
256
+ // unless the caller opts in.
215
257
  return evaluator.evaluateCondition(expr, { throwOnError: true });
216
258
  }
217
259
  catch (err) {
@@ -255,26 +297,6 @@ export class ActionEngine {
255
297
  }
256
298
  return this.runner.execute(registered.action);
257
299
  }
258
- /** Dispatch an event — finds mapped actions and executes them */
259
- async dispatch(event, contextOverride) {
260
- const matchingMappings = this.mappings.filter(m => m.event === event);
261
- if (matchingMappings.length === 0) {
262
- return [];
263
- }
264
- const results = [];
265
- for (const mapping of matchingMappings) {
266
- // Check condition if present
267
- if (mapping.condition) {
268
- const evaluator = this.runner.getEvaluator();
269
- const shouldRun = evaluator.evaluateCondition(mapping.condition);
270
- if (!shouldRun)
271
- continue;
272
- }
273
- const result = await this.executeAction(mapping.actionName, contextOverride);
274
- results.push(result);
275
- }
276
- return results;
277
- }
278
300
  /** Handle a keyboard shortcut event — returns true if handled */
279
301
  async handleShortcut(keys, contextOverride) {
280
302
  const actionName = this.normalizedShortcutMap.get(normalizeShortcut(keys));
@@ -334,10 +356,9 @@ export class ActionEngine {
334
356
  updateContext(context) {
335
357
  this.runner.updateContext(context);
336
358
  }
337
- /** Clear all registered actions and mappings */
359
+ /** Clear all registered actions and shortcuts */
338
360
  clear() {
339
361
  this.actions.clear();
340
- this.mappings = [];
341
362
  this.shortcuts = [];
342
363
  this.normalizedShortcutMap.clear();
343
364
  }