@object-ui/core 16.1.0 → 17.2.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 (60) hide show
  1. package/CHANGELOG.md +1881 -4
  2. package/dist/actions/ActionEngine.d.ts +11 -1
  3. package/dist/actions/ActionEngine.js +14 -3
  4. package/dist/actions/ActionRunner.d.ts +199 -24
  5. package/dist/actions/ActionRunner.js +189 -84
  6. package/dist/actions/actionKeys.d.ts +135 -0
  7. package/dist/actions/actionKeys.js +283 -0
  8. package/dist/actions/index.d.ts +1 -0
  9. package/dist/actions/index.js +1 -0
  10. package/dist/adapters/ApiDataSource.js +10 -1
  11. package/dist/adapters/ValueDataSource.d.ts +2 -2
  12. package/dist/evaluator/fieldRules.d.ts +0 -2
  13. package/dist/evaluator/fieldRules.js +3 -2
  14. package/dist/evaluator/optionLint.js +5 -2
  15. package/dist/evaluator/optionRules.d.ts +13 -6
  16. package/dist/evaluator/optionRules.js +0 -23
  17. package/dist/index.d.ts +4 -0
  18. package/dist/index.js +7 -0
  19. package/dist/query/query-ast.d.ts +6 -6
  20. package/dist/query/query-ast.js +2 -2
  21. package/dist/registry/PluginSystem.d.ts +15 -5
  22. package/dist/registry/PluginSystem.js +1 -1
  23. package/dist/registry/Registry.d.ts +81 -2
  24. package/dist/registry/Registry.js +102 -2
  25. package/dist/registry/WidgetRegistry.d.ts +7 -7
  26. package/dist/registry/WidgetRegistry.js +1 -1
  27. package/dist/registry/public-blocks.js +52 -1
  28. package/dist/styling/scoped-styles.d.ts +14 -8
  29. package/dist/theme/ThemeEngine.js +0 -12
  30. package/dist/utils/chart-series.d.ts +42 -2
  31. package/dist/utils/chart-series.js +55 -0
  32. package/dist/utils/column-identity.d.ts +110 -0
  33. package/dist/utils/column-identity.js +210 -0
  34. package/dist/utils/dashboard-filters.d.ts +11 -2
  35. package/dist/utils/dashboard-filters.js +99 -4
  36. package/dist/utils/dataset-format.d.ts +13 -1
  37. package/dist/utils/dataset-format.js +10 -2
  38. package/dist/utils/expand-fields.js +8 -8
  39. package/dist/utils/filter-converter.d.ts +47 -0
  40. package/dist/utils/filter-converter.js +94 -7
  41. package/dist/utils/filter-tokens.d.ts +108 -0
  42. package/dist/utils/filter-tokens.js +180 -0
  43. package/dist/utils/freeze-schema.d.ts +14 -6
  44. package/dist/utils/freeze-schema.js +14 -6
  45. package/dist/utils/managedBy.d.ts +97 -51
  46. package/dist/utils/managedBy.js +117 -53
  47. package/dist/utils/normalize-list-view.d.ts +109 -0
  48. package/dist/utils/normalize-list-view.js +255 -0
  49. package/dist/utils/sort-values.d.ts +52 -0
  50. package/dist/utils/sort-values.js +125 -0
  51. package/dist/validation/index.d.ts +10 -1
  52. package/dist/validation/index.js +10 -1
  53. package/dist/validation/required-presence.d.ts +31 -0
  54. package/dist/validation/required-presence.js +36 -0
  55. package/dist/validation/schema-validator.d.ts +24 -8
  56. package/dist/validation/validators/index.d.ts +7 -1
  57. package/dist/validation/validators/index.js +7 -1
  58. package/dist/validation/validators/object-validation-engine.d.ts +151 -31
  59. package/dist/validation/validators/object-validation-engine.js +275 -142
  60. package/package.json +4 -4
@@ -5,16 +5,9 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
- /**
9
- * @object-ui/core - Action Runner
10
- *
11
- * Executes actions defined in ActionSchema and EventHandler.
12
- * Supports all spec v2.0.1 action types: script, url, modal, flow, api.
13
- * Features: conditional execution, confirmation, toast notifications,
14
- * redirect handling, action chaining, custom handler registration.
15
- */
16
8
  import { ExpressionEvaluator } from '../evaluator/ExpressionEvaluator';
17
9
  import { globalUndoManager } from './UndoManager';
10
+ import { warnOnUnknownActionKeys } from './actionKeys';
18
11
  /**
19
12
  * Bind the server-CEL-parity `os.user` identity alias into an action context
20
13
  * (#2358 trap 1). `@objectstack/spec` documents the canonical CEL identity
@@ -96,6 +89,34 @@ export class ActionRunner {
96
89
  writable: true,
97
90
  value: void 0
98
91
  });
92
+ /**
93
+ * Built-in dispatch, one entry per runnable action type.
94
+ *
95
+ * A table rather than a `switch` because the type annotation is the guard:
96
+ * `Record<RunnableActionType, …>` stops compiling the moment
97
+ * `@objectstack/spec` adds an `ActionType` this runner has no executor for.
98
+ * That is the whole Tier-2 failure class for actions — a spec name that
99
+ * validates at save and then does nothing at run time (#2942) — converted
100
+ * into a build error. It also rejects the reverse: an executor keyed by a
101
+ * name that is neither in the spec nor a declared local alias.
102
+ *
103
+ * `navigation` is that declared alias (see `ObjectUiLocalActionType`); it
104
+ * shares `url`'s navigator.
105
+ */
106
+ Object.defineProperty(this, "builtinExecutors", {
107
+ enumerable: true,
108
+ configurable: true,
109
+ writable: true,
110
+ value: {
111
+ script: (action) => this.executeScript(action),
112
+ url: (action) => this.executeUrl(action),
113
+ modal: (action) => this.executeModal(action),
114
+ flow: (action) => this.executeFlow(action),
115
+ api: (action) => this.executeAPI(action),
116
+ form: (action) => this.executeForm(action),
117
+ navigation: (action) => this.executeNavigation(action),
118
+ }
119
+ });
99
120
  this.context = withIdentityAlias(context);
100
121
  this.evaluator = new ExpressionEvaluator(this.context);
101
122
  // Default confirmation: window.confirm (can be overridden)
@@ -165,6 +186,11 @@ export class ActionRunner {
165
186
  }
166
187
  async execute(action) {
167
188
  try {
189
+ // `ActionDef` accepts any key of any type, so a typo (`targt`) and a
190
+ // retired key (`execute`) both reach here having type-checked. Neither
191
+ // binds a handler, and binding no handler silently is the #2169 "Mark Done
192
+ // does nothing" shape. Dev-only, warn-once, changes nothing (#4075 step 1).
193
+ warnOnUnknownActionKeys(action);
168
194
  // Resolve the action type
169
195
  const actionType = action.type || action.actionType || action.name || '';
170
196
  // Conditional execution
@@ -237,45 +263,27 @@ export class ActionRunner {
237
263
  await this.handlePostExecution(action, result);
238
264
  return result;
239
265
  }
240
- // Built-in action execution by type
266
+ // Built-in action execution by type.
241
267
  let result;
242
- switch (actionType) {
243
- case 'script':
244
- result = await this.executeScript(action);
245
- break;
246
- case 'url':
247
- result = await this.executeUrl(action);
248
- break;
249
- case 'modal':
250
- result = await this.executeModal(action);
251
- break;
252
- case 'flow':
253
- result = await this.executeFlow(action);
254
- break;
255
- case 'api':
256
- result = await this.executeAPI(action);
257
- break;
258
- case 'form':
259
- result = await this.executeForm(action);
260
- break;
261
- case 'navigation':
262
- result = await this.executeNavigation(action);
263
- break;
264
- default:
265
- // Legacy fallback: check for navigate, api, or onClick
266
- if (action.navigate) {
267
- result = await this.executeNavigation(action);
268
- }
269
- else if (action.api || action.endpoint) {
270
- result = await this.executeAPI(action);
271
- }
272
- else if (action.onClick) {
273
- await action.onClick();
274
- result = { success: true };
275
- }
276
- else {
277
- result = await this.executeActionSchema(action);
278
- }
268
+ const builtin = actionType
269
+ ? this.builtinExecutors[actionType]
270
+ : undefined;
271
+ if (builtin) {
272
+ result = await builtin(action);
273
+ }
274
+ else if (action.navigate) {
275
+ // Legacy fallback: check for navigate, api, or onClick
276
+ result = await this.executeNavigation(action);
277
+ }
278
+ else if (action.api || action.endpoint) {
279
+ result = await this.executeAPI(action);
280
+ }
281
+ else if (action.onClick) {
282
+ await action.onClick();
283
+ result = { success: true };
284
+ }
285
+ else {
286
+ result = await this.executeActionSchema(action);
279
287
  }
280
288
  await this.handlePostExecution(action, result);
281
289
  return result;
@@ -424,8 +432,38 @@ export class ActionRunner {
424
432
  * Supports ${} template expressions referencing data, record, user context.
425
433
  */
426
434
  async executeScript(action) {
427
- const script = action.execute || action.target;
435
+ // `target` is the only handler slot. The `execute` alias was removed in
436
+ // @objectstack/spec 17 (#3855), which rejects an authored `execute` at parse
437
+ // with the rename prescription — so parsed metadata cannot carry it and a
438
+ // `target || execute` fallback could only ever evaluate to `target` (#3856).
439
+ const script = action.target;
428
440
  if (!script) {
441
+ // A spec `body` IS a script — this runner just cannot run one (see the
442
+ // `body` field docs). Saying "no script provided" would send the author
443
+ // hunting for a missing field they actually wrote, so name the real
444
+ // cause and the remedy instead. Checked before the retired `execute` key:
445
+ // a `body` action ignores `target`, so "rename it" would be wrong advice.
446
+ if (action.body != null) {
447
+ return {
448
+ success: false,
449
+ error: 'Action body must be executed server-side — this client runner does not interpret ' +
450
+ '`body` (sandboxed JS needs an isolated VM; expression bodies use the formula engine). ' +
451
+ 'Register a `script` handler that POSTs to /api/v1/actions/{object}/{action}.',
452
+ };
453
+ }
454
+ // ActionDef is open-ended (`[key: string]: any`), so hand-authored
455
+ // metadata that never passed through the spec parser still compiles with
456
+ // the retired key. Carry the same prescription the spec's tombstone does,
457
+ // for the same reason: a bare "no script provided" reads as "you forgot a
458
+ // field" to an author who did write one.
459
+ if (typeof action.execute === 'string') {
460
+ return {
461
+ success: false,
462
+ error: '`execute` was removed in @objectstack/spec 17 — rename the key to `target`. ' +
463
+ 'The value (a script name or expression) is unchanged. ' +
464
+ 'Run `os migrate meta --from 16` to rewrite it automatically.',
465
+ };
466
+ }
429
467
  return { success: false, error: 'No script provided for script action' };
430
468
  }
431
469
  // Named script registry wins over the expression evaluator. This lets
@@ -464,6 +502,23 @@ export class ActionRunner {
464
502
  if (!rawUrl) {
465
503
  return { success: false, error: 'No URL provided for url action' };
466
504
  }
505
+ return this.navigateTo(action, rawUrl, action);
506
+ }
507
+ /**
508
+ * The one navigator behind `type: 'url'` and its `navigation` alias.
509
+ *
510
+ * Both names mean "go to a location", and they had two implementations that
511
+ * drifted: only this one interpolated `${param.X}`, promoted `/api/…` to the
512
+ * `apiBase`, short-circuited a redirect-dance URL to a full-page load, and
513
+ * honoured `openIn`. A `navigation` action was quietly the weaker of the two
514
+ * (#2944 item 3). Sharing the body is what keeps the alias honest.
515
+ *
516
+ * `action` supplies the interpolation context (`params`) and `openIn`;
517
+ * `source` supplies the navigation modifiers, which live on the nested
518
+ * `action.navigate` block for the legacy shape and on the action itself
519
+ * otherwise.
520
+ */
521
+ async navigateTo(action, rawUrl, source) {
467
522
  // Apply target ${param.X} / ${ctx.X} interpolation FIRST — the
468
523
  // ExpressionEvaluator that follows would otherwise see unresolved
469
524
  // `param.provider` and either error or substitute undefined. Doing it
@@ -487,7 +542,9 @@ export class ActionRunner {
487
542
  if (apiBase && /^\/(api|_auth|_account)\//.test(url)) {
488
543
  url = `${apiBase}${url}`;
489
544
  }
490
- const isExternal = url.startsWith('http://') || url.startsWith('https://');
545
+ // `source.external` is the `navigation` shape's explicit override; the
546
+ // scheme heuristic covers every other case.
547
+ const isExternal = Boolean(source.external) || url.startsWith('http://') || url.startsWith('https://');
491
548
  // Same-origin API endpoints (most commonly the auth provider's
492
549
  // `/api/v1/auth/sign-in/social` redirect dance) issue server-side
493
550
  // 302s that must be followed by the *browser*, not the SPA router.
@@ -505,13 +562,22 @@ export class ActionRunner {
505
562
  return { success: true };
506
563
  }
507
564
  // `openIn` is the first-class, declarative switch (ActionSchema.openIn);
508
- // it wins over the legacy `params.newTab` escape hatch and the
509
- // external-URL heuristic.
510
- const newTab = action.openIn === 'new-tab' ? true
511
- : action.openIn === 'self' ? false
512
- : (action.params?.newTab ?? isExternal);
565
+ // it wins over the legacy `navigate.newTab` / `params.newTab` escape
566
+ // hatches and the external-URL heuristic.
567
+ const openIn = source.openIn ?? action.openIn;
568
+ const newTab = openIn === 'new-tab' ? true
569
+ : openIn === 'self' ? false
570
+ : (source.newTab ?? action.params?.newTab ?? isExternal);
571
+ // History semantics, the one thing the `navigation` shape carries that
572
+ // `ActionSchema` has no field for. Omitted from the handler call when
573
+ // unset so hosts see the same options object they always did.
574
+ const replace = source.replace ?? action.replace;
513
575
  if (this.navigationHandler) {
514
- this.navigationHandler(url, { external: isExternal, newTab });
576
+ this.navigationHandler(url, {
577
+ external: isExternal,
578
+ newTab,
579
+ ...(replace === undefined ? {} : { replace: Boolean(replace) }),
580
+ });
515
581
  return { success: true };
516
582
  }
517
583
  if (newTab) {
@@ -556,9 +622,37 @@ export class ActionRunner {
556
622
  error: `Flow handler not registered. Cannot execute flow: ${flowName}`,
557
623
  };
558
624
  }
625
+ /**
626
+ * Terminal fallback for a dispatch the switch above did not recognize: no
627
+ * known `type`, no registered handler, no `navigate` / `api` / `endpoint` /
628
+ * `onClick`. Some schema-only shapes are still runnable here — an action
629
+ * whose whole payload is a `redirect`, or an explicit `reload` / `close`
630
+ * directive — so this stays a real execution path.
631
+ *
632
+ * What it must never do is report success for an action it did not run. It
633
+ * used to return `{ success: true, reload: true, close: true }` for a
634
+ * COMPLETELY EMPTY schema, which is how a legacy string row action —
635
+ * dispatched as `{ type: '<action name>' }`, the name landing in the `type`
636
+ * slot — reached the user as a green "Action completed successfully" toast
637
+ * having issued zero requests (objectui#2960). An unresolvable dispatch is a
638
+ * wiring bug; surface it instead of laundering it into a success.
639
+ */
559
640
  async executeActionSchema(action) {
641
+ const hasApi = !!(action.api || action.endpoint);
642
+ // `reload`/`close` default to true downstream, so only an EXPLICIT `true`
643
+ // counts as declared intent — otherwise every empty schema would look like
644
+ // it asked for a reload and slip back through this guard.
645
+ const declaresBehavior = hasApi || !!action.onClick || !!action.redirect
646
+ || action.reload === true || action.close === true;
647
+ if (!declaresBehavior) {
648
+ const label = action.name || action.type || action.actionType || 'action';
649
+ return {
650
+ success: false,
651
+ error: `Action "${label}" is not executable: no handler is registered under that name and it declares no api, endpoint, navigate, redirect or onClick.`,
652
+ };
653
+ }
560
654
  const result = { success: true };
561
- if (action.api || action.endpoint) {
655
+ if (hasApi) {
562
656
  const apiResult = await this.executeAPI(action);
563
657
  if (!apiResult.success)
564
658
  return apiResult;
@@ -574,9 +668,6 @@ export class ActionRunner {
574
668
  }
575
669
  return result;
576
670
  }
577
- /**
578
- * Execute navigation action
579
- */
580
671
  /**
581
672
  * `form` action — open a FormView as a routed page (`/forms/:name`, per the
582
673
  * spec). `target` is the FormView name. Without this case the action fell
@@ -599,31 +690,23 @@ export class ActionRunner {
599
690
  }
600
691
  return { success: true, redirect: to };
601
692
  }
693
+ /**
694
+ * `navigation` — objectui's alias of the spec's `url` (`ObjectUiLocalActionType`).
695
+ *
696
+ * Kept because `{ type: 'navigation', to: … }` is authored today
697
+ * (`element:button` CTAs) and because deleting the case would not fail loudly:
698
+ * the action would land in `executeActionSchema` and report success while
699
+ * navigating nowhere. All it does now is resolve the alias's own spelling of
700
+ * the target and hand off to the shared navigator, so the two names cannot
701
+ * drift apart again. Prefer `type: 'url'` + `openIn` in new metadata.
702
+ */
602
703
  async executeNavigation(action) {
603
- const nav = action.navigate || action;
604
- const to = this.evaluator.evaluate(nav.to || nav.target);
605
- if (!this.isValidUrl(to)) {
606
- return {
607
- success: false,
608
- error: 'Invalid URL scheme. Only http://, https://, and relative URLs are allowed.',
609
- };
610
- }
611
- const isExternal = nav.external || (typeof to === 'string' && (to.startsWith('http://') || to.startsWith('https://')));
612
- if (this.navigationHandler) {
613
- this.navigationHandler(to, {
614
- external: isExternal,
615
- newTab: nav.newTab ?? isExternal,
616
- replace: nav.replace,
617
- });
618
- return { success: true };
619
- }
620
- if (isExternal) {
621
- window.open(to, '_blank', 'noopener,noreferrer');
622
- }
623
- else {
624
- return { success: true, redirect: to };
704
+ const nav = (action.navigate || action);
705
+ const rawUrl = nav.to || nav.target || nav.redirect;
706
+ if (!rawUrl) {
707
+ return { success: false, error: 'No URL provided for navigation action' };
625
708
  }
626
- return { success: true };
709
+ return this.navigateTo(action, rawUrl, nav);
627
710
  }
628
711
  /**
629
712
  * Execute API action — supports both simple string endpoint and complex ApiConfig.
@@ -719,9 +802,15 @@ export class ActionRunner {
719
802
  * fragment.
720
803
  *
721
804
  * `ctx.origin`, `ctx.user.*`, `ctx.org.*`, `ctx.recordId` are surfaced
722
- * implicitly from the runner's ActionContext + window. Consumers can
723
- * extend by stuffing extra keys under `context.ctx = {...}` before
724
- * calling `runner.execute()`.
805
+ * implicitly from the runner's ActionContext + window, and
806
+ * `ctx.selection.ids` / `ctx.selection.count` reflect the grid's current
807
+ * checkbox selection (`context.selectedRecords`) — `${ctx.selection.ids}`
808
+ * comma-joins the ids via the standard `String(array)` behaviour, so a
809
+ * list_toolbar url/api action can carry the selection without any bulk
810
+ * plumbing (objectui#3139). In an aggregate bulk dispatch,
811
+ * `${param._selectedIds}` interpolates the same ids from the injected
812
+ * param. Consumers can extend by stuffing extra keys under
813
+ * `context.ctx = {...}` before calling `runner.execute()`.
725
814
  */
726
815
  interpolateTarget(target, action) {
727
816
  if (typeof target !== 'string' || target.indexOf('${') === -1)
@@ -756,6 +845,22 @@ export class ActionRunner {
756
845
  user: this.context.user ?? {},
757
846
  org: this.context.org ?? this.context.organization ?? {},
758
847
  recordId: this.context.record?.id ?? this.context.recordId,
848
+ // Current list selection (published by the grid as `selectedRecords`).
849
+ // `${ctx.selection.ids}` in a url/api target comma-joins the ids —
850
+ // String(array) — giving toolbar actions selection access without any
851
+ // bulk-executor involvement (objectui#3139). Empty when nothing is
852
+ // selected, so authors can treat "" as "no selection".
853
+ selection: {
854
+ ids: Array.isArray(this.context.selectedRecords)
855
+ ? this.context.selectedRecords
856
+ .map((r) => r?.id)
857
+ .filter((v) => v != null)
858
+ .map(String)
859
+ : [],
860
+ count: Array.isArray(this.context.selectedRecords)
861
+ ? this.context.selectedRecords.length
862
+ : 0,
863
+ },
759
864
  };
760
865
  if (this.context.ctx && typeof this.context.ctx === 'object') {
761
866
  Object.assign(ctx, this.context.ctx);
@@ -0,0 +1,135 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * What an action is allowed to carry — objectstack#4075 step 1.
10
+ *
11
+ * `ActionDef` ends with `[key: string]: any`, so it accepts any key of any type.
12
+ * Concretely (objectui#2990): deleting `ActionDef.execute` produced ZERO compile
13
+ * errors even though the field had just been removed, and stale metadata still
14
+ * authoring `execute: 'markDone'` type-checks today. The same deletion against
15
+ * `@object-ui/types`' `ActionSchema` — which has no index signature — correctly
16
+ * produced `TS2353` at the authoring site. One of the two readers can catch a
17
+ * retired key; the other is structurally incapable.
18
+ *
19
+ * That asymmetry is the issue. An open key set on a DECLARED METADATA CONTRACT
20
+ * is what lets a typo (`targt`, `exectue`) and a tombstoned key (`execute`) both
21
+ * sail through to a runner that then silently does nothing — the #2169 "Mark
22
+ * Done does nothing" shape.
23
+ *
24
+ * This module is step 1 of the staged narrowing: it makes the key set VISIBLE
25
+ * and warns on anything outside it, without changing a single type. Nothing
26
+ * breaks, and an invisible failure becomes an audible one. Steps 2 and 3 —
27
+ * promoting the legitimate keys to explicit optional fields, then removing the
28
+ * index signature — are what finally let `tsc` catch typos and retired keys.
29
+ *
30
+ * The lists are pinned by `__tests__/actionKeys.pin.test.ts`, which re-derives
31
+ * each one from its actual source. A hand-maintained list that drifts from the
32
+ * interface it mirrors would be the same "declared ≠ enforced" trap this whole
33
+ * thread is about.
34
+ *
35
+ * ── What the inventory found, and what step 2 did with it ────────────────────
36
+ * Step 1 found `ActionDef` declaring 34 keys against the spec `ActionSchema`'s
37
+ * 36, overlapping on 17. The two halves of the difference were different
38
+ * problems, and step 2 (objectstack#4075) treated them differently:
39
+ *
40
+ * 18 keys the SPEC owns that `ActionDef` never declared — `ai`, `aria`,
41
+ * `bodyExtra`, `bodyShape`, `bulkEnabled`, `component`, `icon`, `locations`,
42
+ * `mode`, `objectName`, `order`, `recordIdField`, `recordIdParam`,
43
+ * `requiredPermissions`, `requiresFeature`, `shortcut`, `variant`, `visible`.
44
+ * These are authored today and used to reach readers through the index
45
+ * signature. **Step 2 promoted all 18 to explicit optional fields**, typed by
46
+ * DERIVATION from `@objectstack/spec`'s `ActionInput` rather than by hand-copied
47
+ * shapes. The `as any` casts `ActionEngine` needed to read `visible`,
48
+ * `locations` and `requiredPermissions` are gone with them — those casts existed
49
+ * only because the fields were undeclared.
50
+ *
51
+ * Two of the 18 landed as `undefined` rather than as usable types, which is the
52
+ * correct outcome: spec 17 retired `shortcut` and `bulkEnabled` as `retiredKey()`
53
+ * tombstones (`z.never()`), so deriving turns "authoring this is a parse
54
+ * rejection" into a compile error at no cost. Hand-copying would have quietly
55
+ * re-legitimized two dead keys — which is why the types are derived.
56
+ *
57
+ * 17 keys `ActionDef` declares that the spec does not own — `actionType`, `api`,
58
+ * `chain`, `chainMode`, `close`, `condition`, `confirm`, `endpoint`, `modal`,
59
+ * `navigate`, `onClick`, `onFailure`, `onSuccess`, `redirect`, `reload`, `toast`,
60
+ * `actionParams`. Step 2 marked `@deprecated`, with the spec spelling to use
61
+ * instead, ONLY the four the runner itself proves are aliases: `actionType` (→
62
+ * `type`), `api` and `endpoint` (→ `target`; `executeAPI` resolves
63
+ * `api || endpoint || target`), and `navigate` (→ flat `target`/`openIn`;
64
+ * `executeNavigation` already falls back to them). The rest are runner mechanics
65
+ * with no spec counterpart — chaining, toasts, post-execution reload/close — and
66
+ * deprecating them toward a spelling that does not exist would be worse than
67
+ * leaving them declared.
68
+ *
69
+ * Step 3 remains: remove the index signature, at which point `tsc` catches both
70
+ * typos and retired keys and the dev-mode warning below can retire with it.
71
+ */
72
+ /**
73
+ * Every property `ActionDef` declares, in declaration order.
74
+ *
75
+ * Kept as data because TypeScript types do not survive to runtime — and
76
+ * `keyof ActionDef` cannot substitute for this list: the index signature widens
77
+ * it to `string | number`, which is precisely the problem being worked around.
78
+ */
79
+ export declare const ACTION_DEF_KEYS: readonly ["type", "actionType", "name", "label", "confirmText", "confirm", "condition", "disabled", "api", "endpoint", "method", "navigate", "onClick", "reload", "close", "redirect", "toast", "successMessage", "errorMessage", "refreshAfter", "undoable", "params", "actionParams", "resultDialog", "target", "body", "openIn", "modal", "chain", "chainMode", "onSuccess", "onFailure", "opensInNewTab", "newTabUrl", "locations", "visible", "requiredPermissions", "icon", "variant", "order", "component", "objectName", "ai", "aria", "bodyExtra", "bodyShape", "mode", "recordIdField", "recordIdParam", "requiresFeature", "shortcut", "bulkEnabled"];
80
+ /**
81
+ * Every property `@objectstack/spec`'s `ActionSchema` declares.
82
+ *
83
+ * `ActionDef` mirrors that schema, so a key the spec owns is legitimate even
84
+ * when `ActionDef` has not declared it yet — several are read today only through
85
+ * an `as any` cast (`visible` and `locations` in `ActionEngine`), which is the
86
+ * cast this list exists to eventually retire.
87
+ *
88
+ * Pinned rather than read off the schema at runtime: the spec exports
89
+ * `ActionSchema` as a lazy proxy that does not forward `.shape`, so resolving it
90
+ * means walking zod internals (`_def.in` / `_def.innerType`). That is fine in a
91
+ * test and wrong in shipped code — `@object-ui/types` made the same call for
92
+ * `ActionComponent`. The pin test does the walk and fails the day this drifts.
93
+ */
94
+ export declare const SPEC_ACTION_KEYS: readonly ["ai", "aria", "body", "bodyExtra", "bodyShape", "bulkEnabled", "component", "confirmText", "disabled", "errorMessage", "execute", "icon", "label", "locations", "method", "mode", "name", "newTabUrl", "objectName", "openIn", "opensInNewTab", "order", "params", "recordIdField", "recordIdParam", "refreshAfter", "requiredPermissions", "requiresFeature", "resultDialog", "shortcut", "successMessage", "target", "type", "undoable", "variant", "visible"];
95
+ /**
96
+ * The `navigation` alias's own spelling of a target, read off the action itself
97
+ * when no nested `navigate` object is present (`ActionRunner.executeNavigation`).
98
+ *
99
+ * A declared objectui-side dialect, not spec vocabulary: `replace` is documented
100
+ * at its read site as "the one thing the `navigation` shape carries that
101
+ * `ActionSchema` has no field for". Listing it here is what keeps it from being
102
+ * silent dialect — the alternative is a warning that cries wolf on a shape the
103
+ * runner itself supports.
104
+ */
105
+ export declare const NAVIGATION_ALIAS_KEYS: readonly ["to", "external", "newTab", "replace"];
106
+ /**
107
+ * Keys the spec has TOMBSTONED: still present in `ActionSchema` so the parser can
108
+ * reject them BY NAME with a rename prescription, rather than fail with a bare
109
+ * "unrecognized key".
110
+ *
111
+ * Warned about separately from unknown keys, and more loudly: an unknown key is
112
+ * probably a typo, while a retired key is metadata that used to work. That
113
+ * distinction is why `executeScript` carries a runtime branch returning the
114
+ * rename prescription — a branch that exists solely to compensate for the index
115
+ * signature, and that can retire with it in step 3.
116
+ */
117
+ export declare const RETIRED_ACTION_KEYS: Readonly<Record<string, string>>;
118
+ /** Every key an action may legitimately carry today. */
119
+ export declare const KNOWN_ACTION_KEYS: ReadonlySet<string>;
120
+ /** Split an action's own keys into the two things worth saying out loud. */
121
+ export declare function classifyActionKeys(action: object | null | undefined): {
122
+ unknown: string[];
123
+ retired: string[];
124
+ };
125
+ /** Reset the warn-once memo. Exported for tests. */
126
+ export declare function resetActionKeyWarnings(): void;
127
+ /**
128
+ * Dev-mode only: name the keys an action carries that no reader will ever look
129
+ * at. Non-breaking by construction — it changes no types and rejects nothing.
130
+ *
131
+ * Silently binding no handler is the #2169 "Mark Done does nothing" shape, and
132
+ * the compiler cannot help while the index signature stands. Until it comes
133
+ * down, this is the audible half.
134
+ */
135
+ export declare function warnOnUnknownActionKeys(action: object | null | undefined, where?: string): void;