@object-ui/core 17.5.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 +7404 -0
- package/README.md +127 -26
- package/dist/actions/ActionEngine.d.ts +5 -3
- package/dist/actions/ActionEngine.js +14 -10
- package/dist/actions/ActionRunner.d.ts +363 -30
- package/dist/actions/ActionRunner.js +544 -58
- package/dist/actions/TransactionManager.d.ts +7 -2
- package/dist/actions/actionKeys.d.ts +75 -13
- package/dist/actions/actionKeys.js +131 -16
- package/dist/actions/actionResponse.js +3 -3
- package/dist/actions/index.d.ts +2 -0
- package/dist/actions/index.js +2 -0
- package/dist/actions/recordDelete.d.ts +118 -0
- package/dist/actions/recordDelete.js +112 -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/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 +128 -1
- package/dist/evaluator/ExpressionEvaluator.js +226 -13
- package/dist/evaluator/FormulaFunctions.d.ts +0 -10
- package/dist/evaluator/FormulaFunctions.js +166 -27
- package/dist/evaluator/SafeExpressionParser.js +0 -4
- package/dist/evaluator/declaredPredicate.d.ts +93 -9
- package/dist/evaluator/declaredPredicate.js +118 -17
- package/dist/evaluator/fieldRules.d.ts +107 -4
- package/dist/evaluator/fieldRules.js +231 -35
- package/dist/evaluator/index.d.ts +2 -0
- package/dist/evaluator/index.js +2 -0
- package/dist/evaluator/listConditional.d.ts +125 -9
- package/dist/evaluator/listConditional.js +167 -26
- package/dist/evaluator/optionRules.d.ts +126 -0
- package/dist/evaluator/optionRules.js +141 -4
- package/dist/evaluator/predicateInput.d.ts +15 -4
- package/dist/evaluator/predicateInput.js +35 -2
- package/dist/evaluator/rowPredicateCanon.d.ts +56 -0
- package/dist/evaluator/rowPredicateCanon.js +158 -0
- 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 +18 -0
- package/dist/index.js +97 -2
- package/dist/protocols/index.d.ts +0 -1
- package/dist/protocols/index.js +9 -1
- package/dist/registry/Registry.d.ts +397 -48
- 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/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-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 +188 -0
- package/dist/utils/chart-presentation.js +345 -0
- package/dist/utils/chart-series.d.ts +271 -31
- package/dist/utils/chart-series.js +478 -55
- package/dist/utils/column-identity.d.ts +43 -0
- package/dist/utils/column-identity.js +47 -1
- 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 +407 -52
- 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/debug.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 +171 -11
- package/dist/utils/expand-fields.js +171 -11
- 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 -34
- package/dist/utils/freeze-schema.d.ts +38 -1
- package/dist/utils/freeze-schema.js +10 -0
- package/dist/utils/grouping-fields.d.ts +61 -0
- package/dist/utils/grouping-fields.js +84 -0
- package/dist/utils/humanize-label.d.ts +60 -0
- package/dist/utils/humanize-label.js +62 -0
- package/dist/utils/managedBy.d.ts +30 -5
- 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 +131 -1
- package/dist/utils/normalize-list-view.js +248 -6
- package/dist/utils/number-display.d.ts +70 -17
- 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 +62 -4
- package/dist/utils/predicate-fields.js +169 -4
- 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/retired-field-types.d.ts +90 -0
- package/dist/utils/retired-field-types.js +124 -0
- package/dist/utils/sort-query.d.ts +128 -26
- 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/unmaterialized-fields.d.ts +52 -0
- package/dist/utils/unmaterialized-fields.js +57 -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 +9 -4
- package/dist/validation/index.js +9 -4
- package/dist/validation/schema-validator.d.ts +1 -1
- package/dist/validation/schema-validator.js +305 -82
- 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.d.ts +1 -1
- package/dist/validation/validators/object-validation-engine.js +14 -5
- package/package.json +8 -9
- package/dist/__benchmarks__/core.bench.d.ts +0 -8
- package/dist/__benchmarks__/core.bench.js +0 -53
- 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
|
@@ -5,10 +5,52 @@
|
|
|
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';
|
|
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
|
+
};
|
|
12
54
|
/**
|
|
13
55
|
* Bind the server-CEL-parity `os.user` identity alias into an action context
|
|
14
56
|
* (#2358 trap 1). `@objectstack/spec` documents the canonical CEL identity
|
|
@@ -79,6 +121,24 @@ function readContextObjectName(context) {
|
|
|
79
121
|
const name = context.objectName;
|
|
80
122
|
return typeof name === 'string' && name.trim() !== '' ? name.trim() : undefined;
|
|
81
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
|
+
}
|
|
82
142
|
/**
|
|
83
143
|
* Whether opening FormView `viewName` from a record of `contextObject` would
|
|
84
144
|
* cross an object boundary (objectui#4292).
|
|
@@ -101,6 +161,91 @@ function crossesObjectBoundary(viewName, contextObject) {
|
|
|
101
161
|
return false;
|
|
102
162
|
return !viewName.startsWith(`${contextObject}.`);
|
|
103
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
|
+
}
|
|
104
249
|
/*
|
|
105
250
|
* ## The two predicate gates in `execute`, and the one question they ask first
|
|
106
251
|
*
|
|
@@ -128,8 +273,10 @@ function crossesObjectBoundary(viewName, contextObject) {
|
|
|
128
273
|
* Both read {@link hasDeclaredPredicate} — the repo's one definition of that
|
|
129
274
|
* question, in `evaluator/declaredPredicate.ts` beside the normalizer it is
|
|
130
275
|
* derived from, with the scope objectui#3850 ruled on (`''` / whitespace-only /
|
|
131
|
-
* empty-`source` envelope
|
|
132
|
-
*
|
|
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
|
|
133
280
|
* scope note saying it stayed private until that ruling landed; it has, so the
|
|
134
281
|
* definition moved down a layer and the renderer side reads the same one
|
|
135
282
|
* (`components/renderers/action/visibility-gate.ts` re-exports it as
|
|
@@ -163,9 +310,15 @@ function crossesObjectBoundary(viewName, contextObject) {
|
|
|
163
310
|
* historical `Boolean(raw)` coercion, so `0` there meant "hidden" — fail-CLOSED
|
|
164
311
|
* on junk, the opposite of what this module committed to for `disabled`
|
|
165
312
|
* (`catch { isDisabled = false }`). That filter now reads
|
|
166
|
-
* {@link hasDeclaredPredicate} too (objectui#3957), so
|
|
167
|
-
*
|
|
168
|
-
*
|
|
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`.
|
|
169
322
|
*/
|
|
170
323
|
export class ActionRunner {
|
|
171
324
|
constructor(context = {}) {
|
|
@@ -229,6 +382,12 @@ export class ActionRunner {
|
|
|
229
382
|
writable: true,
|
|
230
383
|
value: void 0
|
|
231
384
|
});
|
|
385
|
+
Object.defineProperty(this, "translate", {
|
|
386
|
+
enumerable: true,
|
|
387
|
+
configurable: true,
|
|
388
|
+
writable: true,
|
|
389
|
+
value: void 0
|
|
390
|
+
});
|
|
232
391
|
/**
|
|
233
392
|
* Built-in dispatch, one entry per runnable action type.
|
|
234
393
|
*
|
|
@@ -266,6 +425,21 @@ export class ActionRunner {
|
|
|
266
425
|
this.navigationHandler = null;
|
|
267
426
|
this.paramCollectionHandler = null;
|
|
268
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;
|
|
269
443
|
}
|
|
270
444
|
/**
|
|
271
445
|
* Set a custom confirmation handler (e.g., Shadcn AlertDialog).
|
|
@@ -328,10 +502,10 @@ export class ActionRunner {
|
|
|
328
502
|
try {
|
|
329
503
|
// `ActionDef` accepts any key of any type, so a typo (`targt`) and a
|
|
330
504
|
// retired key (`execute`) both reach here having type-checked. Neither
|
|
331
|
-
// binds a handler, and binding no handler silently is the #2169 "Mark Done
|
|
332
|
-
// 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).
|
|
333
507
|
warnOnUnknownActionKeys(action);
|
|
334
|
-
// 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
|
|
335
509
|
// ruling 2026-08-06 direction A). Checked HERE, before the param-collection
|
|
336
510
|
// block below rewrites `action.params` into a values map — after that point
|
|
337
511
|
// an action that authored a legitimate ActionParam[] DEFINITION array also
|
|
@@ -375,9 +549,16 @@ export class ActionRunner {
|
|
|
375
549
|
// code only evaluated the STRING form and treated any object as truthy,
|
|
376
550
|
// so an envelope-disabled action was ALWAYS "disabled" — silently
|
|
377
551
|
// blocking every execution (param dialog never opened, handler never
|
|
378
|
-
// ran). `evaluateCondition` already handles boolean/string/envelope
|
|
379
|
-
//
|
|
380
|
-
//
|
|
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.
|
|
381
562
|
let isDisabled = false;
|
|
382
563
|
try {
|
|
383
564
|
isDisabled = this.evaluator.evaluateCondition(action.disabled);
|
|
@@ -389,14 +570,13 @@ export class ActionRunner {
|
|
|
389
570
|
return { success: false, error: 'Action is disabled' };
|
|
390
571
|
}
|
|
391
572
|
}
|
|
392
|
-
// Confirmation
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
} : undefined);
|
|
573
|
+
// Confirmation. One spelling: `confirmText` — the only one the
|
|
574
|
+
// translation bundle can address (`…._actions.{name}.confirmText`). The
|
|
575
|
+
// structured `confirm.message` read that used to OUTRANK it is retired
|
|
576
|
+
// (objectui#4314): it had zero producers and no bundle key, so a dialog
|
|
577
|
+
// authored that way silently lost localization.
|
|
578
|
+
if (action.confirmText) {
|
|
579
|
+
const confirmed = await this.confirmHandler(this.evaluator.evaluate(action.confirmText));
|
|
400
580
|
if (!confirmed) {
|
|
401
581
|
return { success: false, error: 'Action cancelled by user' };
|
|
402
582
|
}
|
|
@@ -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
|
}
|
|
@@ -558,12 +817,10 @@ export class ActionRunner {
|
|
|
558
817
|
catch (err) {
|
|
559
818
|
// Acknowledgement failure is non-fatal; the underlying action already
|
|
560
819
|
// succeeded. Log so consumers can wire it through their error reporter.
|
|
561
|
-
// eslint-disable-next-line no-console
|
|
562
820
|
console.warn('[ActionRunner] resultDialog handler rejected; treating as acknowledged', err);
|
|
563
821
|
}
|
|
564
822
|
}
|
|
565
823
|
else {
|
|
566
|
-
// eslint-disable-next-line no-console
|
|
567
824
|
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
825
|
}
|
|
569
826
|
}
|
|
@@ -586,25 +843,204 @@ export class ActionRunner {
|
|
|
586
843
|
if (chainResult.reload)
|
|
587
844
|
result.reload = true;
|
|
588
845
|
}
|
|
589
|
-
//
|
|
846
|
+
// ── ActionSchema.onSuccess — post-success navigation ────────────────────
|
|
847
|
+
//
|
|
848
|
+
// `053fdc8f9`, the console half of objectstack#9566/#9474. The spec
|
|
849
|
+
// declares `onSuccess` as a CLOSED STRICT object
|
|
850
|
+
// `{ navigate: string, openIn: 'self' | 'newTab' }`, refine-scoped to
|
|
851
|
+
// `type: 'api'` and `type: 'script'` — the two types whose success event
|
|
852
|
+
// carries a server response for `${result.*}` to read.
|
|
853
|
+
//
|
|
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.
|
|
860
|
+
//
|
|
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.
|
|
590
865
|
if (result.success && action.onSuccess) {
|
|
591
|
-
const
|
|
592
|
-
|
|
866
|
+
const navigation = readOnSuccessNavigation(action.onSuccess);
|
|
867
|
+
if (navigation) {
|
|
868
|
+
this.navigateOnSuccess(navigation, action, result);
|
|
869
|
+
}
|
|
593
870
|
}
|
|
594
871
|
if (!result.success && action.onFailure) {
|
|
595
872
|
const callbacks = Array.isArray(action.onFailure) ? action.onFailure : [action.onFailure];
|
|
596
873
|
await this.executeChain(callbacks, 'sequential');
|
|
597
874
|
}
|
|
598
875
|
}
|
|
876
|
+
/**
|
|
877
|
+
* Perform the `ActionSchema.onSuccess` hop for an action that just succeeded.
|
|
878
|
+
*
|
|
879
|
+
* **The router is the app's own.** `navigationHandler` is the seam every
|
|
880
|
+
* navigator in this file already goes through, and the console wires it to
|
|
881
|
+
* react-router's `navigate` — so `openIn: 'self'` is a real SPA route hop,
|
|
882
|
+
* in place, immune to popup blocking. No `window.location` / `window.open`
|
|
883
|
+
* mechanism is introduced here; the no-handler fallback is the same
|
|
884
|
+
* `result.redirect` hand-off `navigateTo` already uses.
|
|
885
|
+
*
|
|
886
|
+
* **No default is written here.** `openIn` is a materialised
|
|
887
|
+
* `.default('self')` in the spec, so parse output always carries a resolved
|
|
888
|
+
* member; this reads the ONE member that changes the branch and leaves the
|
|
889
|
+
* other implicit. A `?? 'self'` would be a second source of truth for a
|
|
890
|
+
* default the producer already resolved.
|
|
891
|
+
*
|
|
892
|
+
* **The two `openIn` spellings never cross.** This reads
|
|
893
|
+
* `onSuccess.openIn` (`'self' | 'newTab'`) and nothing else — never the
|
|
894
|
+
* top-level `type: 'url'` switch, which spells its new-tab member
|
|
895
|
+
* `'new-tab'`. Spec refuses each crossover spelling with a keyed error; a
|
|
896
|
+
* renderer that accepted either would be a second, looser contract than the
|
|
897
|
+
* one authors are validated against.
|
|
898
|
+
*/
|
|
899
|
+
navigateOnSuccess(block, action, result) {
|
|
900
|
+
// `${result.*}` reads the HANDLER's return value, one level below the
|
|
901
|
+
// action envelope — the same level `readActionPayload` hands the
|
|
902
|
+
// `redirectUrl` convention. Reading `result.data` raw would see the
|
|
903
|
+
// `{ success, data }` envelope on pre-objectstack#3962 servers, which is
|
|
904
|
+
// the "one level too shallow" bug (#2904) in a new place.
|
|
905
|
+
const url = this.interpolateTarget(block.navigate, action, readActionPayload(result.data));
|
|
906
|
+
// `navigate` is author-supplied metadata and lands in a URL position, so
|
|
907
|
+
// it goes through the same scheme guard as every other navigator here.
|
|
908
|
+
if (!this.isValidUrl(url)) {
|
|
909
|
+
console.warn('[ActionRunner] onSuccess.navigate resolved to a URL this runner refuses to open '
|
|
910
|
+
+ '(only http://, https:// and relative URLs are allowed) — no navigation performed.', { action: action.name, navigate: block.navigate, resolved: url });
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
const newTab = block.openIn === 'newTab';
|
|
914
|
+
// Same external-URL convention `navigateTo` hands the handler; `newTab`
|
|
915
|
+
// stays the declared choice alone, never `?? isExternal`.
|
|
916
|
+
const external = url.startsWith('http://') || url.startsWith('https://');
|
|
917
|
+
if (this.navigationHandler) {
|
|
918
|
+
this.navigationHandler(url, { external, newTab });
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
if (newTab) {
|
|
922
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
result.redirect = url;
|
|
926
|
+
}
|
|
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
|
+
}
|
|
599
1035
|
/**
|
|
600
1036
|
* Execute script action — evaluates client-side expression via ExpressionEvaluator.
|
|
601
1037
|
* Supports ${} template expressions referencing data, record, user context.
|
|
602
1038
|
*/
|
|
603
1039
|
async executeScript(action) {
|
|
604
1040
|
// `target` is the only handler slot. The `execute` alias was removed in
|
|
605
|
-
// @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
|
|
606
1042
|
// with the rename prescription — so parsed metadata cannot carry it and a
|
|
607
|
-
// `target || execute` fallback could only ever evaluate to `target` (#3856).
|
|
1043
|
+
// `target || execute` fallback could only ever evaluate to `target` (objectstack-ai/objectstack#3856).
|
|
608
1044
|
const script = action.target;
|
|
609
1045
|
if (!script) {
|
|
610
1046
|
// A spec `body` IS a script — this runner just cannot run one (see the
|
|
@@ -627,11 +1063,11 @@ export class ActionRunner {
|
|
|
627
1063
|
// `ActionDef` no longer declares `execute` — nor any open index signature
|
|
628
1064
|
// — so `tsc` now catches the retired key at every site that AUTHORS an
|
|
629
1065
|
// action literal in code. That is the half of the problem the type can
|
|
630
|
-
// reach. It is not this half: #3903 established that stored
|
|
1066
|
+
// reach. It is not this half: objectstack-ai/objectstack#3903 established that stored
|
|
631
1067
|
// `sys_metadata` rows are rehydrated UNPARSED, so metadata written before
|
|
632
1068
|
// spec 17 arrives here as a plain object the compiler never saw, still
|
|
633
1069
|
// carrying `execute`. Declaring the key to make this read compile would
|
|
634
|
-
// 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
|
|
635
1071
|
// reject it BY NAME), and deleting the branch would send those rows back
|
|
636
1072
|
// to a bare "no script provided" — which reads as "you forgot a field" to
|
|
637
1073
|
// an author who did write one, the objectstack#2169 shape.
|
|
@@ -953,7 +1389,7 @@ export class ActionRunner {
|
|
|
953
1389
|
* old object-form `params` page — POSTed an empty body here.
|
|
954
1390
|
*
|
|
955
1391
|
* `params` contributes only in its DEPRECATED non-array form (the compat window
|
|
956
|
-
* #5777 opened; see `warnOnDeprecatedObjectParams`). An ARRAY `params` is a
|
|
1392
|
+
* objectstack-ai/objectstack#5777 opened; see `warnOnDeprecatedObjectParams`). An ARRAY `params` is a
|
|
957
1393
|
* parameter DEFINITION list, not a payload: it reaches this method unconsumed
|
|
958
1394
|
* only when no `paramCollectionHandler` is mounted, and POSTing the definitions
|
|
959
1395
|
* as the request body was never a payload any endpoint wanted. It now falls
|
|
@@ -1098,7 +1534,7 @@ export class ActionRunner {
|
|
|
1098
1534
|
* param. Consumers can extend by stuffing extra keys under
|
|
1099
1535
|
* `context.ctx = {...}` before calling `runner.execute()`.
|
|
1100
1536
|
*/
|
|
1101
|
-
interpolateTarget(target, action) {
|
|
1537
|
+
interpolateTarget(target, action, resultScope) {
|
|
1102
1538
|
if (typeof target !== 'string' || target.indexOf('${') === -1)
|
|
1103
1539
|
return target;
|
|
1104
1540
|
// ── The `${param.X}` scope is an INTERNAL runtime value bag, not an
|
|
@@ -1135,14 +1571,24 @@ export class ActionRunner {
|
|
|
1135
1571
|
const params = (action.params && typeof action.params === 'object' && !Array.isArray(action.params))
|
|
1136
1572
|
? action.params
|
|
1137
1573
|
: {};
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1574
|
+
// The scope MAP defines the vocabulary — the pattern is built from its
|
|
1575
|
+
// keys, so a scope that is not in play is not a scope this call recognizes.
|
|
1576
|
+
//
|
|
1577
|
+
// `result` (the handler's own return value) is passed by exactly one caller:
|
|
1578
|
+
// the `ActionSchema.onSuccess` post-success hop, where a response exists.
|
|
1579
|
+
// Every other caller — `executeUrl`, `executeAPI` — interpolates a target
|
|
1580
|
+
// BEFORE the request, so there is no result to read; admitting `${result.*}`
|
|
1581
|
+
// there would widen the authorable surface with nothing behind it and
|
|
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.
|
|
1588
|
+
const scopes = { param: params, ctx: this.buildInterpolationContext() };
|
|
1589
|
+
if (resultScope !== undefined)
|
|
1590
|
+
scopes.result = resultScope;
|
|
1591
|
+
return substituteScopeTokens(target, scopes, encodeURIComponent);
|
|
1146
1592
|
}
|
|
1147
1593
|
/**
|
|
1148
1594
|
* Assemble the `ctx` namespace exposed to target interpolation.
|
|
@@ -1207,6 +1653,46 @@ export async function executeAction(action, context = {}) {
|
|
|
1207
1653
|
const runner = new ActionRunner(context);
|
|
1208
1654
|
return await runner.execute(action);
|
|
1209
1655
|
}
|
|
1656
|
+
/**
|
|
1657
|
+
* Is this `onSuccess` the spec's navigation block?
|
|
1658
|
+
*
|
|
1659
|
+
* The test IS the spec's declaration: a non-array object carrying a STRING
|
|
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.
|
|
1665
|
+
*/
|
|
1666
|
+
export function readOnSuccessNavigation(value) {
|
|
1667
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
1668
|
+
return null;
|
|
1669
|
+
const navigate = value.navigate;
|
|
1670
|
+
if (typeof navigate !== 'string' || navigate === '')
|
|
1671
|
+
return null;
|
|
1672
|
+
return value;
|
|
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
|
+
}
|
|
1210
1696
|
/**
|
|
1211
1697
|
* Dot-path read used by target interpolation. Plain reduce — kept inline
|
|
1212
1698
|
* to avoid pulling lodash for a 3-line helper.
|