@object-ui/core 16.0.0 → 17.1.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 +1589 -1
- package/dist/actions/ActionRunner.d.ts +88 -16
- package/dist/actions/ActionRunner.js +164 -81
- package/dist/actions/actionKeys.d.ts +118 -0
- package/dist/actions/actionKeys.js +243 -0
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/adapters/ApiDataSource.d.ts +11 -1
- package/dist/adapters/ApiDataSource.js +21 -1
- package/dist/adapters/ValueDataSource.d.ts +10 -2
- package/dist/adapters/ValueDataSource.js +9 -0
- package/dist/adapters/batchTransaction.d.ts +49 -0
- package/dist/adapters/batchTransaction.js +118 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js +1 -0
- package/dist/evaluator/ExpressionEvaluator.d.ts +10 -0
- package/dist/evaluator/ExpressionEvaluator.js +48 -0
- package/dist/evaluator/SafeExpressionParser.js +5 -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 +35 -6
- package/dist/evaluator/optionRules.js +18 -23
- package/dist/index.d.ts +5 -0
- package/dist/index.js +8 -0
- package/dist/registry/Registry.d.ts +81 -2
- package/dist/registry/Registry.js +102 -2
- package/dist/registry/public-blocks.js +52 -1
- package/dist/theme/ThemeEngine.js +0 -12
- package/dist/utils/chart-series.d.ts +28 -0
- 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 +2 -2
- package/dist/utils/dataset-format.d.ts +18 -1
- package/dist/utils/dataset-format.js +14 -1
- package/dist/utils/expand-fields.js +8 -8
- package/dist/utils/export-filename.js +4 -1
- package/dist/utils/filter-converter.d.ts +47 -0
- package/dist/utils/filter-converter.js +94 -7
- package/dist/utils/filter-tokens.d.ts +101 -0
- package/dist/utils/filter-tokens.js +173 -0
- package/dist/utils/managedBy.d.ts +119 -0
- package/dist/utils/managedBy.js +147 -0
- package/dist/utils/normalize-list-view.d.ts +99 -0
- package/dist/utils/normalize-list-view.js +255 -0
- package/dist/utils/record-title.js +0 -0
- package/dist/utils/sort-values.d.ts +52 -0
- package/dist/utils/sort-values.js +125 -0
- package/dist/validation/index.d.ts +9 -1
- package/dist/validation/index.js +9 -1
- 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,14 +5,6 @@
|
|
|
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 { type UndoableOperation } from './UndoManager';
|
|
18
10
|
export interface ActionResult {
|
|
@@ -70,7 +62,8 @@ export interface ApiConfig {
|
|
|
70
62
|
* Compatible with both UIActionSchema (spec v2.0.1) and legacy crud.ts ActionSchema.
|
|
71
63
|
*/
|
|
72
64
|
export interface ActionDef {
|
|
73
|
-
/** Action type identifier
|
|
65
|
+
/** Action type identifier — a `RunnableActionType` (the spec's six plus the
|
|
66
|
+
* `navigation` alias of `url`) or a custom type with a registered handler. */
|
|
74
67
|
type?: string;
|
|
75
68
|
/** Legacy action type field */
|
|
76
69
|
actionType?: string;
|
|
@@ -131,10 +124,33 @@ export interface ActionDef {
|
|
|
131
124
|
* reveal dialog rendering `result.data` (see ResultDialogSpec).
|
|
132
125
|
*/
|
|
133
126
|
resultDialog?: ResultDialogSpec;
|
|
134
|
-
/**
|
|
135
|
-
|
|
136
|
-
|
|
127
|
+
/**
|
|
128
|
+
* The one handler slot: script name/expression for `type: 'script'`, URL for
|
|
129
|
+
* `'url'`, flow name for `'flow'`, modal/page for `'modal'`, endpoint for
|
|
130
|
+
* `'api'`, FormView name for `'form'`.
|
|
131
|
+
*
|
|
132
|
+
* The `execute` alias was REMOVED in @objectstack/spec 17 (#3855) and is not
|
|
133
|
+
* read here (#3856) — don't re-add it. Two handler slots is how one action ran
|
|
134
|
+
* one script server-side and a different one client-side (#3713).
|
|
135
|
+
*/
|
|
137
136
|
target?: string;
|
|
137
|
+
/**
|
|
138
|
+
* Action body (spec `ActionSchema.body` — `HookBodySchema`). Opaque here:
|
|
139
|
+
* bodies are a SERVER-side execution surface and this runner never
|
|
140
|
+
* interprets one.
|
|
141
|
+
*
|
|
142
|
+
* L2 (`language: 'js'`) is defined as a function body run inside an isolated
|
|
143
|
+
* VM enforcing declared capabilities, `timeoutMs` and `memoryMb`. A browser
|
|
144
|
+
* has no such isolate, so "enforcing" those client-side would be decoration.
|
|
145
|
+
* L1 (`language: 'expression'`) is formula-engine (CEL) source, a different
|
|
146
|
+
* dialect from this package's `${…}` ExpressionEvaluator — running it here
|
|
147
|
+
* would diverge silently rather than fail.
|
|
148
|
+
*
|
|
149
|
+
* Consumers dispatch bodies by registering a `script` handler that POSTs to
|
|
150
|
+
* `/api/v1/actions/{object}/{action}` (see app-shell's
|
|
151
|
+
* `useConsoleActionRuntime`); the server runs the body through its sandbox.
|
|
152
|
+
*/
|
|
153
|
+
body?: unknown;
|
|
138
154
|
/** For type: 'url' — where to open `target`. `'new-tab'` forces a new
|
|
139
155
|
* browser tab/window, `'self'` forces same-page navigation. When omitted,
|
|
140
156
|
* external URLs open in a new tab and relative URLs navigate in place.
|
|
@@ -258,6 +274,12 @@ export interface ActionParamDef {
|
|
|
258
274
|
* gated on `features.phoneNumber`). Absent = always visible.
|
|
259
275
|
*/
|
|
260
276
|
visible?: string;
|
|
277
|
+
/** Accepted upload types (MIME types / extensions) for `file`/`image` params. */
|
|
278
|
+
accept?: string[];
|
|
279
|
+
/** Max upload size in bytes for `file`/`image` params. */
|
|
280
|
+
maxSize?: number;
|
|
281
|
+
/** Allow multiple values (file/image/lookup/user params → array value). */
|
|
282
|
+
multiple?: boolean;
|
|
261
283
|
/** Object name the lookup picker queries (`reference_to` on the field). */
|
|
262
284
|
referenceTo?: string;
|
|
263
285
|
/** Field on the referenced record used as the human label (default `name`). */
|
|
@@ -266,8 +288,6 @@ export interface ActionParamDef {
|
|
|
266
288
|
idField?: string;
|
|
267
289
|
/** Optional secondary line shown under the label in the picker. */
|
|
268
290
|
descriptionField?: string;
|
|
269
|
-
/** Allow multi-select (renders chip list). */
|
|
270
|
-
multiple?: boolean;
|
|
271
291
|
/** Template (e.g. `{first_name} {last_name}`) used to build the label. */
|
|
272
292
|
titleFormat?: string;
|
|
273
293
|
/** Column definitions surfaced in the enterprise `RecordPickerDialog`. */
|
|
@@ -290,6 +310,21 @@ export declare class ActionRunner {
|
|
|
290
310
|
private navigationHandler;
|
|
291
311
|
private paramCollectionHandler;
|
|
292
312
|
private resultDialogHandler;
|
|
313
|
+
/**
|
|
314
|
+
* Built-in dispatch, one entry per runnable action type.
|
|
315
|
+
*
|
|
316
|
+
* A table rather than a `switch` because the type annotation is the guard:
|
|
317
|
+
* `Record<RunnableActionType, …>` stops compiling the moment
|
|
318
|
+
* `@objectstack/spec` adds an `ActionType` this runner has no executor for.
|
|
319
|
+
* That is the whole Tier-2 failure class for actions — a spec name that
|
|
320
|
+
* validates at save and then does nothing at run time (#2942) — converted
|
|
321
|
+
* into a build error. It also rejects the reverse: an executor keyed by a
|
|
322
|
+
* name that is neither in the spec nor a declared local alias.
|
|
323
|
+
*
|
|
324
|
+
* `navigation` is that declared alias (see `ObjectUiLocalActionType`); it
|
|
325
|
+
* shares `url`'s navigator.
|
|
326
|
+
*/
|
|
327
|
+
private readonly builtinExecutors;
|
|
293
328
|
constructor(context?: ActionContext);
|
|
294
329
|
/**
|
|
295
330
|
* Set a custom confirmation handler (e.g., Shadcn AlertDialog).
|
|
@@ -347,6 +382,21 @@ export declare class ActionRunner {
|
|
|
347
382
|
* Uses navigationHandler for SPA routing, falls back to window.location.
|
|
348
383
|
*/
|
|
349
384
|
private executeUrl;
|
|
385
|
+
/**
|
|
386
|
+
* The one navigator behind `type: 'url'` and its `navigation` alias.
|
|
387
|
+
*
|
|
388
|
+
* Both names mean "go to a location", and they had two implementations that
|
|
389
|
+
* drifted: only this one interpolated `${param.X}`, promoted `/api/…` to the
|
|
390
|
+
* `apiBase`, short-circuited a redirect-dance URL to a full-page load, and
|
|
391
|
+
* honoured `openIn`. A `navigation` action was quietly the weaker of the two
|
|
392
|
+
* (#2944 item 3). Sharing the body is what keeps the alias honest.
|
|
393
|
+
*
|
|
394
|
+
* `action` supplies the interpolation context (`params`) and `openIn`;
|
|
395
|
+
* `source` supplies the navigation modifiers, which live on the nested
|
|
396
|
+
* `action.navigate` block for the legacy shape and on the action itself
|
|
397
|
+
* otherwise.
|
|
398
|
+
*/
|
|
399
|
+
private navigateTo;
|
|
350
400
|
/**
|
|
351
401
|
* Execute modal action — open a dialog.
|
|
352
402
|
* Delegates to the registered modalHandler; returns modal schema if no handler.
|
|
@@ -357,10 +407,22 @@ export declare class ActionRunner {
|
|
|
357
407
|
* Delegates to a registered 'flow' handler; otherwise returns not-implemented.
|
|
358
408
|
*/
|
|
359
409
|
private executeFlow;
|
|
360
|
-
private executeActionSchema;
|
|
361
410
|
/**
|
|
362
|
-
*
|
|
411
|
+
* Terminal fallback for a dispatch the switch above did not recognize: no
|
|
412
|
+
* known `type`, no registered handler, no `navigate` / `api` / `endpoint` /
|
|
413
|
+
* `onClick`. Some schema-only shapes are still runnable here — an action
|
|
414
|
+
* whose whole payload is a `redirect`, or an explicit `reload` / `close`
|
|
415
|
+
* directive — so this stays a real execution path.
|
|
416
|
+
*
|
|
417
|
+
* What it must never do is report success for an action it did not run. It
|
|
418
|
+
* used to return `{ success: true, reload: true, close: true }` for a
|
|
419
|
+
* COMPLETELY EMPTY schema, which is how a legacy string row action —
|
|
420
|
+
* dispatched as `{ type: '<action name>' }`, the name landing in the `type`
|
|
421
|
+
* slot — reached the user as a green "Action completed successfully" toast
|
|
422
|
+
* having issued zero requests (objectui#2960). An unresolvable dispatch is a
|
|
423
|
+
* wiring bug; surface it instead of laundering it into a success.
|
|
363
424
|
*/
|
|
425
|
+
private executeActionSchema;
|
|
364
426
|
/**
|
|
365
427
|
* `form` action — open a FormView as a routed page (`/forms/:name`, per the
|
|
366
428
|
* spec). `target` is the FormView name. Without this case the action fell
|
|
@@ -369,6 +431,16 @@ export declare class ActionRunner {
|
|
|
369
431
|
* hosts that support it; the form route ignores unknown query params.
|
|
370
432
|
*/
|
|
371
433
|
private executeForm;
|
|
434
|
+
/**
|
|
435
|
+
* `navigation` — objectui's alias of the spec's `url` (`ObjectUiLocalActionType`).
|
|
436
|
+
*
|
|
437
|
+
* Kept because `{ type: 'navigation', to: … }` is authored today
|
|
438
|
+
* (`element:button` CTAs) and because deleting the case would not fail loudly:
|
|
439
|
+
* the action would land in `executeActionSchema` and report success while
|
|
440
|
+
* navigating nowhere. All it does now is resolve the alias's own spelling of
|
|
441
|
+
* the target and hand off to the shared navigator, so the two names cannot
|
|
442
|
+
* drift apart again. Prefer `type: 'url'` + `openIn` in new metadata.
|
|
443
|
+
*/
|
|
372
444
|
private executeNavigation;
|
|
373
445
|
/**
|
|
374
446
|
* Execute API action — supports both simple string endpoint and complex ApiConfig.
|
|
@@ -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.
|
|
@@ -0,0 +1,118 @@
|
|
|
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, i.e. step 2's worklist ─────────────────────────
|
|
36
|
+
* `ActionDef` declares 34 keys, the spec's `ActionSchema` declares 36, and they
|
|
37
|
+
* overlap on 17. The two halves of the difference are different problems:
|
|
38
|
+
*
|
|
39
|
+
* 18 keys the SPEC owns that `ActionDef` never declared — `ai`, `aria`,
|
|
40
|
+
* `bodyExtra`, `bodyShape`, `bulkEnabled`, `component`, `icon`, `locations`,
|
|
41
|
+
* `mode`, `objectName`, `order`, `recordIdField`, `recordIdParam`,
|
|
42
|
+
* `requiredPermissions`, `requiresFeature`, `shortcut`, `variant`, `visible`.
|
|
43
|
+
* These are authored today and reach readers through the index signature.
|
|
44
|
+
* `ActionEngine` reads two of them (`visible` at the location filter, `locations`
|
|
45
|
+
* at registration) through an `as any` cast — a cast that exists ONLY because the
|
|
46
|
+
* field is undeclared, and that goes away when step 2 promotes them.
|
|
47
|
+
*
|
|
48
|
+
* 17 keys `ActionDef` declares that the spec does not own — `actionType`, `api`,
|
|
49
|
+
* `chain`, `chainMode`, `close`, `condition`, `confirm`, `endpoint`, `modal`,
|
|
50
|
+
* `navigate`, `onClick`, `onFailure`, `onSuccess`, `redirect`, `reload`, `toast`,
|
|
51
|
+
* `actionParams`. Several are already marked legacy at their declaration. These
|
|
52
|
+
* are objectui's own dialect and need the #4115 treatment: kept and documented as
|
|
53
|
+
* deliberate, or retired — but not silently carried.
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* Every property `ActionDef` declares, in declaration order.
|
|
57
|
+
*
|
|
58
|
+
* Kept as data because TypeScript types do not survive to runtime — and
|
|
59
|
+
* `keyof ActionDef` cannot substitute for this list: the index signature widens
|
|
60
|
+
* it to `string | number`, which is precisely the problem being worked around.
|
|
61
|
+
*/
|
|
62
|
+
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"];
|
|
63
|
+
/**
|
|
64
|
+
* Every property `@objectstack/spec`'s `ActionSchema` declares.
|
|
65
|
+
*
|
|
66
|
+
* `ActionDef` mirrors that schema, so a key the spec owns is legitimate even
|
|
67
|
+
* when `ActionDef` has not declared it yet — several are read today only through
|
|
68
|
+
* an `as any` cast (`visible` and `locations` in `ActionEngine`), which is the
|
|
69
|
+
* cast this list exists to eventually retire.
|
|
70
|
+
*
|
|
71
|
+
* Pinned rather than read off the schema at runtime: the spec exports
|
|
72
|
+
* `ActionSchema` as a lazy proxy that does not forward `.shape`, so resolving it
|
|
73
|
+
* means walking zod internals (`_def.in` / `_def.innerType`). That is fine in a
|
|
74
|
+
* test and wrong in shipped code — `@object-ui/types` made the same call for
|
|
75
|
+
* `ActionComponent`. The pin test does the walk and fails the day this drifts.
|
|
76
|
+
*/
|
|
77
|
+
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"];
|
|
78
|
+
/**
|
|
79
|
+
* The `navigation` alias's own spelling of a target, read off the action itself
|
|
80
|
+
* when no nested `navigate` object is present (`ActionRunner.executeNavigation`).
|
|
81
|
+
*
|
|
82
|
+
* A declared objectui-side dialect, not spec vocabulary: `replace` is documented
|
|
83
|
+
* at its read site as "the one thing the `navigation` shape carries that
|
|
84
|
+
* `ActionSchema` has no field for". Listing it here is what keeps it from being
|
|
85
|
+
* silent dialect — the alternative is a warning that cries wolf on a shape the
|
|
86
|
+
* runner itself supports.
|
|
87
|
+
*/
|
|
88
|
+
export declare const NAVIGATION_ALIAS_KEYS: readonly ["to", "external", "newTab", "replace"];
|
|
89
|
+
/**
|
|
90
|
+
* Keys the spec has TOMBSTONED: still present in `ActionSchema` so the parser can
|
|
91
|
+
* reject them BY NAME with a rename prescription, rather than fail with a bare
|
|
92
|
+
* "unrecognized key".
|
|
93
|
+
*
|
|
94
|
+
* Warned about separately from unknown keys, and more loudly: an unknown key is
|
|
95
|
+
* probably a typo, while a retired key is metadata that used to work. That
|
|
96
|
+
* distinction is why `executeScript` carries a runtime branch returning the
|
|
97
|
+
* rename prescription — a branch that exists solely to compensate for the index
|
|
98
|
+
* signature, and that can retire with it in step 3.
|
|
99
|
+
*/
|
|
100
|
+
export declare const RETIRED_ACTION_KEYS: Readonly<Record<string, string>>;
|
|
101
|
+
/** Every key an action may legitimately carry today. */
|
|
102
|
+
export declare const KNOWN_ACTION_KEYS: ReadonlySet<string>;
|
|
103
|
+
/** Split an action's own keys into the two things worth saying out loud. */
|
|
104
|
+
export declare function classifyActionKeys(action: object | null | undefined): {
|
|
105
|
+
unknown: string[];
|
|
106
|
+
retired: string[];
|
|
107
|
+
};
|
|
108
|
+
/** Reset the warn-once memo. Exported for tests. */
|
|
109
|
+
export declare function resetActionKeyWarnings(): void;
|
|
110
|
+
/**
|
|
111
|
+
* Dev-mode only: name the keys an action carries that no reader will ever look
|
|
112
|
+
* at. Non-breaking by construction — it changes no types and rejects nothing.
|
|
113
|
+
*
|
|
114
|
+
* Silently binding no handler is the #2169 "Mark Done does nothing" shape, and
|
|
115
|
+
* the compiler cannot help while the index signature stands. Until it comes
|
|
116
|
+
* down, this is the audible half.
|
|
117
|
+
*/
|
|
118
|
+
export declare function warnOnUnknownActionKeys(action: object | null | undefined, where?: string): void;
|