@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.
- package/CHANGELOG.md +1881 -4
- package/dist/actions/ActionEngine.d.ts +11 -1
- package/dist/actions/ActionEngine.js +14 -3
- package/dist/actions/ActionRunner.d.ts +199 -24
- package/dist/actions/ActionRunner.js +189 -84
- package/dist/actions/actionKeys.d.ts +135 -0
- package/dist/actions/actionKeys.js +283 -0
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/adapters/ApiDataSource.js +10 -1
- package/dist/adapters/ValueDataSource.d.ts +2 -2
- package/dist/evaluator/fieldRules.d.ts +0 -2
- package/dist/evaluator/fieldRules.js +3 -2
- package/dist/evaluator/optionLint.js +5 -2
- package/dist/evaluator/optionRules.d.ts +13 -6
- package/dist/evaluator/optionRules.js +0 -23
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -0
- package/dist/query/query-ast.d.ts +6 -6
- package/dist/query/query-ast.js +2 -2
- package/dist/registry/PluginSystem.d.ts +15 -5
- package/dist/registry/PluginSystem.js +1 -1
- package/dist/registry/Registry.d.ts +81 -2
- package/dist/registry/Registry.js +102 -2
- package/dist/registry/WidgetRegistry.d.ts +7 -7
- package/dist/registry/WidgetRegistry.js +1 -1
- package/dist/registry/public-blocks.js +52 -1
- package/dist/styling/scoped-styles.d.ts +14 -8
- package/dist/theme/ThemeEngine.js +0 -12
- package/dist/utils/chart-series.d.ts +42 -2
- package/dist/utils/chart-series.js +55 -0
- package/dist/utils/column-identity.d.ts +110 -0
- package/dist/utils/column-identity.js +210 -0
- package/dist/utils/dashboard-filters.d.ts +11 -2
- package/dist/utils/dashboard-filters.js +99 -4
- package/dist/utils/dataset-format.d.ts +13 -1
- package/dist/utils/dataset-format.js +10 -2
- package/dist/utils/expand-fields.js +8 -8
- package/dist/utils/filter-converter.d.ts +47 -0
- package/dist/utils/filter-converter.js +94 -7
- package/dist/utils/filter-tokens.d.ts +108 -0
- package/dist/utils/filter-tokens.js +180 -0
- package/dist/utils/freeze-schema.d.ts +14 -6
- package/dist/utils/freeze-schema.js +14 -6
- package/dist/utils/managedBy.d.ts +97 -51
- package/dist/utils/managedBy.js +117 -53
- package/dist/utils/normalize-list-view.d.ts +109 -0
- package/dist/utils/normalize-list-view.js +255 -0
- package/dist/utils/sort-values.d.ts +52 -0
- package/dist/utils/sort-values.js +125 -0
- package/dist/validation/index.d.ts +10 -1
- package/dist/validation/index.js +10 -1
- package/dist/validation/required-presence.d.ts +31 -0
- package/dist/validation/required-presence.js +36 -0
- package/dist/validation/schema-validator.d.ts +24 -8
- package/dist/validation/validators/index.d.ts +7 -1
- package/dist/validation/validators/index.js +7 -1
- package/dist/validation/validators/object-validation-engine.d.ts +151 -31
- package/dist/validation/validators/object-validation-engine.js +275 -142
- 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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
509
|
-
// external-URL heuristic.
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
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, {
|
|
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 (
|
|
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
|
|
605
|
-
if (!
|
|
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
|
|
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
|
|
723
|
-
*
|
|
724
|
-
*
|
|
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;
|