@object-ui/core 17.5.0 → 17.6.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 +794 -0
- package/README.md +36 -22
- package/dist/actions/ActionEngine.d.ts +1 -1
- package/dist/actions/ActionEngine.js +1 -2
- package/dist/actions/ActionRunner.d.ts +82 -10
- package/dist/actions/ActionRunner.js +128 -22
- package/dist/actions/actionKeys.d.ts +68 -8
- package/dist/actions/actionKeys.js +81 -9
- package/dist/actions/actionResponse.js +1 -1
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/actions/recordIdParam.d.ts +77 -0
- package/dist/actions/recordIdParam.js +37 -0
- package/dist/actions/serverActionHandler.d.ts +1 -1
- package/dist/actions/serverActionHandler.js +1 -1
- package/dist/evaluator/ExpressionEvaluator.d.ts +42 -1
- package/dist/evaluator/ExpressionEvaluator.js +42 -1
- package/dist/evaluator/SafeExpressionParser.js +0 -4
- package/dist/evaluator/declaredPredicate.js +1 -1
- package/dist/evaluator/fieldRules.d.ts +19 -0
- package/dist/evaluator/fieldRules.js +30 -6
- package/dist/evaluator/index.d.ts +1 -0
- package/dist/evaluator/index.js +1 -0
- package/dist/evaluator/listConditional.d.ts +49 -2
- package/dist/evaluator/listConditional.js +70 -6
- package/dist/evaluator/optionRules.d.ts +55 -0
- package/dist/evaluator/optionRules.js +60 -0
- package/dist/evaluator/rowPredicateCanon.d.ts +67 -0
- package/dist/evaluator/rowPredicateCanon.js +208 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +17 -0
- package/dist/registry/Registry.d.ts +41 -19
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.js +1 -1
- package/dist/types/index.d.ts +92 -13
- package/dist/utils/chart-presentation.d.ts +205 -0
- package/dist/utils/chart-presentation.js +280 -0
- package/dist/utils/chart-series.d.ts +140 -26
- package/dist/utils/chart-series.js +380 -39
- package/dist/utils/column-identity.d.ts +43 -0
- package/dist/utils/column-identity.js +47 -1
- package/dist/utils/dataset-format.js +18 -6
- package/dist/utils/debug.js +7 -0
- package/dist/utils/expand-fields.d.ts +97 -7
- package/dist/utils/expand-fields.js +97 -7
- package/dist/utils/filter-tokens.js +0 -1
- package/dist/utils/freeze-schema.d.ts +38 -1
- package/dist/utils/freeze-schema.js +10 -0
- package/dist/utils/humanize-label.d.ts +60 -0
- package/dist/utils/humanize-label.js +62 -0
- package/dist/utils/managedBy.d.ts +27 -2
- package/dist/utils/normalize-list-view.d.ts +10 -0
- package/dist/utils/normalize-list-view.js +63 -1
- package/dist/utils/number-display.d.ts +7 -3
- package/dist/utils/predicate-fields.d.ts +29 -4
- package/dist/utils/predicate-fields.js +44 -4
- package/dist/utils/retired-field-types.d.ts +90 -0
- package/dist/utils/retired-field-types.js +124 -0
- package/dist/utils/sort-query.d.ts +12 -10
- package/dist/utils/unmaterialized-fields.d.ts +52 -0
- package/dist/utils/unmaterialized-fields.js +57 -0
- package/dist/validation/index.d.ts +1 -0
- package/dist/validation/index.js +1 -0
- package/dist/validation/schema-validator.js +105 -0
- package/dist/validation/server-owned-value.d.ts +62 -0
- package/dist/validation/server-owned-value.js +99 -0
- package/dist/validation/validators/object-validation-engine.js +0 -2
- package/package.json +4 -4
- package/dist/__benchmarks__/core.bench.d.ts +0 -8
- package/dist/__benchmarks__/core.bench.js +0 -53
package/README.md
CHANGED
|
@@ -4,7 +4,8 @@ Core logic, types, and validation for Object UI. Zero React dependencies.
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- 🎯 **Type Definitions** -
|
|
7
|
+
- 🎯 **Type Definitions** - Re-exported runtime types; the component schema
|
|
8
|
+
vocabulary itself is `@object-ui/types`
|
|
8
9
|
- 🔍 **Component Registry** - Framework-agnostic component registration system
|
|
9
10
|
- 📊 **Data Scope** - Data scope management and expression evaluation
|
|
10
11
|
- ✅ **Validation** - Zod-based schema validation
|
|
@@ -20,15 +21,20 @@ npm install @object-ui/core
|
|
|
20
21
|
|
|
21
22
|
### Type Definitions
|
|
22
23
|
|
|
24
|
+
The component schema vocabulary lives in `@object-ui/types`. Core depends on
|
|
25
|
+
that package and does not re-export it, so import the types from there. The
|
|
26
|
+
page node type is `PageNodeSchema` — the SDUI node, as distinct from the
|
|
27
|
+
authored page document.
|
|
28
|
+
|
|
23
29
|
```typescript
|
|
24
|
-
import type {
|
|
25
|
-
|
|
26
|
-
FormSchema,
|
|
30
|
+
import type {
|
|
31
|
+
PageNodeSchema,
|
|
32
|
+
FormSchema,
|
|
27
33
|
InputSchema,
|
|
28
|
-
BaseSchema
|
|
29
|
-
} from '@object-ui/
|
|
34
|
+
BaseSchema
|
|
35
|
+
} from '@object-ui/types'
|
|
30
36
|
|
|
31
|
-
const mySchema:
|
|
37
|
+
const mySchema: PageNodeSchema = {
|
|
32
38
|
type: 'page',
|
|
33
39
|
title: 'My Page',
|
|
34
40
|
body: []
|
|
@@ -40,22 +46,30 @@ const mySchema: PageSchema = {
|
|
|
40
46
|
```typescript
|
|
41
47
|
import { ComponentRegistry } from '@object-ui/core'
|
|
42
48
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const metadata = registry.get('button')
|
|
49
|
+
ComponentRegistry.register('button', buttonMetadata)
|
|
50
|
+
const metadata = ComponentRegistry.get('button')
|
|
46
51
|
```
|
|
47
52
|
|
|
53
|
+
`ComponentRegistry` is a process-level singleton exported by `@object-ui/core`;
|
|
54
|
+
`SchemaRenderer` resolves every `type` against it, so a component registered
|
|
55
|
+
here is renderable from schema anywhere in the app.
|
|
56
|
+
|
|
48
57
|
### Data Scope
|
|
49
58
|
|
|
59
|
+
`DataScopeManager` owns the named scopes a component tree reads from, and
|
|
60
|
+
`evaluateExpression` evaluates a `${...}` expression against a context. They
|
|
61
|
+
are separate exports: a scope holds data, it does not evaluate.
|
|
62
|
+
|
|
50
63
|
```typescript
|
|
51
|
-
import {
|
|
64
|
+
import { DataScopeManager, evaluateExpression } from '@object-ui/core'
|
|
52
65
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
})
|
|
66
|
+
const manager = new DataScopeManager()
|
|
67
|
+
manager.registerScope('user', { data: { name: 'John', role: 'admin' } })
|
|
56
68
|
|
|
57
|
-
const userName =
|
|
58
|
-
const isAdmin =
|
|
69
|
+
const userName = manager.getScope('user')?.data.name // 'John'
|
|
70
|
+
const isAdmin = evaluateExpression('${user.role === "admin"}', {
|
|
71
|
+
user: { name: 'John', role: 'admin' },
|
|
72
|
+
}) // true
|
|
59
73
|
```
|
|
60
74
|
|
|
61
75
|
### Server Action Dispatch (`createServerActionHandler`)
|
|
@@ -87,16 +101,16 @@ action identity (ADR-0110), the record-id resolution dance (`_rowRecord`,
|
|
|
87
101
|
guard, and the `/actions` response-envelope rule (`interpretActionResponse` /
|
|
88
102
|
`readActionPayload`, also exported).
|
|
89
103
|
|
|
90
|
-
### System Views (`
|
|
104
|
+
### System Views (`defineSystemView`)
|
|
91
105
|
|
|
92
106
|
Schemas authored in source code are part of the product contract and must
|
|
93
|
-
not be mutated at runtime. Wrap them with `
|
|
94
|
-
graph and tag it as a *System View*.
|
|
107
|
+
not be mutated at runtime. Wrap them with `defineSystemView()` to deep-freeze
|
|
108
|
+
the graph and tag it as a *System View*.
|
|
95
109
|
|
|
96
110
|
```typescript
|
|
97
|
-
import {
|
|
111
|
+
import { defineSystemView, cloneAsOverride, isSystemView } from '@object-ui/core'
|
|
98
112
|
|
|
99
|
-
export const userListView =
|
|
113
|
+
export const userListView = defineSystemView({
|
|
100
114
|
type: 'list',
|
|
101
115
|
data: { object: 'User' },
|
|
102
116
|
columns: [{ name: 'email' }],
|
|
@@ -115,7 +129,7 @@ isSystemView(draft) // false — clone is no longer Sys
|
|
|
115
129
|
|
|
116
130
|
| Tier | Source | Mutable? | API |
|
|
117
131
|
| ----------- | --------------------- | -------- | --------------------------- |
|
|
118
|
-
| System View | code (`import` / `as const`) | ❌ frozen | `
|
|
132
|
+
| System View | code (`import` / `as const`) | ❌ frozen | `defineSystemView()` |
|
|
119
133
|
| Tenant View | backend / DB | ⚠️ admin only | `cloneAsOverride()` + persist |
|
|
120
134
|
| User View | localStorage / API | ✅ user-editable | `cloneAsOverride()` + persist |
|
|
121
135
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Replaces callback-based patterns with a schema-driven pipeline:
|
|
15
15
|
* ActionSchema[] → ActionEngine → ActionRunner → ActionResult
|
|
16
16
|
*/
|
|
17
|
-
import { ActionRunner, type ActionDef, type ActionContext, type ActionResult } from './ActionRunner';
|
|
17
|
+
import { ActionRunner, type ActionDef, type ActionContext, type ActionResult } from './ActionRunner.js';
|
|
18
18
|
/**
|
|
19
19
|
* Action location types — re-exported from `@objectstack/spec/ui` so the
|
|
20
20
|
* platform has one source of truth (`ACTION_LOCATIONS`). Do NOT redeclare
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Replaces callback-based patterns with a schema-driven pipeline:
|
|
15
15
|
* ActionSchema[] → ActionEngine → ActionRunner → ActionResult
|
|
16
16
|
*/
|
|
17
|
-
import { ActionRunner } from './ActionRunner';
|
|
17
|
+
import { ActionRunner } from './ActionRunner.js';
|
|
18
18
|
import { toPredicateInput } from '../evaluator/predicateInput.js';
|
|
19
19
|
import { hasDeclaredPredicate } from '../evaluator/declaredPredicate.js';
|
|
20
20
|
export { ACTION_LOCATIONS } from '@object-ui/types';
|
|
@@ -38,7 +38,6 @@ function warnHiddenPredicate(name, raw, err) {
|
|
|
38
38
|
return;
|
|
39
39
|
_warnedPredicates.add(key);
|
|
40
40
|
const msg = err instanceof Error ? err.message : String(err);
|
|
41
|
-
// eslint-disable-next-line no-console
|
|
42
41
|
console.warn(`[ActionEngine] action "${String(name)}" hidden: its \`visible\` predicate threw — ${msg}. ` +
|
|
43
42
|
`Predicate: ${src}. Action predicates evaluate against { record, recordId, objectName, user, os.user, … }; ` +
|
|
44
43
|
`use \`record.<field>\` (not a bare field).`);
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import type { UIActionSchema } from '@object-ui/types';
|
|
23
23
|
import type { Action as SpecActionInput } from '@objectstack/spec/ui';
|
|
24
|
-
import { ExpressionEvaluator } from '../evaluator/ExpressionEvaluator';
|
|
25
|
-
import { type UndoableOperation } from './UndoManager';
|
|
24
|
+
import { ExpressionEvaluator } from '../evaluator/ExpressionEvaluator.js';
|
|
25
|
+
import { type UndoableOperation } from './UndoManager.js';
|
|
26
26
|
export interface ActionResult {
|
|
27
27
|
success: boolean;
|
|
28
28
|
data?: any;
|
|
@@ -134,15 +134,22 @@ export interface ActionDef {
|
|
|
134
134
|
* it — the two TS2353s the deletion produced repo-wide were both this key.
|
|
135
135
|
*/
|
|
136
136
|
description?: UIActionSchema['description'];
|
|
137
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* Confirmation text — shows a confirm dialog before executing. The ONE
|
|
139
|
+
* confirm spelling: `@objectstack/spec`'s action surface and the
|
|
140
|
+
* TranslationBundle key (`{ns}.objects.{obj}._actions.{name}.confirmText`)
|
|
141
|
+
* both stand on it.
|
|
142
|
+
*/
|
|
138
143
|
confirmText?: string;
|
|
139
|
-
/**
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
/**
|
|
145
|
+
* RETIRED (objectui#4314): the structured confirm object used to sit here
|
|
146
|
+
* and its `message` OUTRANKED `confirmText` — untranslated, since no bundle
|
|
147
|
+
* key exists for it. `?: never` so constructing an action with it is a tsc
|
|
148
|
+
* error rather than a silently-ignored key; the authorable twin
|
|
149
|
+
* (`@object-ui/types` `crud.ts`) carries the same tombstone.
|
|
150
|
+
* @deprecated Retired — author `confirmText` instead.
|
|
151
|
+
*/
|
|
152
|
+
confirm?: never;
|
|
146
153
|
/**
|
|
147
154
|
* Condition predicate — the action executes only while it holds.
|
|
148
155
|
*
|
|
@@ -382,6 +389,29 @@ export type ActionRunnerHandler = (action: ActionDef, context: ActionContext) =>
|
|
|
382
389
|
/**
|
|
383
390
|
* Confirmation handler — replaces window.confirm.
|
|
384
391
|
* Consumers can provide an async implementation (e.g., Shadcn AlertDialog).
|
|
392
|
+
*
|
|
393
|
+
* `options` is LIVE, and its producer is not in this file — searching the
|
|
394
|
+
* runner alone will tell you otherwise. The only call site here passes one
|
|
395
|
+
* argument (the structured `confirm` arm that used to forward a bag was
|
|
396
|
+
* retired, objectui#4314), so a runner-scoped sweep reads this parameter as
|
|
397
|
+
* declared-but-never-produced and files it for removal. That has already
|
|
398
|
+
* happened once: objectui#5205, ruled KEEP on 2026-08-22.
|
|
399
|
+
*
|
|
400
|
+
* The producer bypasses the runner entirely. `handleDeleteView` in
|
|
401
|
+
* `packages/app-shell/src/views/ObjectView.tsx` — the admin "delete saved
|
|
402
|
+
* view" confirmation — invokes a `ConfirmationHandler` with all three fields
|
|
403
|
+
* populated and localized (`console.objectView.deleteViewTitle` / `.delete` /
|
|
404
|
+
* `.cancel`), and `app-shell`'s `ActionConfirmDialog.tsx` renders them in
|
|
405
|
+
* place of its generic `actionConfirm.*` fallbacks. Line numbers are left out
|
|
406
|
+
* on purpose: #5205's pointer had already drifted by the time it was executed.
|
|
407
|
+
*
|
|
408
|
+
* So re-measure across the repo, not this package: grep for two-argument
|
|
409
|
+
* calls of `ConfirmationHandler`-typed values (`confirmHandler(`, `onConfirm(`)
|
|
410
|
+
* in `app-shell` as well as here. Dropping `options` costs that dialog its
|
|
411
|
+
* title and both button labels, and narrows a published `.d.ts` against the
|
|
412
|
+
* natural `(message, options) => …` spelling — which TypeScript rejects
|
|
413
|
+
* against a one-parameter target unless the second parameter is written
|
|
414
|
+
* optional.
|
|
385
415
|
*/
|
|
386
416
|
export type ConfirmationHandler = (message: string, options?: {
|
|
387
417
|
title?: string;
|
|
@@ -604,6 +634,30 @@ export declare class ActionRunner {
|
|
|
604
634
|
* Post-execution: emit toast notifications, handle chaining, callbacks.
|
|
605
635
|
*/
|
|
606
636
|
private handlePostExecution;
|
|
637
|
+
/**
|
|
638
|
+
* Perform the `ActionSchema.onSuccess` hop for an action that just succeeded.
|
|
639
|
+
*
|
|
640
|
+
* **The router is the app's own.** `navigationHandler` is the seam every
|
|
641
|
+
* navigator in this file already goes through, and the console wires it to
|
|
642
|
+
* react-router's `navigate` — so `openIn: 'self'` is a real SPA route hop,
|
|
643
|
+
* in place, immune to popup blocking. No `window.location` / `window.open`
|
|
644
|
+
* mechanism is introduced here; the no-handler fallback is the same
|
|
645
|
+
* `result.redirect` hand-off `navigateTo` already uses.
|
|
646
|
+
*
|
|
647
|
+
* **No default is written here.** `openIn` is a materialised
|
|
648
|
+
* `.default('self')` in the spec, so parse output always carries a resolved
|
|
649
|
+
* member; this reads the ONE member that changes the branch and leaves the
|
|
650
|
+
* other implicit. A `?? 'self'` would be a second source of truth for a
|
|
651
|
+
* default the producer already resolved.
|
|
652
|
+
*
|
|
653
|
+
* **The two `openIn` spellings never cross.** This reads
|
|
654
|
+
* `onSuccess.openIn` (`'self' | 'newTab'`) and nothing else — never the
|
|
655
|
+
* top-level `type: 'url'` switch, which spells its new-tab member
|
|
656
|
+
* `'new-tab'`. Spec refuses each crossover spelling with a keyed error; a
|
|
657
|
+
* renderer that accepted either would be a second, looser contract than the
|
|
658
|
+
* one authors are validated against.
|
|
659
|
+
*/
|
|
660
|
+
private navigateOnSuccess;
|
|
607
661
|
/**
|
|
608
662
|
* Execute script action — evaluates client-side expression via ExpressionEvaluator.
|
|
609
663
|
* Supports ${} template expressions referencing data, record, user context.
|
|
@@ -787,3 +841,21 @@ export declare class ActionRunner {
|
|
|
787
841
|
* Convenience function to execute a single action with a one-off runner.
|
|
788
842
|
*/
|
|
789
843
|
export declare function executeAction(action: ActionDef, context?: ActionContext): Promise<ActionResult>;
|
|
844
|
+
/** The `ActionSchema.onSuccess` block, as the pinned spec declares it. */
|
|
845
|
+
export interface OnSuccessNavigation {
|
|
846
|
+
navigate: string;
|
|
847
|
+
/** `'self' | 'newTab'` — read as data, so an off-contract value cannot widen the branch. */
|
|
848
|
+
openIn?: unknown;
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Is this `onSuccess` the SPEC's navigation block, or this runner's older
|
|
852
|
+
* chained-callback channel (`ActionDef | ActionDef[]`)?
|
|
853
|
+
*
|
|
854
|
+
* The test IS the spec's declaration: a non-array object carrying a STRING
|
|
855
|
+
* `navigate`. Nothing else can produce that shape — the spec object is strict
|
|
856
|
+
* with `navigate: z.string()` required, and on a callback `ActionDef`,
|
|
857
|
+
* `navigate` is the deprecated nested navigation ENVELOPE that
|
|
858
|
+
* `executeNavigation` reads `to`/`target`/`redirect` off, so a bare string
|
|
859
|
+
* there has never been runnable.
|
|
860
|
+
*/
|
|
861
|
+
export declare function readOnSuccessNavigation(value: unknown): OnSuccessNavigation | null;
|
|
@@ -5,10 +5,11 @@
|
|
|
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
|
-
import { ExpressionEvaluator } from '../evaluator/ExpressionEvaluator';
|
|
9
|
-
import { hasDeclaredPredicate } from '../evaluator/declaredPredicate';
|
|
10
|
-
import { globalUndoManager } from './UndoManager';
|
|
11
|
-
import { warnOnDeprecatedObjectParams, warnOnUnknownActionKeys } from './actionKeys';
|
|
8
|
+
import { ExpressionEvaluator } from '../evaluator/ExpressionEvaluator.js';
|
|
9
|
+
import { hasDeclaredPredicate } from '../evaluator/declaredPredicate.js';
|
|
10
|
+
import { globalUndoManager } from './UndoManager.js';
|
|
11
|
+
import { warnOnDeprecatedObjectParams, warnOnUnknownActionKeys } from './actionKeys.js';
|
|
12
|
+
import { readActionPayload } from './actionResponse.js';
|
|
12
13
|
/**
|
|
13
14
|
* Bind the server-CEL-parity `os.user` identity alias into an action context
|
|
14
15
|
* (#2358 trap 1). `@objectstack/spec` documents the canonical CEL identity
|
|
@@ -389,14 +390,13 @@ export class ActionRunner {
|
|
|
389
390
|
return { success: false, error: 'Action is disabled' };
|
|
390
391
|
}
|
|
391
392
|
}
|
|
392
|
-
// Confirmation
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
} : undefined);
|
|
393
|
+
// Confirmation. One spelling: `confirmText` — the only one the
|
|
394
|
+
// translation bundle can address (`…._actions.{name}.confirmText`). The
|
|
395
|
+
// structured `confirm.message` read that used to OUTRANK it is retired
|
|
396
|
+
// (objectui#4314): it had zero producers and no bundle key, so a dialog
|
|
397
|
+
// authored that way silently lost localization.
|
|
398
|
+
if (action.confirmText) {
|
|
399
|
+
const confirmed = await this.confirmHandler(this.evaluator.evaluate(action.confirmText));
|
|
400
400
|
if (!confirmed) {
|
|
401
401
|
return { success: false, error: 'Action cancelled by user' };
|
|
402
402
|
}
|
|
@@ -558,12 +558,10 @@ export class ActionRunner {
|
|
|
558
558
|
catch (err) {
|
|
559
559
|
// Acknowledgement failure is non-fatal; the underlying action already
|
|
560
560
|
// succeeded. Log so consumers can wire it through their error reporter.
|
|
561
|
-
// eslint-disable-next-line no-console
|
|
562
561
|
console.warn('[ActionRunner] resultDialog handler rejected; treating as acknowledged', err);
|
|
563
562
|
}
|
|
564
563
|
}
|
|
565
564
|
else {
|
|
566
|
-
// eslint-disable-next-line no-console
|
|
567
565
|
console.warn('[ActionRunner] action.resultDialog set but no resultDialogHandler registered — the response value will not be shown to the user.', { action: action.name, data: result.data });
|
|
568
566
|
}
|
|
569
567
|
}
|
|
@@ -586,16 +584,94 @@ export class ActionRunner {
|
|
|
586
584
|
if (chainResult.reload)
|
|
587
585
|
result.reload = true;
|
|
588
586
|
}
|
|
589
|
-
//
|
|
587
|
+
// ── ActionSchema.onSuccess — post-success navigation ────────────────────
|
|
588
|
+
//
|
|
589
|
+
// objectui#5221, the console half of objectstack#9566/#9474. The spec
|
|
590
|
+
// declares `onSuccess` as a CLOSED STRICT object
|
|
591
|
+
// `{ navigate: string, openIn: 'self' | 'newTab' }`, refine-scoped to
|
|
592
|
+
// `type: 'api'` and `type: 'script'` — the two types whose success event
|
|
593
|
+
// carries a server response for `${result.*}` to read.
|
|
594
|
+
//
|
|
595
|
+
// This runner's OWN `ActionDef.onSuccess` predates that key and means
|
|
596
|
+
// something else entirely: `ActionDef | ActionDef[]`, chained callbacks.
|
|
597
|
+
// The two are told apart by the spec's own declaration — a non-array object
|
|
598
|
+
// whose `navigate` is a STRING is the spec block and nothing else can be:
|
|
599
|
+
// `navigate` on a callback ActionDef is the deprecated nested navigation
|
|
600
|
+
// ENVELOPE (`executeNavigation` reads `navigate.to`), so a string there has
|
|
601
|
+
// never been runnable. This is a NARROWING to the declared contract, not a
|
|
602
|
+
// lenient fallback: a shape the spec refuses gets no new reading here.
|
|
603
|
+
//
|
|
604
|
+
// Before this branch existed, the ruled shape fell into the callback path,
|
|
605
|
+
// dispatched `{ navigate: '<string>' }` as an action, and failed inside
|
|
606
|
+
// `executeNavigation` with "No URL provided for navigation action" — the
|
|
607
|
+
// author got a red toast and no hop.
|
|
590
608
|
if (result.success && action.onSuccess) {
|
|
591
|
-
const
|
|
592
|
-
|
|
609
|
+
const navigation = readOnSuccessNavigation(action.onSuccess);
|
|
610
|
+
if (navigation) {
|
|
611
|
+
this.navigateOnSuccess(navigation, action, result);
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
const callbacks = Array.isArray(action.onSuccess) ? action.onSuccess : [action.onSuccess];
|
|
615
|
+
await this.executeChain(callbacks, 'sequential');
|
|
616
|
+
}
|
|
593
617
|
}
|
|
594
618
|
if (!result.success && action.onFailure) {
|
|
595
619
|
const callbacks = Array.isArray(action.onFailure) ? action.onFailure : [action.onFailure];
|
|
596
620
|
await this.executeChain(callbacks, 'sequential');
|
|
597
621
|
}
|
|
598
622
|
}
|
|
623
|
+
/**
|
|
624
|
+
* Perform the `ActionSchema.onSuccess` hop for an action that just succeeded.
|
|
625
|
+
*
|
|
626
|
+
* **The router is the app's own.** `navigationHandler` is the seam every
|
|
627
|
+
* navigator in this file already goes through, and the console wires it to
|
|
628
|
+
* react-router's `navigate` — so `openIn: 'self'` is a real SPA route hop,
|
|
629
|
+
* in place, immune to popup blocking. No `window.location` / `window.open`
|
|
630
|
+
* mechanism is introduced here; the no-handler fallback is the same
|
|
631
|
+
* `result.redirect` hand-off `navigateTo` already uses.
|
|
632
|
+
*
|
|
633
|
+
* **No default is written here.** `openIn` is a materialised
|
|
634
|
+
* `.default('self')` in the spec, so parse output always carries a resolved
|
|
635
|
+
* member; this reads the ONE member that changes the branch and leaves the
|
|
636
|
+
* other implicit. A `?? 'self'` would be a second source of truth for a
|
|
637
|
+
* default the producer already resolved.
|
|
638
|
+
*
|
|
639
|
+
* **The two `openIn` spellings never cross.** This reads
|
|
640
|
+
* `onSuccess.openIn` (`'self' | 'newTab'`) and nothing else — never the
|
|
641
|
+
* top-level `type: 'url'` switch, which spells its new-tab member
|
|
642
|
+
* `'new-tab'`. Spec refuses each crossover spelling with a keyed error; a
|
|
643
|
+
* renderer that accepted either would be a second, looser contract than the
|
|
644
|
+
* one authors are validated against.
|
|
645
|
+
*/
|
|
646
|
+
navigateOnSuccess(block, action, result) {
|
|
647
|
+
// `${result.*}` reads the HANDLER's return value, one level below the
|
|
648
|
+
// action envelope — the same level `readActionPayload` hands the
|
|
649
|
+
// `redirectUrl` convention. Reading `result.data` raw would see the
|
|
650
|
+
// `{ success, data }` envelope on pre-objectstack#3962 servers, which is
|
|
651
|
+
// the "one level too shallow" bug (#2904) in a new place.
|
|
652
|
+
const url = this.interpolateTarget(block.navigate, action, readActionPayload(result.data));
|
|
653
|
+
// `navigate` is author-supplied metadata and lands in a URL position, so
|
|
654
|
+
// it goes through the same scheme guard as every other navigator here.
|
|
655
|
+
if (!this.isValidUrl(url)) {
|
|
656
|
+
console.warn('[ActionRunner] onSuccess.navigate resolved to a URL this runner refuses to open '
|
|
657
|
+
+ '(only http://, https:// and relative URLs are allowed) — no navigation performed.', { action: action.name, navigate: block.navigate, resolved: url });
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const newTab = block.openIn === 'newTab';
|
|
661
|
+
// Same external-URL convention `navigateTo` hands the handler; `newTab`
|
|
662
|
+
// stays the declared choice alone, never `?? isExternal`.
|
|
663
|
+
const external = url.startsWith('http://') || url.startsWith('https://');
|
|
664
|
+
if (this.navigationHandler) {
|
|
665
|
+
this.navigationHandler(url, { external, newTab });
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (newTab) {
|
|
669
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
result.redirect = url;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
599
675
|
/**
|
|
600
676
|
* Execute script action — evaluates client-side expression via ExpressionEvaluator.
|
|
601
677
|
* Supports ${} template expressions referencing data, record, user context.
|
|
@@ -1098,7 +1174,7 @@ export class ActionRunner {
|
|
|
1098
1174
|
* param. Consumers can extend by stuffing extra keys under
|
|
1099
1175
|
* `context.ctx = {...}` before calling `runner.execute()`.
|
|
1100
1176
|
*/
|
|
1101
|
-
interpolateTarget(target, action) {
|
|
1177
|
+
interpolateTarget(target, action, resultScope) {
|
|
1102
1178
|
if (typeof target !== 'string' || target.indexOf('${') === -1)
|
|
1103
1179
|
return target;
|
|
1104
1180
|
// ── The `${param.X}` scope is an INTERNAL runtime value bag, not an
|
|
@@ -1135,10 +1211,21 @@ export class ActionRunner {
|
|
|
1135
1211
|
const params = (action.params && typeof action.params === 'object' && !Array.isArray(action.params))
|
|
1136
1212
|
? action.params
|
|
1137
1213
|
: {};
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1214
|
+
// The scope MAP defines the vocabulary — the pattern is built from its
|
|
1215
|
+
// keys, so a scope that is not in play is not a scope this call recognizes.
|
|
1216
|
+
//
|
|
1217
|
+
// `result` (the handler's own return value) is passed by exactly one caller:
|
|
1218
|
+
// the `ActionSchema.onSuccess` post-success hop, where a response exists.
|
|
1219
|
+
// Every other caller — `executeUrl`, `executeAPI` — interpolates a target
|
|
1220
|
+
// BEFORE the request, so there is no result to read; admitting `${result.*}`
|
|
1221
|
+
// there would widen the authorable surface with nothing behind it and
|
|
1222
|
+
// silently blank the token instead of leaving the author's mistake visible.
|
|
1223
|
+
const scopes = { param: params, ctx: this.buildInterpolationContext() };
|
|
1224
|
+
if (resultScope !== undefined)
|
|
1225
|
+
scopes.result = resultScope;
|
|
1226
|
+
const pattern = new RegExp(`\\$\\{(${Object.keys(scopes).join('|')})\\.([\\w.]+)\\}`, 'g');
|
|
1227
|
+
return target.replace(pattern, (_match, scope, path) => {
|
|
1228
|
+
const value = readPath(scopes[scope], path);
|
|
1142
1229
|
if (value == null)
|
|
1143
1230
|
return '';
|
|
1144
1231
|
return encodeURIComponent(String(value));
|
|
@@ -1207,6 +1294,25 @@ export async function executeAction(action, context = {}) {
|
|
|
1207
1294
|
const runner = new ActionRunner(context);
|
|
1208
1295
|
return await runner.execute(action);
|
|
1209
1296
|
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Is this `onSuccess` the SPEC's navigation block, or this runner's older
|
|
1299
|
+
* chained-callback channel (`ActionDef | ActionDef[]`)?
|
|
1300
|
+
*
|
|
1301
|
+
* The test IS the spec's declaration: a non-array object carrying a STRING
|
|
1302
|
+
* `navigate`. Nothing else can produce that shape — the spec object is strict
|
|
1303
|
+
* with `navigate: z.string()` required, and on a callback `ActionDef`,
|
|
1304
|
+
* `navigate` is the deprecated nested navigation ENVELOPE that
|
|
1305
|
+
* `executeNavigation` reads `to`/`target`/`redirect` off, so a bare string
|
|
1306
|
+
* there has never been runnable.
|
|
1307
|
+
*/
|
|
1308
|
+
export function readOnSuccessNavigation(value) {
|
|
1309
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
1310
|
+
return null;
|
|
1311
|
+
const navigate = value.navigate;
|
|
1312
|
+
if (typeof navigate !== 'string' || navigate === '')
|
|
1313
|
+
return null;
|
|
1314
|
+
return value;
|
|
1315
|
+
}
|
|
1210
1316
|
/**
|
|
1211
1317
|
* Dot-path read used by target interpolation. Plain reduce — kept inline
|
|
1212
1318
|
* to avoid pulling lodash for a 3-line helper.
|
|
@@ -106,9 +106,10 @@
|
|
|
106
106
|
/**
|
|
107
107
|
* Every property `ActionDef` declares, in declaration order.
|
|
108
108
|
*
|
|
109
|
-
* Kept as data because TypeScript types do not survive to runtime —
|
|
110
|
-
*
|
|
111
|
-
*
|
|
109
|
+
* Kept as data because TypeScript types do not survive to runtime — `keyof
|
|
110
|
+
* ActionDef` is erased along with the interface, so it cannot hand a runtime
|
|
111
|
+
* classifier a list of anything. `actionKeys.pin.test.ts` re-derives this one
|
|
112
|
+
* from the interface's AST, which is what keeps the data true.
|
|
112
113
|
*/
|
|
113
114
|
export declare const ACTION_DEF_KEYS: readonly ["type", "actionType", "name", "label", "description", "confirmText", "confirm", "condition", "disabled", "api", "endpoint", "method", "navigate", "to", "external", "newTab", "replace", "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"];
|
|
114
115
|
/**
|
|
@@ -125,7 +126,7 @@ export declare const ACTION_DEF_KEYS: readonly ["type", "actionType", "name", "l
|
|
|
125
126
|
* test and wrong in shipped code — `@object-ui/types` made the same call for
|
|
126
127
|
* `ActionComponent`. The pin test does the walk and fails the day this drifts.
|
|
127
128
|
*/
|
|
128
|
-
export declare const SPEC_ACTION_KEYS: readonly ["_lock", "_lockDocsUrl", "_lockReason", "_lockSource", "_packageId", "_packageVersion", "_provenance", "ai", "aria", "body", "bodyExtra", "bodyShape", "bulkEnabled", "component", "confirmText", "description", "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"];
|
|
129
|
+
export declare const SPEC_ACTION_KEYS: readonly ["_lock", "_lockDocsUrl", "_lockReason", "_lockSource", "_packageId", "_packageVersion", "_provenance", "ai", "aria", "body", "bodyExtra", "bodyShape", "bulkEnabled", "component", "confirmText", "description", "disabled", "errorMessage", "execute", "icon", "label", "locations", "method", "mode", "name", "newTabUrl", "objectName", "onSuccess", "openIn", "opensInNewTab", "order", "params", "recordIdField", "recordIdParam", "refreshAfter", "requiredPermissions", "requiresFeature", "resultDialog", "shortcut", "successMessage", "target", "type", "undoable", "variant", "visible"];
|
|
129
130
|
/**
|
|
130
131
|
* The `navigation` alias's own spelling of a target, read off the action itself
|
|
131
132
|
* when no nested `navigate` object is present (`ActionRunner.executeNavigation`).
|
|
@@ -137,6 +138,54 @@ export declare const SPEC_ACTION_KEYS: readonly ["_lock", "_lockDocsUrl", "_lock
|
|
|
137
138
|
* runner itself supports.
|
|
138
139
|
*/
|
|
139
140
|
export declare const NAVIGATION_ALIAS_KEYS: readonly ["to", "external", "newTab", "replace"];
|
|
141
|
+
/**
|
|
142
|
+
* Keys a HOST composes on the DISPATCH OBJECT at dispatch time — chrome the
|
|
143
|
+
* runtime reads once on its way to the dialog, that no author ever writes and
|
|
144
|
+
* no store ever holds.
|
|
145
|
+
*
|
|
146
|
+
* Distinct from every other list in this module, and the distinction is the
|
|
147
|
+
* whole point: each of the three lists above restates one AUTHORED surface —
|
|
148
|
+
* what `ActionDef` itself declares, what the spec's `ActionSchema` declares,
|
|
149
|
+
* and the `navigation` alias's objectui dialect — so a key joining one of them
|
|
150
|
+
* is a key an author may legally write. This list claims no such derivation and
|
|
151
|
+
* must not acquire one: a key here is the opposite, and writing it in metadata
|
|
152
|
+
* is still a compile error that must stay one. Maintainer ruling
|
|
153
|
+
* 2026-08-22 fixes both halves of that: ⛔ the key is NOT declared on
|
|
154
|
+
* `ActionDef` and ⛔ NOT added to {@link ACTION_DEF_KEYS}; it is declared at the
|
|
155
|
+
* seam, on `packages/app-shell/src/consoleActionDispatch.ts`'s
|
|
156
|
+
* `ConsoleActionDispatch`, and appears HERE only so the dev-mode warning below
|
|
157
|
+
* stops calling it unknown.
|
|
158
|
+
*
|
|
159
|
+
* Why the warning has to know: `classifyActionKeys` sees the object the runner
|
|
160
|
+
* was handed, which is the DISPATCH, not the stored metadata row. With the key
|
|
161
|
+
* absent from the inventory, `warnOnUnknownActionKeys` told the author that a
|
|
162
|
+
* key TWO files read is one "no reader recognizes", and prescribed promoting it
|
|
163
|
+
* to `ActionDef` — the one shape the ruling forbids for it. A diagnostic that
|
|
164
|
+
* fires on the product's own privileged path is how a console gets muted.
|
|
165
|
+
*
|
|
166
|
+
* {@link HOST_STASHED_PARAM_KEYS} below is the same category one level down —
|
|
167
|
+
* host-composed keys inside `params` rather than on the action itself — and
|
|
168
|
+
* cites `DeclaredActionsBar` by name for the same reason this does. Two lists
|
|
169
|
+
* because the two warnings ask different questions of different objects, not
|
|
170
|
+
* because the categories differ.
|
|
171
|
+
*
|
|
172
|
+
* `overrideNotice` — the privileged-override safety copy (objectui#5178),
|
|
173
|
+
* naming the approvers about to be bypassed.
|
|
174
|
+
* - producer: `packages/app-shell/src/views/DeclaredActionsBar.tsx`, on the
|
|
175
|
+
* `can_act:false && can_override:true` branch.
|
|
176
|
+
* - readers: `packages/app-shell/src/hooks/useConsoleActionRuntime.tsx` and
|
|
177
|
+
* `packages/app-shell/src/views/RecordDetailView.tsx`, the console's two
|
|
178
|
+
* param-collection handlers, which render it ahead of the declared
|
|
179
|
+
* description in the dialog's subtitle.
|
|
180
|
+
*
|
|
181
|
+
* Anything added here must satisfy all three of: composed by a host in code,
|
|
182
|
+
* never read back out of stored metadata, and read by the runtime. A key an
|
|
183
|
+
* AUTHOR is meant to write belongs on `ActionDef` — or, when the spec owns it,
|
|
184
|
+
* in `@objectstack/spec` first — and not in this list. `actionKeys.pin.test.ts`
|
|
185
|
+
* pins the contents exactly, so growing the set is a deliberate, reviewed edit
|
|
186
|
+
* rather than a quiet one.
|
|
187
|
+
*/
|
|
188
|
+
export declare const HOST_DISPATCH_ACTION_KEYS: readonly ["overrideNotice"];
|
|
140
189
|
/**
|
|
141
190
|
* Keys the spec has TOMBSTONED: still present in `ActionSchema` so the parser can
|
|
142
191
|
* reject them BY NAME with a rename prescription, rather than fail with a bare
|
|
@@ -156,7 +205,16 @@ export declare const NAVIGATION_ALIAS_KEYS: readonly ["to", "external", "newTab"
|
|
|
156
205
|
* a tombstone the spec keeps only in order to reject it by name.
|
|
157
206
|
*/
|
|
158
207
|
export declare const RETIRED_ACTION_KEYS: Readonly<Record<string, string>>;
|
|
159
|
-
/**
|
|
208
|
+
/**
|
|
209
|
+
* Every key an action may legitimately carry today — authored or host-composed.
|
|
210
|
+
*
|
|
211
|
+
* Four inputs, three of them mirrors of an authored surface and the fourth
|
|
212
|
+
* ({@link HOST_DISPATCH_ACTION_KEYS}) the host-composed dispatch chrome. The
|
|
213
|
+
* union is what `classifyActionKeys` consults, and it is deliberately wider
|
|
214
|
+
* than the authored surface: the runner classifies the object it was HANDED,
|
|
215
|
+
* and a host hands it a dispatch. Membership here grants nothing to an author —
|
|
216
|
+
* `ActionDef` is what decides that, and it is closed.
|
|
217
|
+
*/
|
|
160
218
|
export declare const KNOWN_ACTION_KEYS: ReadonlySet<string>;
|
|
161
219
|
/** Split an action's own keys into the two things worth saying out loud. */
|
|
162
220
|
export declare function classifyActionKeys(action: object | null | undefined): {
|
|
@@ -169,9 +227,11 @@ export declare function resetActionKeyWarnings(): void;
|
|
|
169
227
|
* Dev-mode only: name the keys an action carries that no reader will ever look
|
|
170
228
|
* at. Non-breaking by construction — it changes no types and rejects nothing.
|
|
171
229
|
*
|
|
172
|
-
* Silently binding no handler is the #2169 "Mark Done does nothing" shape
|
|
173
|
-
*
|
|
174
|
-
*
|
|
230
|
+
* Silently binding no handler is the #2169 "Mark Done does nothing" shape. The
|
|
231
|
+
* compiler now catches it in action literals AUTHORED IN CODE — objectstack#4075
|
|
232
|
+
* step 3 closed `ActionDef` — but not in the actions that arrive as DATA, which
|
|
233
|
+
* reach the runner unparsed and which no compiler ever looked at. This is the
|
|
234
|
+
* audible half for that population; see this module's header.
|
|
175
235
|
*/
|
|
176
236
|
export declare function warnOnUnknownActionKeys(action: object | null | undefined, where?: string): void;
|
|
177
237
|
/**
|