@object-ui/core 17.6.0 → 17.7.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 +6610 -0
- package/README.md +101 -14
- package/dist/actions/ActionEngine.d.ts +4 -2
- package/dist/actions/ActionEngine.js +13 -8
- package/dist/actions/ActionRunner.d.ts +288 -27
- package/dist/actions/ActionRunner.js +443 -63
- package/dist/actions/TransactionManager.d.ts +7 -2
- package/dist/actions/actionKeys.d.ts +9 -7
- package/dist/actions/actionKeys.js +57 -14
- package/dist/actions/actionResponse.js +2 -2
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/actions/recordDelete.d.ts +118 -0
- package/dist/actions/recordDelete.js +112 -0
- package/dist/adapters/ValueDataSource.d.ts +6 -1
- package/dist/adapters/ValueDataSource.js +1090 -98
- package/dist/adapters/batchTransaction.d.ts +1 -1
- package/dist/adapters/batchTransaction.js +1 -1
- package/dist/builder/schema-builder.d.ts +38 -81
- package/dist/builder/schema-builder.js +33 -152
- package/dist/data-scope/ViewDataProvider.d.ts +1 -1
- package/dist/data-scope/ViewDataProvider.js +21 -1
- package/dist/data-scope/element-data-source.d.ts +261 -7
- package/dist/data-scope/element-data-source.js +342 -52
- package/dist/data-scope/index.d.ts +9 -4
- package/dist/data-scope/index.js +9 -4
- package/dist/evaluator/ExpressionEvaluator.d.ts +86 -0
- package/dist/evaluator/ExpressionEvaluator.js +184 -12
- package/dist/evaluator/FormulaFunctions.d.ts +0 -10
- package/dist/evaluator/FormulaFunctions.js +166 -27
- package/dist/evaluator/declaredPredicate.d.ts +93 -9
- package/dist/evaluator/declaredPredicate.js +117 -16
- package/dist/evaluator/fieldRules.d.ts +88 -4
- package/dist/evaluator/fieldRules.js +203 -31
- package/dist/evaluator/index.d.ts +1 -0
- package/dist/evaluator/index.js +1 -0
- package/dist/evaluator/listConditional.d.ts +103 -34
- package/dist/evaluator/listConditional.js +127 -50
- package/dist/evaluator/optionRules.d.ts +77 -6
- package/dist/evaluator/optionRules.js +87 -10
- package/dist/evaluator/predicateInput.d.ts +15 -4
- package/dist/evaluator/predicateInput.js +35 -2
- package/dist/evaluator/rowPredicateCanon.d.ts +7 -18
- package/dist/evaluator/rowPredicateCanon.js +65 -115
- package/dist/evaluator/subjectPermissions.d.ts +76 -0
- package/dist/evaluator/subjectPermissions.js +44 -0
- package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
- package/dist/evaluator/unevaluablePredicate.js +70 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +80 -2
- package/dist/protocols/index.d.ts +0 -1
- package/dist/protocols/index.js +9 -1
- package/dist/registry/Registry.d.ts +358 -31
- package/dist/registry/Registry.js +263 -11
- package/dist/registry/WidgetRegistry.js +7 -3
- package/dist/registry/html-tier-intrinsics.d.ts +90 -0
- package/dist/registry/html-tier-intrinsics.js +110 -0
- package/dist/registry/public-blocks.d.ts +8 -0
- package/dist/registry/public-blocks.js +57 -0
- package/dist/theme/ThemeEngine.js +1 -1
- package/dist/utils/chart-category-key.d.ts +82 -0
- package/dist/utils/chart-category-key.js +84 -0
- package/dist/utils/chart-measure-key.d.ts +64 -0
- package/dist/utils/chart-measure-key.js +65 -0
- package/dist/utils/chart-presentation.d.ts +95 -112
- package/dist/utils/chart-presentation.js +169 -104
- package/dist/utils/chart-series.d.ts +131 -5
- package/dist/utils/chart-series.js +98 -16
- package/dist/utils/column-sortability.d.ts +157 -0
- package/dist/utils/column-sortability.js +217 -0
- package/dist/utils/dashboard-filters.d.ts +38 -6
- package/dist/utils/dashboard-filters.js +107 -61
- package/dist/utils/dataset-format.d.ts +211 -30
- package/dist/utils/dataset-format.js +389 -46
- package/dist/utils/date-display.d.ts +354 -0
- package/dist/utils/date-display.js +463 -0
- package/dist/utils/date-macros.js +7 -0
- package/dist/utils/drill-down.d.ts +78 -0
- package/dist/utils/drill-down.js +93 -0
- package/dist/utils/emptiness.d.ts +96 -0
- package/dist/utils/emptiness.js +101 -0
- package/dist/utils/expand-fields.d.ts +76 -6
- package/dist/utils/expand-fields.js +76 -6
- package/dist/utils/extract-records.d.ts +65 -5
- package/dist/utils/extract-records.js +65 -8
- package/dist/utils/filter-converter.d.ts +152 -4
- package/dist/utils/filter-converter.js +1092 -22
- package/dist/utils/filter-tokens.d.ts +49 -5
- package/dist/utils/filter-tokens.js +105 -33
- package/dist/utils/grouping-fields.d.ts +61 -0
- package/dist/utils/grouping-fields.js +84 -0
- package/dist/utils/managedBy.d.ts +3 -3
- package/dist/utils/managedBy.js +5 -5
- package/dist/utils/native-date-value.d.ts +48 -0
- package/dist/utils/native-date-value.js +167 -0
- package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
- package/dist/utils/non-grid-row-ceiling.js +141 -0
- package/dist/utils/normalize-list-view.d.ts +121 -1
- package/dist/utils/normalize-list-view.js +186 -6
- package/dist/utils/number-display.d.ts +63 -14
- package/dist/utils/number-display.js +34 -12
- package/dist/utils/object-aggregate-query.d.ts +113 -0
- package/dist/utils/object-aggregate-query.js +50 -0
- package/dist/utils/parent-scope.d.ts +123 -0
- package/dist/utils/parent-scope.js +140 -0
- package/dist/utils/predicate-fields.d.ts +45 -12
- package/dist/utils/predicate-fields.js +137 -12
- package/dist/utils/record-color.d.ts +75 -0
- package/dist/utils/record-color.js +65 -0
- package/dist/utils/record-source.d.ts +298 -0
- package/dist/utils/record-source.js +331 -0
- package/dist/utils/record-title.d.ts +148 -8
- package/dist/utils/record-title.js +220 -26
- package/dist/utils/reference-keys.d.ts +21 -22
- package/dist/utils/reference-keys.js +532 -29
- package/dist/utils/sort-query.d.ts +115 -15
- package/dist/utils/sort-query.js +240 -13
- package/dist/utils/text-comparand.d.ts +92 -0
- package/dist/utils/text-comparand.js +99 -0
- package/dist/utils/without-denied-fields.d.ts +49 -0
- package/dist/utils/without-denied-fields.js +52 -0
- package/dist/validation/index.d.ts +8 -4
- package/dist/validation/index.js +8 -4
- package/dist/validation/schema-validator.d.ts +1 -1
- package/dist/validation/schema-validator.js +200 -82
- package/dist/validation/validators/object-validation-engine.d.ts +1 -1
- package/dist/validation/validators/object-validation-engine.js +14 -3
- package/package.json +8 -9
- package/dist/data-scope/DataScopeManager.d.ts +0 -127
- package/dist/data-scope/DataScopeManager.js +0 -229
- package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
- package/dist/protocols/ResponsiveProtocol.js +0 -158
- package/dist/validation/validation-engine.d.ts +0 -88
- package/dist/validation/validation-engine.js +0 -434
|
@@ -10,6 +10,47 @@ import { hasDeclaredPredicate } from '../evaluator/declaredPredicate.js';
|
|
|
10
10
|
import { globalUndoManager } from './UndoManager.js';
|
|
11
11
|
import { warnOnDeprecatedObjectParams, warnOnUnknownActionKeys } from './actionKeys.js';
|
|
12
12
|
import { readActionPayload } from './actionResponse.js';
|
|
13
|
+
import { toPredicateRecord } from '../utils/predicate-record.js';
|
|
14
|
+
/**
|
|
15
|
+
* The text the runner writes itself, and so the only text
|
|
16
|
+
* {@link ActionRunner.setTranslator}'s translator is asked for. In each entry
|
|
17
|
+
* `key` names the locale-pack entry (`@object-ui/i18n`'s packs define it) and
|
|
18
|
+
* `defaultValue` is the English source, which is also what shows when no
|
|
19
|
+
* translator is installed.
|
|
20
|
+
*
|
|
21
|
+
* - `completedSuccessfully` — the success toast when the author supplied no
|
|
22
|
+
* copy for it: no `outcomeMessages` entry for the answer's `outcome` and no
|
|
23
|
+
* `successMessage` (objectui#10900, objectui#11344).
|
|
24
|
+
* - `failed` — the error toast when the error that reached the toast carries
|
|
25
|
+
* no readable message, and a parallel chain's result when its last action
|
|
26
|
+
* rejected (objectui#10969).
|
|
27
|
+
* - `parallelFailed` — a parallel chain's error when no failed action reported
|
|
28
|
+
* one of its own (objectui#10969).
|
|
29
|
+
* - `undo` — the label the runner hands the toast handler for an undoable
|
|
30
|
+
* success toast's Undo affordance (objectui#10969).
|
|
31
|
+
*
|
|
32
|
+
* An author's `outcomeMessages` / `successMessage` / `errorMessage` and an
|
|
33
|
+
* action's own error are never in this table: the host localizes authored copy
|
|
34
|
+
* before dispatch, and the runner only fills in its `${result.*}` tokens.
|
|
35
|
+
*/
|
|
36
|
+
const RUNNER_TEXT = {
|
|
37
|
+
completedSuccessfully: {
|
|
38
|
+
key: 'actions.completedSuccessfully',
|
|
39
|
+
defaultValue: 'Action completed successfully',
|
|
40
|
+
},
|
|
41
|
+
failed: {
|
|
42
|
+
key: 'actions.failed',
|
|
43
|
+
defaultValue: 'Action failed',
|
|
44
|
+
},
|
|
45
|
+
parallelFailed: {
|
|
46
|
+
key: 'actions.parallelFailed',
|
|
47
|
+
defaultValue: 'One or more parallel actions failed',
|
|
48
|
+
},
|
|
49
|
+
undo: {
|
|
50
|
+
key: 'actions.undo',
|
|
51
|
+
defaultValue: 'Undo',
|
|
52
|
+
},
|
|
53
|
+
};
|
|
13
54
|
/**
|
|
14
55
|
* Bind the server-CEL-parity `os.user` identity alias into an action context
|
|
15
56
|
* (#2358 trap 1). `@objectstack/spec` documents the canonical CEL identity
|
|
@@ -80,6 +121,24 @@ function readContextObjectName(context) {
|
|
|
80
121
|
const name = context.objectName;
|
|
81
122
|
return typeof name === 'string' && name.trim() !== '' ? name.trim() : undefined;
|
|
82
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* The field definitions the host published for `objectName`, or `undefined`
|
|
126
|
+
* (objectui#11122).
|
|
127
|
+
*
|
|
128
|
+
* A host publishes `objectFields` BESIDE `objectName` (`useConsoleActionRuntime`
|
|
129
|
+
* and `RecordDetailView` both do), so the pair names one object and its fields.
|
|
130
|
+
* They are answered only for THAT object: an action that retargets another
|
|
131
|
+
* object (a related-list row on the record page carries the child's
|
|
132
|
+
* `objectName`) gets no field map rather than the host object's, because
|
|
133
|
+
* reading one object's field types against another object's row would decide
|
|
134
|
+
* which of its fields are relations by the wrong schema.
|
|
135
|
+
*/
|
|
136
|
+
function readContextObjectFields(context, objectName) {
|
|
137
|
+
if (readContextObjectName(context) !== objectName)
|
|
138
|
+
return undefined;
|
|
139
|
+
const fields = context.objectFields;
|
|
140
|
+
return fields && typeof fields === 'object' ? fields : undefined;
|
|
141
|
+
}
|
|
83
142
|
/**
|
|
84
143
|
* Whether opening FormView `viewName` from a record of `contextObject` would
|
|
85
144
|
* cross an object boundary (objectui#4292).
|
|
@@ -102,6 +161,91 @@ function crossesObjectBoundary(viewName, contextObject) {
|
|
|
102
161
|
return false;
|
|
103
162
|
return !viewName.startsWith(`${contextObject}.`);
|
|
104
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Is this the declarative single-record field write?
|
|
166
|
+
*
|
|
167
|
+
* Asks the AUTHORED key, not `type`: the spec materializes `type: 'script'`
|
|
168
|
+
* here and refuses every other explicit spelling, so `type` cannot answer the
|
|
169
|
+
* question — see the dispatch comment in {@link ActionRunner.execute}.
|
|
170
|
+
*
|
|
171
|
+
* `'update'` is the only member the spec declares (`'delete'` and `'custom'`
|
|
172
|
+
* have no row-level form), and an unrecognized spelling is a parse rejection
|
|
173
|
+
* upstream. This reads the ONE member rather than "any truthy `operation`" so a
|
|
174
|
+
* future member cannot silently inherit the update write path — the dispatch
|
|
175
|
+
* table's own guard reasoning, applied to this key.
|
|
176
|
+
*/
|
|
177
|
+
function isUpdateOperationAction(action) {
|
|
178
|
+
return action.operation === 'update';
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The fields an `operation: 'update'` action writes, and their prior values.
|
|
182
|
+
*
|
|
183
|
+
* `undoable` promises to restore EXACTLY the fields the action wrote, so the
|
|
184
|
+
* captured set is the effective payload — `patch` merged under the collected
|
|
185
|
+
* `params` — and never `patch` alone: a param-collected field left out of the
|
|
186
|
+
* capture would make Undo restore SOME of the edit and report a full one, which
|
|
187
|
+
* is worse than offering no Undo at all.
|
|
188
|
+
*
|
|
189
|
+
* Prior values come from the row record the invoking surface stashed under
|
|
190
|
+
* `params._rowRecord` (the same client-side stash the record-id dance reads).
|
|
191
|
+
* A field the row carries as `null` is captured as `null`: it was empty, and
|
|
192
|
+
* clearing it back to empty is what restoring it means on the data plane.
|
|
193
|
+
*
|
|
194
|
+
* ⛔ A field the row does not CARRY is never captured as `null`
|
|
195
|
+
* (objectui#10404). Absent is not empty: a list row is projected by `$select`,
|
|
196
|
+
* and a written field no column shows is missing from the row while the server
|
|
197
|
+
* holds a real value for it. Recording `null` there made Undo write `null` over
|
|
198
|
+
* the value it existed to restore. The list harvest (`listViewPredicates`) now
|
|
199
|
+
* asks for the written fields of an `undoable` action, so a projected row
|
|
200
|
+
* carries them; this is the backstop for the ones it cannot ask for (a field
|
|
201
|
+
* the object does not declare or the principal may not read, a row no harvest
|
|
202
|
+
* shaped). "Carries" means an own key whose value is not `undefined`: JSON
|
|
203
|
+
* cannot send `undefined`, so such a value says nothing about what is stored.
|
|
204
|
+
*
|
|
205
|
+
* Answers `undefined` when any written field is not carried, and the caller
|
|
206
|
+
* then offers no Undo at all. Capturing only the carried fields is not an
|
|
207
|
+
* option, for the reason above: a partial restore reported as a full one is
|
|
208
|
+
* worse than no Undo.
|
|
209
|
+
*
|
|
210
|
+
* ⛔ A relation's prior value is its STORED id, never the record `$expand`
|
|
211
|
+
* put in its place (objectui#11122). Surfaces read rows with `$expand` on the
|
|
212
|
+
* relations they show (a grid, its visible and grouping columns; the record
|
|
213
|
+
* page, every relation the reader may read), and the server replaces the id
|
|
214
|
+
* in place with the related record. Copied verbatim, that record became the
|
|
215
|
+
* Undo value and Undo wrote it into the reference slot, which stores an id:
|
|
216
|
+
* refused under a strict value-shape posture, stored as corruption under the
|
|
217
|
+
* lenient one. So each carried value goes through `toPredicateRecord`, the one
|
|
218
|
+
* rule that binds a fetched record the way the server stores it: a field the
|
|
219
|
+
* object DECLARES relational (`EXPANDABLE_FIELD_TYPES`, the set that decides
|
|
220
|
+
* what is expanded in the first place) collapses to its id, element-wise for a
|
|
221
|
+
* `multiple` relation, and every other field is left exactly as the row
|
|
222
|
+
* carries it. It is read from `fields`, never from the value's shape: a `json`
|
|
223
|
+
* field may hold an object with an `id`, and that object is its stored value.
|
|
224
|
+
*
|
|
225
|
+
* `fields` is the written object's field definitions (`objectSchema.fields`,
|
|
226
|
+
* either served shape), and it is REQUIRED so that a caller without them has
|
|
227
|
+
* to say so: `undefined` collapses nothing, because a caller that cannot name
|
|
228
|
+
* the relations must not guess them.
|
|
229
|
+
*
|
|
230
|
+
* Exported (objectui#11082) because it is THE rule, not this runner's: every
|
|
231
|
+
* surface that builds an `update` Undo snapshot itself calls it rather than
|
|
232
|
+
* restating it. Today that is the console runtime's `api` handler
|
|
233
|
+
* (`useConsoleActionRuntime`) and the record page's own `api` handler
|
|
234
|
+
* (`RecordDetailView`), which reads prior values off the page's loaded record;
|
|
235
|
+
* that record lacks a written field when field-level security hides it from
|
|
236
|
+
* the reader. To name the fields that blocked a capture, ask per field:
|
|
237
|
+
* `captureUpdateUndoData([field], rowRecord, fields) === undefined`.
|
|
238
|
+
*/
|
|
239
|
+
export function captureUpdateUndoData(writtenFields, rowRecord, fields) {
|
|
240
|
+
if (!writtenFields.every((field) => rowCarries(rowRecord, field)))
|
|
241
|
+
return undefined;
|
|
242
|
+
const stored = toPredicateRecord(rowRecord, fields);
|
|
243
|
+
return Object.fromEntries(writtenFields.map((field) => [field, stored[field]]));
|
|
244
|
+
}
|
|
245
|
+
/** Whether the row CARRIES `field`: an own key whose value is not `undefined`. */
|
|
246
|
+
function rowCarries(rowRecord, field) {
|
|
247
|
+
return Object.prototype.hasOwnProperty.call(rowRecord, field) && rowRecord[field] !== undefined;
|
|
248
|
+
}
|
|
105
249
|
/*
|
|
106
250
|
* ## The two predicate gates in `execute`, and the one question they ask first
|
|
107
251
|
*
|
|
@@ -129,8 +273,10 @@ function crossesObjectBoundary(viewName, contextObject) {
|
|
|
129
273
|
* Both read {@link hasDeclaredPredicate} — the repo's one definition of that
|
|
130
274
|
* question, in `evaluator/declaredPredicate.ts` beside the normalizer it is
|
|
131
275
|
* derived from, with the scope objectui#3850 ruled on (`''` / whitespace-only /
|
|
132
|
-
* empty-`source` envelope
|
|
133
|
-
*
|
|
276
|
+
* empty-`source` envelope are NOT declared; a declared `false` IS) and the
|
|
277
|
+
* third state objectui#11358 added (a present value with no evaluable `source`
|
|
278
|
+
* — `0`, `{}`, an array, an `ast`-only envelope — IS declared, and faults).
|
|
279
|
+
* This gate arrived here first as a module-private helper with a
|
|
134
280
|
* scope note saying it stayed private until that ruling landed; it has, so the
|
|
135
281
|
* definition moved down a layer and the renderer side reads the same one
|
|
136
282
|
* (`components/renderers/action/visibility-gate.ts` re-exports it as
|
|
@@ -164,9 +310,15 @@ function crossesObjectBoundary(viewName, contextObject) {
|
|
|
164
310
|
* historical `Boolean(raw)` coercion, so `0` there meant "hidden" — fail-CLOSED
|
|
165
311
|
* on junk, the opposite of what this module committed to for `disabled`
|
|
166
312
|
* (`catch { isDisabled = false }`). That filter now reads
|
|
167
|
-
* {@link hasDeclaredPredicate} too (objectui#3957), so
|
|
168
|
-
*
|
|
169
|
-
*
|
|
313
|
+
* {@link hasDeclaredPredicate} too (objectui#3957), so the two entries answer
|
|
314
|
+
* a value that is not a predicate at all the same way. Since objectui#11358
|
|
315
|
+
* that answer is "declared, and faulting" rather than "no gate": `0` / `{}` /
|
|
316
|
+
* an `ast`-only envelope reach `evaluateCondition`, which reports them and
|
|
317
|
+
* answers its fail-soft `true` — so `disabled` refuses the action (its closed
|
|
318
|
+
* direction, as for any faulting `disabled`) and `condition` lets it run (the
|
|
319
|
+
* direction a faulting `condition` has always taken), and the engine filter
|
|
320
|
+
* hides it (`throwOnError`). Pinned in `ActionRunner.disabledGate.test.ts` /
|
|
321
|
+
* `ActionRunner.conditionGate.test.ts`.
|
|
170
322
|
*/
|
|
171
323
|
export class ActionRunner {
|
|
172
324
|
constructor(context = {}) {
|
|
@@ -230,6 +382,12 @@ export class ActionRunner {
|
|
|
230
382
|
writable: true,
|
|
231
383
|
value: void 0
|
|
232
384
|
});
|
|
385
|
+
Object.defineProperty(this, "translate", {
|
|
386
|
+
enumerable: true,
|
|
387
|
+
configurable: true,
|
|
388
|
+
writable: true,
|
|
389
|
+
value: void 0
|
|
390
|
+
});
|
|
233
391
|
/**
|
|
234
392
|
* Built-in dispatch, one entry per runnable action type.
|
|
235
393
|
*
|
|
@@ -267,6 +425,21 @@ export class ActionRunner {
|
|
|
267
425
|
this.navigationHandler = null;
|
|
268
426
|
this.paramCollectionHandler = null;
|
|
269
427
|
this.resultDialogHandler = null;
|
|
428
|
+
this.translate = null;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Set the translator for the text the runner supplies itself — the host's
|
|
432
|
+
* `t`, injected so this package takes no i18n dependency. That text is the
|
|
433
|
+
* `RUNNER_TEXT` table in this file: the generic success toast shown when an
|
|
434
|
+
* action declares neither an `outcomeMessages` entry for the answer nor a
|
|
435
|
+
* `successMessage`, the error fallbacks when no readable error message
|
|
436
|
+
* reached the runner, and the Undo label of an undoable success toast. An
|
|
437
|
+
* author's `outcomeMessages` / `successMessage` / `errorMessage` and an
|
|
438
|
+
* action's own error never go through this translator: the host localizes
|
|
439
|
+
* authored copy before dispatch. With no translator the text stays English.
|
|
440
|
+
*/
|
|
441
|
+
setTranslator(translate) {
|
|
442
|
+
this.translate = translate;
|
|
270
443
|
}
|
|
271
444
|
/**
|
|
272
445
|
* Set a custom confirmation handler (e.g., Shadcn AlertDialog).
|
|
@@ -329,10 +502,10 @@ export class ActionRunner {
|
|
|
329
502
|
try {
|
|
330
503
|
// `ActionDef` accepts any key of any type, so a typo (`targt`) and a
|
|
331
504
|
// retired key (`execute`) both reach here having type-checked. Neither
|
|
332
|
-
// binds a handler, and binding no handler silently is the #2169 "Mark Done
|
|
333
|
-
// does nothing" shape. Dev-only, warn-once, changes nothing (#4075 step 1).
|
|
505
|
+
// binds a handler, and binding no handler silently is the objectstack-ai/objectstack#2169 "Mark Done
|
|
506
|
+
// does nothing" shape. Dev-only, warn-once, changes nothing (objectstack-ai/objectstack#4075 step 1).
|
|
334
507
|
warnOnUnknownActionKeys(action);
|
|
335
|
-
// The compat window for the object-form `params` payload (#5777, maintainer
|
|
508
|
+
// The compat window for the object-form `params` payload (objectstack-ai/objectstack#5777, maintainer
|
|
336
509
|
// ruling 2026-08-06 direction A). Checked HERE, before the param-collection
|
|
337
510
|
// block below rewrites `action.params` into a values map — after that point
|
|
338
511
|
// an action that authored a legitimate ActionParam[] DEFINITION array also
|
|
@@ -376,9 +549,16 @@ export class ActionRunner {
|
|
|
376
549
|
// code only evaluated the STRING form and treated any object as truthy,
|
|
377
550
|
// so an envelope-disabled action was ALWAYS "disabled" — silently
|
|
378
551
|
// blocking every execution (param dialog never opened, handler never
|
|
379
|
-
// ran). `evaluateCondition` already handles boolean/string/envelope
|
|
380
|
-
//
|
|
381
|
-
//
|
|
552
|
+
// ran). `evaluateCondition` already handles boolean/string/envelope.
|
|
553
|
+
//
|
|
554
|
+
// A predicate that FAULTS — an unbound root, a misspelled field,
|
|
555
|
+
// `current_user.can(…)` before the permissions payload has loaded — does
|
|
556
|
+
// NOT reach the `catch` below: `evaluateCondition` handles its own
|
|
557
|
+
// faults and answers its fail-soft `true`, which on this key means
|
|
558
|
+
// DISABLED, so the action is refused ("Action is disabled"). That is the
|
|
559
|
+
// closed direction every `disabled` leg takes (objectui#11212,
|
|
560
|
+
// objectui#11242). The `catch` only covers a throw from outside that
|
|
561
|
+
// fault handling, and leaves such an action not disabled.
|
|
382
562
|
let isDisabled = false;
|
|
383
563
|
try {
|
|
384
564
|
isDisabled = this.evaluator.evaluateCondition(action.disabled);
|
|
@@ -425,6 +605,32 @@ export class ActionRunner {
|
|
|
425
605
|
action.params._rowRecord = rowRecord;
|
|
426
606
|
}
|
|
427
607
|
}
|
|
608
|
+
// ── `operation` is read BEFORE `type`, and this is the only order that
|
|
609
|
+
// can work ────────────────────────────────────────────────────────────
|
|
610
|
+
//
|
|
611
|
+
// `operation: 'update'` is a PARALLEL key beside `type`, not a member of
|
|
612
|
+
// it: `@objectstack/spec` materializes `type: 'script'` on such an action
|
|
613
|
+
// (the platform action route is the script type's own route) and refuses
|
|
614
|
+
// any other explicit `type`. `ActionType` therefore did not gain a
|
|
615
|
+
// member, which is why `builtinExecutors` — a `Record<RunnableActionType,
|
|
616
|
+
// …>` — still compiles unchanged across the bump that added this key.
|
|
617
|
+
//
|
|
618
|
+
// The cost of that design is paid here: by the time `actionType` is
|
|
619
|
+
// consulted, an update action is indistinguishable from every other
|
|
620
|
+
// script action. Dispatched on `type` it would reach the registered
|
|
621
|
+
// `script` handler with its `patch` never merged into the request — the
|
|
622
|
+
// route would be POSTed a payload with no field values in it and answer a
|
|
623
|
+
// green success for having written nothing (objectui#2960). Worse, a row
|
|
624
|
+
// rehydrated from `sys_metadata` UNPARSED (objectui#3903) carries no
|
|
625
|
+
// materialized `type` at all, so `actionType` falls back to the action's
|
|
626
|
+
// NAME, matches no executor, and the action lands in
|
|
627
|
+
// `executeActionSchema`. Reading `operation` first is what makes both of
|
|
628
|
+
// those impossible.
|
|
629
|
+
if (isUpdateOperationAction(action)) {
|
|
630
|
+
const result = await this.executeUpdateOperation(action);
|
|
631
|
+
await this.handlePostExecution(action, result);
|
|
632
|
+
return result;
|
|
633
|
+
}
|
|
428
634
|
// Check for a registered custom handler first
|
|
429
635
|
if (actionType && this.handlers.has(actionType)) {
|
|
430
636
|
const handler = this.handlers.get(actionType);
|
|
@@ -477,13 +683,13 @@ export class ActionRunner {
|
|
|
477
683
|
const firstFail = results.find(r => r.status === 'fulfilled' && !r.value.success);
|
|
478
684
|
return {
|
|
479
685
|
success: false,
|
|
480
|
-
error: firstFail?.value?.error ||
|
|
686
|
+
error: firstFail?.value?.error || this.runnerText(RUNNER_TEXT.parallelFailed),
|
|
481
687
|
};
|
|
482
688
|
}
|
|
483
689
|
const lastResult = results[results.length - 1];
|
|
484
690
|
return lastResult.status === 'fulfilled'
|
|
485
691
|
? lastResult.value
|
|
486
|
-
: { success: false, error:
|
|
692
|
+
: { success: false, error: this.runnerText(RUNNER_TEXT.failed) };
|
|
487
693
|
}
|
|
488
694
|
// Sequential execution — stop on first failure
|
|
489
695
|
let lastResult = { success: true };
|
|
@@ -495,6 +701,61 @@ export class ActionRunner {
|
|
|
495
701
|
}
|
|
496
702
|
return lastResult;
|
|
497
703
|
}
|
|
704
|
+
/**
|
|
705
|
+
* One entry of `RUNNER_TEXT`, in the installed translator's language. An
|
|
706
|
+
* empty answer from the translator falls back to the English source rather
|
|
707
|
+
* than raising an empty toast or an empty label.
|
|
708
|
+
*/
|
|
709
|
+
runnerText({ key, defaultValue }) {
|
|
710
|
+
return this.translate?.(key, { defaultValue }) || defaultValue;
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* The success toast's text, by the contract's three rungs
|
|
714
|
+
* (`ActionSchema.outcomeMessages`, objectui#11344):
|
|
715
|
+
*
|
|
716
|
+
* 1. `outcomeMessages[outcome]`, where `outcome` is the top-level `outcome`
|
|
717
|
+
* of the handler's return value — the payload `${result.*}` reads, so it
|
|
718
|
+
* is the same fact an author writes as `${result.outcome}`;
|
|
719
|
+
* 2. `successMessage`;
|
|
720
|
+
* 3. the runner's own default text, in its translator's language.
|
|
721
|
+
*
|
|
722
|
+
* Rungs 1 and 2 are the author's copy. Both arrive already localized — the
|
|
723
|
+
* host's localizer resolves the bundle entry or the inline `I18nLabel` map
|
|
724
|
+
* before dispatch — and both have their `${result.*}` tokens filled in here,
|
|
725
|
+
* through the one token grammar and scope `onSuccess.navigate` reads
|
|
726
|
+
* (`substituteScopeTokens` over `readActionPayload(result.data)`). Only the
|
|
727
|
+
* sink differs: a toast is text, so a value is inserted as-is, where a URL
|
|
728
|
+
* position percent-encodes it.
|
|
729
|
+
*
|
|
730
|
+
* ⛔ `result.data.message` is not a rung. It used to outrank both rungs of
|
|
731
|
+
* author copy, which put whatever language the server happened to write in
|
|
732
|
+
* front of the user's locale; the contract names no server-message rung, and
|
|
733
|
+
* an author who wants the server's sentence can still write
|
|
734
|
+
* `${result.message}` as the copy.
|
|
735
|
+
*
|
|
736
|
+
* A rung counts only when it is a non-empty string. An `I18nLabel` map that
|
|
737
|
+
* reached the runner without passing through a localizer has no language to
|
|
738
|
+
* resolve against here, so it is skipped rather than handed to the toast as
|
|
739
|
+
* an object — the React #31 crash the error branch below guards against.
|
|
740
|
+
*/
|
|
741
|
+
composeSuccessMessage(action, result) {
|
|
742
|
+
const payload = readActionPayload(result.data);
|
|
743
|
+
const outcome = payload && typeof payload === 'object'
|
|
744
|
+
? payload.outcome
|
|
745
|
+
: undefined;
|
|
746
|
+
const outcomeMessages = action.outcomeMessages;
|
|
747
|
+
const outcomeCopy = typeof outcome === 'string'
|
|
748
|
+
&& outcomeMessages && typeof outcomeMessages === 'object'
|
|
749
|
+
&& Object.prototype.hasOwnProperty.call(outcomeMessages, outcome)
|
|
750
|
+
? outcomeMessages[outcome]
|
|
751
|
+
: undefined;
|
|
752
|
+
for (const copy of [outcomeCopy, action.successMessage]) {
|
|
753
|
+
if (typeof copy === 'string' && copy.trim() !== '') {
|
|
754
|
+
return substituteScopeTokens(copy, { result: payload }, (value) => value);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return this.runnerText(RUNNER_TEXT.completedSuccessfully);
|
|
758
|
+
}
|
|
498
759
|
/**
|
|
499
760
|
* Post-execution: emit toast notifications, handle chaining, callbacks.
|
|
500
761
|
*/
|
|
@@ -510,27 +771,25 @@ export class ActionRunner {
|
|
|
510
771
|
const showToast = action.toast ?? { showOnSuccess: true, showOnError: true };
|
|
511
772
|
const duration = action.toast?.duration;
|
|
512
773
|
if (result.success && !hasResultDialog && !result.silent && showToast.showOnSuccess !== false) {
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
|
|
516
|
-
// ("2 app updates available: CRM 1.0.0→1.0.1", "Published v1.2.0")
|
|
517
|
-
// that the static label can't express; without this the user only ever
|
|
518
|
-
// sees a generic "Done". Falls back to the static label, then a default.
|
|
519
|
-
const dyn = (result.data && typeof result.data === 'object'
|
|
520
|
-
&& typeof result.data.message === 'string')
|
|
521
|
-
? String(result.data.message).trim()
|
|
522
|
-
: '';
|
|
523
|
-
const message = dyn || action.successMessage || 'Action completed successfully';
|
|
774
|
+
// The server returns FACTS and the console writes the sentence
|
|
775
|
+
// (ruling A on objectstack-ai/cloud#2315) — see `composeSuccessMessage`.
|
|
776
|
+
const message = this.composeSuccessMessage(action, result);
|
|
524
777
|
// Undoable action: register the captured operation on the global
|
|
525
778
|
// UndoManager and surface an "Undo" affordance on the toast (the
|
|
526
|
-
// consumer's toast handler wires the button to UndoManager).
|
|
779
|
+
// consumer's toast handler wires the button to UndoManager). The
|
|
780
|
+
// button's label is the runner's own text, so it is translated here
|
|
781
|
+
// rather than left to each handler's English default (objectui#10969).
|
|
527
782
|
if (result.undo) {
|
|
528
783
|
try {
|
|
529
784
|
globalUndoManager.push(result.undo);
|
|
530
785
|
}
|
|
531
786
|
catch { /* non-fatal */ }
|
|
532
787
|
}
|
|
533
|
-
this.toastHandler(message, {
|
|
788
|
+
this.toastHandler(message, {
|
|
789
|
+
type: 'success',
|
|
790
|
+
duration,
|
|
791
|
+
undo: result.undo ? { label: this.runnerText(RUNNER_TEXT.undo) } : undefined,
|
|
792
|
+
});
|
|
534
793
|
}
|
|
535
794
|
if (!result.success && showToast.showOnError !== false && result.error) {
|
|
536
795
|
// `result.error` is typed as a string, but a handler bug can leak an
|
|
@@ -543,7 +802,7 @@ export class ActionRunner {
|
|
|
543
802
|
? raw
|
|
544
803
|
: (raw && typeof raw.message === 'string')
|
|
545
804
|
? raw.message
|
|
546
|
-
:
|
|
805
|
+
: this.runnerText(RUNNER_TEXT.failed);
|
|
547
806
|
this.toastHandler(message, { type: 'error', duration });
|
|
548
807
|
}
|
|
549
808
|
}
|
|
@@ -586,34 +845,28 @@ export class ActionRunner {
|
|
|
586
845
|
}
|
|
587
846
|
// ── ActionSchema.onSuccess — post-success navigation ────────────────────
|
|
588
847
|
//
|
|
589
|
-
//
|
|
848
|
+
// `053fdc8f9`, the console half of objectstack#9566/#9474. The spec
|
|
590
849
|
// declares `onSuccess` as a CLOSED STRICT object
|
|
591
850
|
// `{ navigate: string, openIn: 'self' | 'newTab' }`, refine-scoped to
|
|
592
851
|
// `type: 'api'` and `type: 'script'` — the two types whose success event
|
|
593
852
|
// carries a server response for `${result.*}` to read.
|
|
594
853
|
//
|
|
595
|
-
//
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
//
|
|
599
|
-
//
|
|
600
|
-
//
|
|
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.
|
|
854
|
+
// That declared meaning is the key's ONLY meaning. The runner's older
|
|
855
|
+
// chained-callback channel (`onSuccess?: ActionDef | ActionDef[]`,
|
|
856
|
+
// dispatched through `executeChain`) was retired by objectui#5934
|
|
857
|
+
// (maintainer ruling 2026-08-31): the spec strict-refuses a callback shape
|
|
858
|
+
// at parse, so no validated metadata could ever reach it, and the census
|
|
859
|
+
// found zero producers outside the channel's own pins.
|
|
603
860
|
//
|
|
604
|
-
//
|
|
605
|
-
//
|
|
606
|
-
//
|
|
607
|
-
//
|
|
861
|
+
// `readOnSuccessNavigation` stays as the shape guard, not as a
|
|
862
|
+
// discriminator: stored rows are rehydrated UNPARSED (objectstack-ai/objectstack#3903), so the value
|
|
863
|
+
// is still read as data, and a shape the spec refuses gets no reading —
|
|
864
|
+
// no navigation, no callback dispatch, no lenient fallback.
|
|
608
865
|
if (result.success && action.onSuccess) {
|
|
609
866
|
const navigation = readOnSuccessNavigation(action.onSuccess);
|
|
610
867
|
if (navigation) {
|
|
611
868
|
this.navigateOnSuccess(navigation, action, result);
|
|
612
869
|
}
|
|
613
|
-
else {
|
|
614
|
-
const callbacks = Array.isArray(action.onSuccess) ? action.onSuccess : [action.onSuccess];
|
|
615
|
-
await this.executeChain(callbacks, 'sequential');
|
|
616
|
-
}
|
|
617
870
|
}
|
|
618
871
|
if (!result.success && action.onFailure) {
|
|
619
872
|
const callbacks = Array.isArray(action.onFailure) ? action.onFailure : [action.onFailure];
|
|
@@ -672,15 +925,122 @@ export class ActionRunner {
|
|
|
672
925
|
result.redirect = url;
|
|
673
926
|
}
|
|
674
927
|
}
|
|
928
|
+
/**
|
|
929
|
+
* `operation: 'update'` — the declarative single-record field write.
|
|
930
|
+
*
|
|
931
|
+
* ## The write is the ROUTE's, never this client's
|
|
932
|
+
*
|
|
933
|
+
* This method composes a request and hands it to the registered `script`
|
|
934
|
+
* dispatch; it does not touch a `DataSource`. That is a security boundary,
|
|
935
|
+
* not an implementation preference. `POST /api/v1/actions/{object}/{action}`
|
|
936
|
+
* runs the write AS THE CALLER — the caller's permissions, the object's hooks
|
|
937
|
+
* and its validations all fire as for a user edit — while a client-side
|
|
938
|
+
* `dataSource.update()` reaches the data plane having consulted none of them.
|
|
939
|
+
* The spec says the same thing from its end by refusing `target`, `body`,
|
|
940
|
+
* `method` and `bodyExtra` beside this key and pinning `type` to `'script'`:
|
|
941
|
+
* there is exactly one execution surface for an update action, and it is the
|
|
942
|
+
* route.
|
|
943
|
+
*
|
|
944
|
+
* Consequently, a consumer that has registered NO server-action dispatch gets
|
|
945
|
+
* a loud refusal naming the remedy. The two silent alternatives were both
|
|
946
|
+
* available and both wrong: falling through to `executeScript` ends at "No
|
|
947
|
+
* script provided" (an update action declares no `target` — the author is
|
|
948
|
+
* sent hunting for a field they were right not to write), and falling through
|
|
949
|
+
* to `executeActionSchema` reports on an action that never ran.
|
|
950
|
+
*
|
|
951
|
+
* ## `patch` rides UNDER the collected params
|
|
952
|
+
*
|
|
953
|
+
* The spec's words: static field values "merged UNDER the user-supplied
|
|
954
|
+
* `params` so a fixed value can be declared without exposing it in the
|
|
955
|
+
* dialog". So a collected param of the same name WINS — the author declared a
|
|
956
|
+
* default, and the user answered it.
|
|
957
|
+
*/
|
|
958
|
+
async executeUpdateOperation(action) {
|
|
959
|
+
const dispatch = this.handlers.get('script');
|
|
960
|
+
if (!dispatch) {
|
|
961
|
+
return {
|
|
962
|
+
success: false,
|
|
963
|
+
error: 'An `operation: "update"` action is written by the platform action route, which this ' +
|
|
964
|
+
'client runner cannot reach: no `script` handler is registered. The write is never ' +
|
|
965
|
+
'performed client-side — a direct data-plane update bypasses the route\'s permission ' +
|
|
966
|
+
'floor, the object\'s hooks and its validations. Register a handler that POSTs to ' +
|
|
967
|
+
'/api/v1/actions/{object}/{action} — build one with createServerActionHandler from ' +
|
|
968
|
+
'@object-ui/core.',
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
const collected = (action.params && !Array.isArray(action.params))
|
|
972
|
+
? { ...action.params }
|
|
973
|
+
: {};
|
|
974
|
+
const patch = (action.patch && typeof action.patch === 'object' && !Array.isArray(action.patch))
|
|
975
|
+
? { ...action.patch }
|
|
976
|
+
: {};
|
|
977
|
+
// `patch` UNDER the collected params — see the doc above.
|
|
978
|
+
const params = { ...patch, ...collected };
|
|
979
|
+
// The row stash and the record selector are transport mechanics, not
|
|
980
|
+
// written fields: the dispatch strips `_rowRecord` before POSTing and reads
|
|
981
|
+
// `recordId` as the record's address.
|
|
982
|
+
const rowRecord = (collected._rowRecord && typeof collected._rowRecord === 'object')
|
|
983
|
+
? collected._rowRecord
|
|
984
|
+
: undefined;
|
|
985
|
+
const writtenFields = Object.keys(params).filter((k) => k !== '_rowRecord' && k !== 'recordId');
|
|
986
|
+
const result = await dispatch({ ...action, params }, this.context);
|
|
987
|
+
// A refusal is returned exactly as the dispatch reported it. The route's
|
|
988
|
+
// envelope rule (`interpretActionResponse`) has already turned a 4xx/5xx
|
|
989
|
+
// AND a 200-with-`success:false` business rejection into `error`, and
|
|
990
|
+
// `handlePostExecution` surfaces that as the failure toast. Re-labelling it
|
|
991
|
+
// here, or answering success because the POST itself completed, is the
|
|
992
|
+
// objectui#2960 shape this branch exists to prevent.
|
|
993
|
+
if (!result.success)
|
|
994
|
+
return result;
|
|
995
|
+
// Undo: prior values of exactly the fields this action wrote. Needs a row
|
|
996
|
+
// record to read them from — without one there is nothing to restore, so
|
|
997
|
+
// the affordance is correctly not offered rather than offered empty. The
|
|
998
|
+
// same holds when the row lacks a written field (objectui#10404): no Undo,
|
|
999
|
+
// never one that writes `null` over the stored value. A relation the row
|
|
1000
|
+
// carries expanded is captured as its stored id, read against the field
|
|
1001
|
+
// map the host published for this object (objectui#11122).
|
|
1002
|
+
if (action.undoable && rowRecord && writtenFields.length > 0) {
|
|
1003
|
+
const objectName = action.objectName || readContextObjectName(this.context);
|
|
1004
|
+
const recordId = collected.recordId ?? rowRecord.id;
|
|
1005
|
+
if (objectName && recordId != null) {
|
|
1006
|
+
const undoData = captureUpdateUndoData(writtenFields, rowRecord, readContextObjectFields(this.context, objectName));
|
|
1007
|
+
if (undoData) {
|
|
1008
|
+
result.undo = {
|
|
1009
|
+
id: `undo-${objectName}-${String(recordId)}-${Date.now()}`,
|
|
1010
|
+
type: 'update',
|
|
1011
|
+
objectName,
|
|
1012
|
+
recordId: String(recordId),
|
|
1013
|
+
timestamp: Date.now(),
|
|
1014
|
+
// objectui#11080 — no English verb here. The toast that reports the
|
|
1015
|
+
// Undo / Redo already says which one it is, from a pack key, so an
|
|
1016
|
+
// action that declared no label is named by the object it acted on.
|
|
1017
|
+
description: action.label || objectName,
|
|
1018
|
+
undoData,
|
|
1019
|
+
redoData: Object.fromEntries(writtenFields.map((k) => [k, params[k]])),
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
else {
|
|
1023
|
+
// The success toast then carries no Undo button, which is how the
|
|
1024
|
+
// user learns this one cannot be undone; this names the cause for
|
|
1025
|
+
// the author.
|
|
1026
|
+
console.warn('[ActionRunner] `undoable` update succeeded but offers no Undo: the row it ran on does not '
|
|
1027
|
+
+ 'carry every field it wrote, so their prior values are unknown and an Undo would overwrite '
|
|
1028
|
+
+ 'stored data. A list row carries a written field when the object declares it and the '
|
|
1029
|
+
+ 'principal may read it.', { action: action.name, missing: writtenFields.filter((k) => !rowCarries(rowRecord, k)) });
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
return result;
|
|
1034
|
+
}
|
|
675
1035
|
/**
|
|
676
1036
|
* Execute script action — evaluates client-side expression via ExpressionEvaluator.
|
|
677
1037
|
* Supports ${} template expressions referencing data, record, user context.
|
|
678
1038
|
*/
|
|
679
1039
|
async executeScript(action) {
|
|
680
1040
|
// `target` is the only handler slot. The `execute` alias was removed in
|
|
681
|
-
// @objectstack/spec 17 (#3855), which rejects an authored `execute` at parse
|
|
1041
|
+
// @objectstack/spec 17 (objectstack-ai/objectstack#3855), which rejects an authored `execute` at parse
|
|
682
1042
|
// with the rename prescription — so parsed metadata cannot carry it and a
|
|
683
|
-
// `target || execute` fallback could only ever evaluate to `target` (#3856).
|
|
1043
|
+
// `target || execute` fallback could only ever evaluate to `target` (objectstack-ai/objectstack#3856).
|
|
684
1044
|
const script = action.target;
|
|
685
1045
|
if (!script) {
|
|
686
1046
|
// A spec `body` IS a script — this runner just cannot run one (see the
|
|
@@ -703,11 +1063,11 @@ export class ActionRunner {
|
|
|
703
1063
|
// `ActionDef` no longer declares `execute` — nor any open index signature
|
|
704
1064
|
// — so `tsc` now catches the retired key at every site that AUTHORS an
|
|
705
1065
|
// action literal in code. That is the half of the problem the type can
|
|
706
|
-
// reach. It is not this half: #3903 established that stored
|
|
1066
|
+
// reach. It is not this half: objectstack-ai/objectstack#3903 established that stored
|
|
707
1067
|
// `sys_metadata` rows are rehydrated UNPARSED, so metadata written before
|
|
708
1068
|
// spec 17 arrives here as a plain object the compiler never saw, still
|
|
709
1069
|
// carrying `execute`. Declaring the key to make this read compile would
|
|
710
|
-
// re-legitimize a tombstone (#3855 removed it; the spec keeps it only to
|
|
1070
|
+
// re-legitimize a tombstone (objectstack-ai/objectstack#3855 removed it; the spec keeps it only to
|
|
711
1071
|
// reject it BY NAME), and deleting the branch would send those rows back
|
|
712
1072
|
// to a bare "no script provided" — which reads as "you forgot a field" to
|
|
713
1073
|
// an author who did write one, the objectstack#2169 shape.
|
|
@@ -1029,7 +1389,7 @@ export class ActionRunner {
|
|
|
1029
1389
|
* old object-form `params` page — POSTed an empty body here.
|
|
1030
1390
|
*
|
|
1031
1391
|
* `params` contributes only in its DEPRECATED non-array form (the compat window
|
|
1032
|
-
* #5777 opened; see `warnOnDeprecatedObjectParams`). An ARRAY `params` is a
|
|
1392
|
+
* objectstack-ai/objectstack#5777 opened; see `warnOnDeprecatedObjectParams`). An ARRAY `params` is a
|
|
1033
1393
|
* parameter DEFINITION list, not a payload: it reaches this method unconsumed
|
|
1034
1394
|
* only when no `paramCollectionHandler` is mounted, and POSTing the definitions
|
|
1035
1395
|
* as the request body was never a payload any endpoint wanted. It now falls
|
|
@@ -1220,16 +1580,15 @@ export class ActionRunner {
|
|
|
1220
1580
|
// BEFORE the request, so there is no result to read; admitting `${result.*}`
|
|
1221
1581
|
// there would widen the authorable surface with nothing behind it and
|
|
1222
1582
|
// silently blank the token instead of leaving the author's mistake visible.
|
|
1583
|
+
//
|
|
1584
|
+
// The success toast's copy is the one other caller of the grammar, through
|
|
1585
|
+
// `substituteScopeTokens` directly (`composeSuccessMessage`): `result` only,
|
|
1586
|
+
// because that is the one scope the contract declares for the copy, and no
|
|
1587
|
+
// encoding, because a toast is text.
|
|
1223
1588
|
const scopes = { param: params, ctx: this.buildInterpolationContext() };
|
|
1224
1589
|
if (resultScope !== undefined)
|
|
1225
1590
|
scopes.result = resultScope;
|
|
1226
|
-
|
|
1227
|
-
return target.replace(pattern, (_match, scope, path) => {
|
|
1228
|
-
const value = readPath(scopes[scope], path);
|
|
1229
|
-
if (value == null)
|
|
1230
|
-
return '';
|
|
1231
|
-
return encodeURIComponent(String(value));
|
|
1232
|
-
});
|
|
1591
|
+
return substituteScopeTokens(target, scopes, encodeURIComponent);
|
|
1233
1592
|
}
|
|
1234
1593
|
/**
|
|
1235
1594
|
* Assemble the `ctx` namespace exposed to target interpolation.
|
|
@@ -1295,15 +1654,14 @@ export async function executeAction(action, context = {}) {
|
|
|
1295
1654
|
return await runner.execute(action);
|
|
1296
1655
|
}
|
|
1297
1656
|
/**
|
|
1298
|
-
* Is this `onSuccess` the
|
|
1299
|
-
* chained-callback channel (`ActionDef | ActionDef[]`)?
|
|
1657
|
+
* Is this `onSuccess` the spec's navigation block?
|
|
1300
1658
|
*
|
|
1301
1659
|
* The test IS the spec's declaration: a non-array object carrying a STRING
|
|
1302
|
-
* `navigate`.
|
|
1303
|
-
*
|
|
1304
|
-
*
|
|
1305
|
-
*
|
|
1306
|
-
*
|
|
1660
|
+
* `navigate`. Stored rows are rehydrated UNPARSED (objectstack-ai/objectstack#3903), so the runner reads
|
|
1661
|
+
* the value as data and anything else gets NO reading — since objectui#5934
|
|
1662
|
+
* retired the legacy chained-callback channel (`ActionDef | ActionDef[]`),
|
|
1663
|
+
* there is no other channel for an off-contract shape to fall into. This is a
|
|
1664
|
+
* shape GUARD on unparsed data, not a discriminator between two meanings.
|
|
1307
1665
|
*/
|
|
1308
1666
|
export function readOnSuccessNavigation(value) {
|
|
1309
1667
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
@@ -1313,6 +1671,28 @@ export function readOnSuccessNavigation(value) {
|
|
|
1313
1671
|
return null;
|
|
1314
1672
|
return value;
|
|
1315
1673
|
}
|
|
1674
|
+
/**
|
|
1675
|
+
* The runner's ONE `${scope.path}` token grammar — the substitution behind
|
|
1676
|
+
* `interpolateTarget` (url / api targets and the `onSuccess.navigate` hop) and
|
|
1677
|
+
* behind the success toast's `${result.*}` copy (`composeSuccessMessage`).
|
|
1678
|
+
*
|
|
1679
|
+
* The scope MAP defines the vocabulary: the pattern is built from its keys, so
|
|
1680
|
+
* a token naming a scope the caller did not pass is left in the text exactly as
|
|
1681
|
+
* written. A path that resolves to nothing becomes `''`. `encode` is the only
|
|
1682
|
+
* thing a caller varies, and it belongs to the SINK, not to the dialect: a URL
|
|
1683
|
+
* position percent-encodes each value, a toast inserts it as text.
|
|
1684
|
+
*/
|
|
1685
|
+
function substituteScopeTokens(template, scopes, encode) {
|
|
1686
|
+
if (typeof template !== 'string' || template.indexOf('${') === -1)
|
|
1687
|
+
return template;
|
|
1688
|
+
const pattern = new RegExp(`\\$\\{(${Object.keys(scopes).join('|')})\\.([\\w.]+)\\}`, 'g');
|
|
1689
|
+
return template.replace(pattern, (_match, scope, path) => {
|
|
1690
|
+
const value = readPath(scopes[scope], path);
|
|
1691
|
+
if (value == null)
|
|
1692
|
+
return '';
|
|
1693
|
+
return encode(String(value));
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1316
1696
|
/**
|
|
1317
1697
|
* Dot-path read used by target interpolation. Plain reduce — kept inline
|
|
1318
1698
|
* to avoid pulling lodash for a 3-line helper.
|