@object-ui/core 17.7.0 → 18.0.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +650 -0
  2. package/README.md +127 -0
  3. package/dist/actions/ActionRunner.d.ts +192 -49
  4. package/dist/actions/ActionRunner.js +389 -102
  5. package/dist/actions/UndoManager.d.ts +8 -13
  6. package/dist/actions/actionKeys.d.ts +1 -1
  7. package/dist/actions/actionKeys.js +10 -0
  8. package/dist/actions/index.d.ts +1 -0
  9. package/dist/actions/index.js +1 -0
  10. package/dist/actions/navigationTarget.d.ts +41 -0
  11. package/dist/actions/navigationTarget.js +20 -0
  12. package/dist/actions/recordDelete.d.ts +59 -23
  13. package/dist/actions/recordDelete.js +40 -42
  14. package/dist/adapters/ApiDataSource.d.ts +8 -4
  15. package/dist/adapters/ApiDataSource.js +19 -7
  16. package/dist/adapters/resolveDataSource.d.ts +9 -6
  17. package/dist/adapters/resolveDataSource.js +9 -12
  18. package/dist/data-scope/ViewDataProvider.d.ts +7 -24
  19. package/dist/data-scope/ViewDataProvider.js +6 -53
  20. package/dist/evaluator/deadlineSettled.d.ts +40 -0
  21. package/dist/evaluator/deadlineSettled.js +45 -0
  22. package/dist/evaluator/fieldRules.js +6 -1
  23. package/dist/evaluator/index.d.ts +1 -0
  24. package/dist/evaluator/index.js +1 -0
  25. package/dist/evaluator/optionRules.d.ts +1 -1
  26. package/dist/evaluator/optionRules.js +1 -1
  27. package/dist/evaluator/subjectPermissions.d.ts +30 -1
  28. package/dist/evaluator/subjectPermissions.js +42 -0
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.js +3 -0
  31. package/dist/registry/Registry.d.ts +34 -13
  32. package/dist/registry/Registry.js +5 -22
  33. package/dist/utils/affordanceGrants.d.ts +371 -0
  34. package/dist/utils/affordanceGrants.js +252 -0
  35. package/dist/utils/chart-series.d.ts +34 -0
  36. package/dist/utils/chart-series.js +25 -2
  37. package/dist/utils/date-display.d.ts +42 -50
  38. package/dist/utils/date-display.js +192 -11
  39. package/dist/utils/expand-fields.d.ts +6 -4
  40. package/dist/utils/expand-fields.js +6 -4
  41. package/dist/utils/filter-converter.d.ts +6 -2
  42. package/dist/utils/filter-converter.js +6 -2
  43. package/dist/utils/normalize-list-view.d.ts +16 -12
  44. package/dist/utils/normalize-list-view.js +66 -67
  45. package/dist/utils/predicate-fields.js +6 -2
  46. package/dist/utils/record-source.d.ts +1 -1
  47. package/dist/utils/record-source.js +1 -1
  48. package/dist/utils/reference-keys.js +1 -1
  49. package/dist/validation/schema-validator.js +29 -5
  50. package/dist/validation/server-owned-value.d.ts +5 -1
  51. package/dist/validation/server-owned-value.js +5 -1
  52. package/package.json +4 -4
package/README.md CHANGED
@@ -179,6 +179,75 @@ withoutDeniedFields({ account_code: 'A-1', salary: 100 }, policy, 'account', ['a
179
179
  - When nothing is withheld the SAME object comes back, so callers can tell the
180
180
  two cases apart by identity.
181
181
 
182
+ ### Affordance-to-grant map (`AFFORDANCE_GRANTS`, `resolveAffordance`)
183
+
184
+ Every console affordance that offers a write — a create form's fields, the
185
+ record header's Edit, a list's New and Import, a lookup's "Create new", a grid
186
+ row's Delete, and the rest — reads the grant it exercises from ONE table,
187
+ `AFFORDANCE_GRANTS` (objectui#12082). A row names the CRUD-affordance bit the
188
+ affordance needs, the object grant it exercises (`create`, `update` or
189
+ `delete`) and, for an affordance that offers fields, the field question it asks
190
+ (`create` for an insert, `write` for an update). The fields of a form, of the
191
+ record page's in-place edit (`recordEdit`) and of a list's in-cell edit
192
+ (`listInlineEdit`) each ask that question, so a field the caller's permission
193
+ set marks `editable: false` is offered read-only on every one of them
194
+ (objectui#12103). The `write` question is the server's whole update rule for a
195
+ field, so for the record's owner field it also needs the transfer grant
196
+ (`allowTransfer`, or `modifyAllRecords`).
197
+
198
+ `resolveAffordance` is the one verdict: the object's managed-object policy, the
199
+ server's effective API operation set and the caller's grant must all allow it,
200
+ and the row's `userActions` predicates are surfaced only then.
201
+
202
+ ```typescript
203
+ import { resolveAffordance, resolveFieldAffordance } from '@object-ui/core'
204
+
205
+ // `perms` is structural: `usePermissions()` from `@object-ui/permissions`
206
+ // satisfies it. With no provider mounted every grant reads open.
207
+ const perms = {
208
+ isLoaded: true,
209
+ can: (_object: string, grant: 'create' | 'update' | 'delete') => grant === 'create',
210
+ checkField: (_object: string, _field: string, action: 'create' | 'write') => action === 'create',
211
+ getObjectApiOperations: () => undefined,
212
+ }
213
+
214
+ resolveAffordance('listNew', { objectSchema: null, objectName: 'orders', perms })
215
+ // { allowed: true } — the create grant is held
216
+ resolveAffordance('recordEdit', { objectSchema: null, objectName: 'orders', perms })
217
+ // { allowed: false } — Edit reads the update grant, which is not
218
+
219
+ resolveFieldAffordance('createFormFields', perms, 'orders', 'title') // true
220
+ resolveFieldAffordance('editFormFields', perms, 'orders', 'title') // false
221
+ ```
222
+
223
+ An affordance added later gets a row here and reads it through these functions;
224
+ it does not spell its own `can(object, verb)`. That includes an affordance that
225
+ reads NO grant at all — the calendar's quick-create and drag-to-reschedule
226
+ (`calendarQuickCreate`, `calendarReschedule`) and the kanban card move
227
+ (`kanbanCardMove`) were such affordances until they gained their rows. An
228
+ affordance that writes with the same verb on the same object reuses that row:
229
+ the record overlay's inline edit reads `recordEdit`, a gantt's bar edits and a
230
+ grid's bulk field update read `rowEdit`, a gantt row's Delete reads `rowDelete`,
231
+ the AI build panel's spreadsheet import reads `listImport`, a related list's
232
+ Add existing reads `relatedNew` (a link row) or `relatedRowEdit` (a re-parent),
233
+ and Setup's permission-set and position holders read `relatedNew` (a holder
234
+ added) and `relatedRowDelete` (a holder removed), each asked of its assignment
235
+ object (`sys_user_permission_set`, `sys_user_position`). Reaching Setup is not
236
+ that grant: the Setup app asks `setup.access`, which a caller can hold while the
237
+ server refuses their write of an assignment row.
238
+ The record feed's writes have rows of their own, each asked of the object it
239
+ writes: `feedComment` (a comment or reply, `sys_comment`), `feedReactionAdd` and
240
+ `feedReactionRemove` (the caller's own `sys_comment_reaction` row), and
241
+ `feedReactionColumn` (a reaction on a deployment that stores them in a
242
+ `sys_comment` column). The map's
243
+ enumeration pin in `@object-ui/plugin-form` (`affordanceGrantMap-12082.test.tsx`)
244
+ also counts every write call site in the console tree and holds each to the row
245
+ it sits behind, or to the reason it has none — and, for a component whose write
246
+ its host gates (the import wizard), holds every mount to reading the row. The
247
+ sites it found with no row yet formed a frozen backlog, which the pin now holds
248
+ empty: a new one cannot be added, so a write affordance gets its row when it
249
+ lands.
250
+
182
251
  ### Undo snapshot for an update (`captureUpdateUndoData`)
183
252
 
184
253
  An `undoable` update restores exactly the fields it wrote, from their values
@@ -223,6 +292,64 @@ captureUpdateUndoData(['config'], { id: 't1', config: { id: 'c1', mode: 'strict'
223
292
  - `fields` is required. Pass `undefined` only when the caller has no field
224
293
  definitions for the object; nothing is then treated as a relation.
225
294
 
295
+ ### Which fields an `operation: 'update'` Undo covers
296
+
297
+ The platform action route writes the `patch` of the declared action the
298
+ action's `name` names, under the collected params. So `ActionRunner`'s Undo
299
+ snapshot covers that declaration's `patch` keys plus the collected params. The
300
+ runner reads the declaration from its context: a host publishes the object's
301
+ declared actions as `objectActions`, beside `objectName` and `objectFields`.
302
+
303
+ ```typescript
304
+ import { ActionRunner } from '@object-ui/core'
305
+
306
+ // The object definition the host already holds, as the metadata API serves it.
307
+ declare const taskDef: { fields: Record<string, unknown>; actions: unknown[] }
308
+
309
+ // The same context `ActionProvider` takes. `objectFields` and `objectActions`
310
+ // are read for `objectName`'s object only.
311
+ new ActionRunner({
312
+ objectName: 'task',
313
+ objectFields: taskDef.fields,
314
+ objectActions: taskDef.actions,
315
+ })
316
+ ```
317
+
318
+ - The list is read only for the context's own object. A dispatch to another
319
+ object, or a host that publishes no list, reads the dispatched action's own
320
+ `patch`: a host that dispatches an object's action whole hands it in.
321
+ - A page action row's own `patch` is never covered, and no action renderer
322
+ forwards one: `@objectstack/spec` 18 retires the key on the row. A row that
323
+ names an action whose declaration has no `patch` snapshots the collected
324
+ params alone.
325
+
326
+ ### Display time zone (`setDisplayTimeZone`)
327
+
328
+ The date and datetime faces this package exports (`formatDate`,
329
+ `formatDateTime`, `formatDateTimeCompactParts`, `formatRelativeDate`) render
330
+ an instant in one display zone. A host declares it once; no caller passes a
331
+ zone, so every face on the page agrees.
332
+
333
+ ```typescript
334
+ import { formatDateTime, setDisplayTimeZone } from '@object-ui/core'
335
+
336
+ formatDateTime('2026-09-02T03:00:00Z', { locale: 'en-US' }) // the viewer's zone
337
+ setDisplayTimeZone('America/Los_Angeles')
338
+ formatDateTime('2026-09-02T03:00:00Z', { locale: 'en-US' }) // "Sep 1, 2026, 08:00 PM"
339
+ formatDateTime('2026-09-01', { locale: 'en-US' }) // "Sep 1, 2026, 12:00 AM", in every zone
340
+ setDisplayTimeZone(undefined) // back to the viewer's zone
341
+ ```
342
+
343
+ - A date-only value names a calendar day and keeps it in every zone, including
344
+ the `Date` that `toDisplayDate` builds for one and a caller hands on.
345
+ - "Today" for the relative face is the display zone's today.
346
+ - An IANA name the runtime's `Intl` does not know clears the zone, with a
347
+ console warning.
348
+ - `getDisplayTimeZone()` reads it back, and `subscribeDisplayTimeZone(listener)`
349
+ reports changes in React's `useSyncExternalStore` shape. In a React tree,
350
+ `LocalizationProvider` from `@object-ui/i18n` sets it from its `timezone`
351
+ value; a renderer never calls the setter.
352
+
226
353
  ## Philosophy
227
354
 
228
355
  This package is designed to be **framework-agnostic**. It contains:
@@ -19,44 +19,37 @@
19
19
  * Features: conditional execution, confirmation, toast notifications,
20
20
  * redirect handling, action chaining, custom handler registration.
21
21
  */
22
- import type { UIActionSchema } from '@object-ui/types';
22
+ import type { ActionContext, ActionResult, UIActionSchema } from '@object-ui/types';
23
23
  import type { Action as SpecActionInput } from '@objectstack/spec/ui';
24
24
  import { ExpressionEvaluator } from '../evaluator/ExpressionEvaluator.js';
25
- import { type UndoableOperation } from './UndoManager.js';
26
25
  import { type FieldContainerLike } from '../utils/predicate-record.js';
27
- export interface ActionResult {
28
- success: boolean;
29
- data?: any;
30
- error?: string;
31
- reload?: boolean;
32
- close?: boolean;
33
- redirect?: string;
34
- /** Modal schema to render (for type: 'modal') */
35
- modal?: any;
36
- /**
37
- * Suppress the automatic success toast for this result. A handler sets this
38
- * when the action only HANDED OFF to a follow-up UI rather than completing —
39
- * e.g. a `flow` action that paused at a screen and opened the flow-runner. The
40
- * action hasn't "completed yet", so a "success" toast on open would be
41
- * misleading; the follow-up surface owns its own completion messaging.
42
- */
43
- silent?: boolean;
44
- /**
45
- * An undoable operation captured by the handler (e.g. an `undoable` update
46
- * action's prior field values). When present, the runner pushes it onto the
47
- * global UndoManager and the success toast offers an "Undo" affordance.
48
- */
49
- undo?: UndoableOperation;
50
- }
51
- export interface ActionContext {
52
- data?: Record<string, any>;
53
- record?: any;
54
- selectedRecords?: Record<string, any>[];
55
- /** Live page-variable snapshot (ADR-0049), published by PageVariableActionBridge. */
56
- pageVariables?: Record<string, any>;
57
- user?: any;
58
- [key: string]: any;
59
- }
26
+ /**
27
+ * What an action handler returns and the runner reads — ONE authority, in
28
+ * `@object-ui/types` (objectui#6349, batch 4), re-exported here exactly as
29
+ * {@link ActionContext} is below. The members this module used to declare
30
+ * (`reload`, `redirect`, `modal`, `silent`, `undo`) moved down with it, and
31
+ * `UndoableOperation`, which types `undo`, moved down beside it
32
+ * (`./UndoManager.js` re-exports that one). The gate that counts authorities is
33
+ * `scripts/__tests__/one-authority-per-exported-name-6273.test.ts`; a re-export
34
+ * is not one.
35
+ */
36
+ export type { ActionResult } from '@object-ui/types';
37
+ /**
38
+ * The context a runner executes actions in — ONE authority, in
39
+ * `@object-ui/types` (objectui#6349, batch 4). This module RE-EXPORTS that
40
+ * declaration instead of declaring a second one, so an import of
41
+ * `ActionContext` from `@object-ui/core` and one from `@object-ui/types` are
42
+ * the same type. `@object-ui/types` is the dependency-legal side: it depends on
43
+ * no `@object-ui/*` package, and this package depends on it.
44
+ *
45
+ * A re-export is not a second authority —
46
+ * `scripts/__tests__/one-authority-per-exported-name-6273.test.ts` counts
47
+ * declarations and ALIASING re-exports, never `export type { X } from …` —
48
+ * which is why this convergence takes `ActionContext` off that gate's
49
+ * `KNOWN_COLLISIONS` baseline. The member drift it removed is recorded on the
50
+ * declaration itself.
51
+ */
52
+ export type { ActionContext } from '@object-ui/types';
60
53
  /**
61
54
  * API configuration for complex requests.
62
55
  */
@@ -358,7 +351,8 @@ export interface ActionDef {
358
351
  * caller's permissions, the object's hooks and its validations all fire as
359
352
  * for a user edit. That is why {@link execute} reads this key BEFORE `type`:
360
353
  * by the time `type` is consulted an update action is indistinguishable from
361
- * any other script action, and its `patch` would never reach the request.
354
+ * any other script action, and it would run without the update path's Undo
355
+ * capture and its no-route refusal.
362
356
  */
363
357
  operation?: SpecActionInput['operation'];
364
358
  /**
@@ -366,6 +360,16 @@ export interface ActionDef {
366
360
  * record, merged UNDER the collected `params` so a fixed value can be
367
361
  * declared without exposing it in the dialog. The spec refuses it on an
368
362
  * action without `operation: 'update'`, where it would be silently dropped.
363
+ *
364
+ * The ROUTE applies it, from the declaration it resolves by name; this
365
+ * runner never sends it (objectui#12941) and reads it only to know which
366
+ * fields an Undo must restore.
367
+ *
368
+ * It is a DECLARATION's key here: a host that dispatches an object's action
369
+ * whole hands it along. No action renderer forwards a page row's own `patch`
370
+ * (objectui#13022, following `@objectstack/spec` 18's retirement of the row
371
+ * key), and the runner reads the host-published declaration ahead of this
372
+ * one (`declaredUpdatePatch`).
369
373
  */
370
374
  patch?: SpecActionInput['patch'];
371
375
  /** Execution mode of the action. */
@@ -458,6 +462,11 @@ export type ActionRunnerHandler = (action: ActionDef, context: ActionContext) =>
458
462
  * `.cancel`), and `app-shell`'s `ActionConfirmDialog.tsx` renders them in
459
463
  * place of its generic `actionConfirm.*` fallbacks. Line numbers are left out
460
464
  * on purpose: #5205's pointer had already drifted by the time it was executed.
465
+ * The record delete is a second producer (objectui#11695): `app-shell`'s
466
+ * `useObjectActions` (the list's row and bulk Delete) and `RecordDetailView`
467
+ * (the record page's Delete) hand it `recordDelete.confirmCopy`'s title and
468
+ * confirm label with `destructive: true`, which paints the confirm button in
469
+ * the destructive button style.
461
470
  *
462
471
  * So re-measure across the repo, not this package: grep for two-argument
463
472
  * calls of `ConfirmationHandler`-typed values (`confirmHandler(`, `onConfirm(`)
@@ -471,6 +480,8 @@ export type ConfirmationHandler = (message: string, options?: {
471
480
  title?: string;
472
481
  confirmText?: string;
473
482
  cancelText?: string;
483
+ /** The confirmed action destroys data: paint the confirm button destructive. */
484
+ destructive?: boolean;
474
485
  }) => Promise<boolean>;
475
486
  /**
476
487
  * Toast handler — consumers can wire to Sonner or any toast library.
@@ -500,8 +511,35 @@ export type NavigationHandler = (url: string, options?: {
500
511
  * Param collection handler — consumers provide to show a dialog
501
512
  * for collecting ActionParam values before action execution.
502
513
  * Returns collected values, or null if cancelled.
514
+ *
515
+ * `interpolate` fills the `${...}` holes of the dialog's authored copy (the
516
+ * action's `description`, each param's `helpText`) in the same template scope
517
+ * the runner reads `confirmText` in — for a row dispatch, the row is `record`
518
+ * (see `templateEvaluatorFor`). The runner cannot fill them itself before the
519
+ * call: the handler localizes that copy AFTER it receives it (a translation
520
+ * bundle entry or an inline per-locale map), so the hole has to be filled on
521
+ * the text the user will actually read (objectui#12967).
522
+ *
523
+ * `run` lets the handler keep its dialog open until the run its values start
524
+ * has an outcome (objectui#13161). Called with the collected values, it
525
+ * dispatches the action with them and resolves with that run's
526
+ * {@link ActionResult} (a dispatch that throws resolves as a refused run, the
527
+ * way `execute` reports one). A refused run has already had its
528
+ * post-execution (the error toast, `onFailure`), so the handler can show the
529
+ * refusal beside the values the user typed and call `run` again with the
530
+ * edited values. A successful run's post-execution (success toast, result
531
+ * dialog, chain, `onSuccess`) waits until the handler settles its promise,
532
+ * so the dialog is closed before any of it shows. A call made while an
533
+ * earlier one is still in flight returns that earlier run, and dispatches
534
+ * nothing new.
535
+ *
536
+ * Once `run` has been called, the handler's own promise only ends the
537
+ * session: the runner never dispatches the values it resolves with. `null`
538
+ * after a refused run is a cancel, reported exactly as a cancel before any
539
+ * run. A handler that never calls `run` keeps the old contract: the runner
540
+ * dispatches once with the values it resolves.
503
541
  */
504
- export type ParamCollectionHandler = (params: ActionParamDef[], action?: ActionDef) => Promise<Record<string, any> | null>;
542
+ export type ParamCollectionHandler = (params: ActionParamDef[], action?: ActionDef, interpolate?: (text: string) => string, run?: (values: Record<string, any>) => Promise<ActionResult>) => Promise<Record<string, any> | null>;
505
543
  /**
506
544
  * The contract's own result-dialog block, and one entry of its field list.
507
545
  *
@@ -819,7 +857,32 @@ export declare class ActionRunner {
819
857
  */
820
858
  registerScript(scriptName: string, handler: ActionRunnerHandler): void;
821
859
  unregisterScript(scriptName: string): void;
822
- execute(action: ActionDef): Promise<ActionResult>;
860
+ /**
861
+ * Run `action`; with `callContext`, run it against that context for this
862
+ * one dispatch (objectui#13063).
863
+ *
864
+ * The runner's context names the PAGE: its `record` and its `objectName`.
865
+ * A dispatch that runs against some other record, such as a related-list
866
+ * row on a record page, which is a record of the CHILD object, hands that
867
+ * record here as `{ record, objectName }`. Every executor then reads it as
868
+ * the record the action runs against, the way a dispatch from that record's
869
+ * own page does: `executeForm` forwards its id and object, and its
870
+ * boundary check compares the target view against the child object instead
871
+ * of the page's.
872
+ *
873
+ * The runner's own context is not touched: the dispatch runs on a one-shot
874
+ * runner over the merged context that shares this runner's handlers (see
875
+ * {@link ActionRunner.scopedTo}).
876
+ */
877
+ execute(action: ActionDef, callContext?: Partial<ActionContext>): Promise<ActionResult>;
878
+ /**
879
+ * Dispatch an action whose params are collected, and answer what it
880
+ * returned — WITHOUT post-execution, which each caller runs itself:
881
+ * `execute` right after a lone run, and the param-collection session
882
+ * (`ParamCollectionHandler`'s `run`, objectui#13161) once per refused attempt
883
+ * and once for the successful one when the session ends.
884
+ */
885
+ private dispatchCollected;
823
886
  /**
824
887
  * Execute multiple actions in sequence or parallel.
825
888
  */
@@ -872,7 +935,9 @@ export declare class ActionRunner {
872
935
  * react-router's `navigate` — so `openIn: 'self'` is a real SPA route hop,
873
936
  * in place, immune to popup blocking. No `window.location` / `window.open`
874
937
  * mechanism is introduced here; the no-handler fallback is the same
875
- * `result.redirect` hand-off `navigateTo` already uses.
938
+ * `result.redirect` hand-off `navigateTo` already uses. The one exception is
939
+ * shared with `navigateTo`: a `mailto:` target is not a route, so it is a
940
+ * full-page navigation in place (objectui#12191).
876
941
  *
877
942
  * **No default is written here.** `openIn` is a materialised
878
943
  * `.default('self')` in the spec, so parse output always carries a resolved
@@ -911,12 +976,34 @@ export declare class ActionRunner {
911
976
  * sent hunting for a field they were right not to write), and falling through
912
977
  * to `executeActionSchema` reports on an action that never ran.
913
978
  *
914
- * ## `patch` rides UNDER the collected params
979
+ * ## `patch` is the route's: it never rides on the wire (objectui#12941)
980
+ *
981
+ * The request carries the values collected for the action's declared
982
+ * `params`, plus the transport keys the dispatch reads (`recordId`, and the
983
+ * `_rowRecord` stash it strips before POSTing) — and NOTHING from `patch`.
984
+ * The route applies the declared `patch` itself, from the declaration it
985
+ * resolves by the action's name (`@objectstack/runtime`'s
986
+ * `declarativeUpdateWrite`, which writes `{ ...patch, ...params }`). The
987
+ * patch keys are not params the action declares, so posting them makes every
988
+ * declarative-update button a request carrying undeclared params — the very
989
+ * request the ADR-0104 D2 param contract refuses (objectstack#24268). A
990
+ * client copy of `patch` on the wire also adds nothing: the server never
991
+ * reads it.
992
+ *
993
+ * The spec's words for the merge the route performs: static field values
994
+ * "merged UNDER the user-supplied `params` so a fixed value can be declared
995
+ * without exposing it in the dialog". So a collected param of the same name
996
+ * WINS — the author declared a default, and the user answered it. This
997
+ * runner still computes that same merge, but only to know what the route
998
+ * WROTE, which is what an Undo snapshot must cover; it is never sent.
915
999
  *
916
- * The spec's words: static field values "merged UNDER the user-supplied
917
- * `params` so a fixed value can be declared without exposing it in the
918
- * dialog". So a collected param of the same name WINS — the author declared a
919
- * default, and the user answered it.
1000
+ * ## The `patch` in that merge is the DECLARATION's (objectui#13022)
1001
+ *
1002
+ * The route writes the `patch` of the declaration the action's `name` names,
1003
+ * so that is the `patch` the snapshot covers: read by
1004
+ * {@link declaredUpdatePatch} from the declarations the host published, else
1005
+ * off a def that is the declaration itself. A page action row's own `patch`
1006
+ * is never written (`@objectstack/spec` 18 retires it) and never covered.
920
1007
  */
921
1008
  private executeUpdateOperation;
922
1009
  /**
@@ -929,6 +1016,24 @@ export declare class ActionRunner {
929
1016
  * Uses navigationHandler for SPA routing, falls back to window.location.
930
1017
  */
931
1018
  private executeUrl;
1019
+ /**
1020
+ * A navigation announces nothing unless its action declares the copy
1021
+ * (objectui#12755).
1022
+ *
1023
+ * `type: 'url'`, its `navigation` alias and `type: 'form'` only change the
1024
+ * route: nothing was written, so there is nothing to confirm. Their
1025
+ * `{ success: true }` used to reach `handlePostExecution` like a write's, and
1026
+ * the generic `completedSuccessfully` toast ("操作已成功完成") told the user an
1027
+ * operation had completed when a page had merely opened (objectstack-ai/cloud#2909:
1028
+ * an "invite member" button that only navigated read as "invitation sent").
1029
+ *
1030
+ * The result keeps `success: true` (callers read it as "the navigation
1031
+ * happened") and is marked `silent`, the flag that already means "no success
1032
+ * toast for this result". An author who wants a toast after the navigation
1033
+ * declares `successMessage`, and that copy still shows: the declaration is the
1034
+ * only signal, so there is no separate switch to forget.
1035
+ */
1036
+ private announceNavigation;
932
1037
  /**
933
1038
  * The one navigator behind `type: 'url'` and its `navigation` alias.
934
1039
  *
@@ -1053,10 +1158,6 @@ export declare class ActionRunner {
1053
1158
  * Execute API action — supports both simple string endpoint and complex ApiConfig.
1054
1159
  */
1055
1160
  private executeAPI;
1056
- /**
1057
- * Validate URL to prevent javascript: or data: protocol injection.
1058
- */
1059
- private isValidUrl;
1060
1161
  /**
1061
1162
  * Substitute `${param.X}` and `${ctx.X}` tokens in an action target.
1062
1163
  *
@@ -1091,8 +1192,51 @@ export declare class ActionRunner {
1091
1192
  * placed under context.ctx is merged on top.
1092
1193
  */
1093
1194
  private buildInterpolationContext;
1195
+ /**
1196
+ * The evaluator a dispatch's authored copy is read through: `confirmText`,
1197
+ * and the param dialog's `description` / `helpText` via the handler's
1198
+ * `interpolate` argument.
1199
+ *
1200
+ * One rule for every door (objectui#12967): the record the action runs
1201
+ * against is `record`. The record header binds it in the runner's context
1202
+ * (`RecordDetailView`'s `ActionProvider`). A row surface does not — it
1203
+ * stashes the clicked row under `params._rowRecord` (ObjectGrid's row
1204
+ * actions, a related list's row actions, `DeclaredActionsBar`, a page
1205
+ * header, `action:button`'s update write) and fires through the shared
1206
+ * runner, whose context names the page, not the row. So when the dispatch
1207
+ * carries a row, the row is bound as `record` for this dispatch's templates,
1208
+ * and it wins over a page-level `record` (a related-list row on a record page
1209
+ * is the child, not the page's record). Nothing else in the scope changes,
1210
+ * and the runner's own context is not touched.
1211
+ *
1212
+ * A hole naming a field the record does not carry degrades the way the
1213
+ * header's always has: the evaluator reads `undefined` and the hole prints
1214
+ * empty.
1215
+ */
1216
+ private templateEvaluatorFor;
1094
1217
  updateContext(newContext: Partial<ActionContext>): void;
1095
1218
  getContext(): ActionContext;
1219
+ /**
1220
+ * A one-shot runner for {@link ActionRunner.execute}'s `callContext`
1221
+ * (objectui#13063): this runner's context with `callContext` merged over it,
1222
+ * and this runner's handlers, translator and registries, shared rather than
1223
+ * copied, so the dispatch confirms, collects params, toasts and navigates
1224
+ * exactly as this runner does.
1225
+ *
1226
+ * Two keys follow the merge rather than the base:
1227
+ *
1228
+ * - `ctx`, the `ctx.*` mirror `ActionProvider` publishes beside the flat
1229
+ * keys. A `${ctx.record.…}` hole has to read the same record as a
1230
+ * `${record.…}` one.
1231
+ * - `objectFields` / `objectActions`. A host publishes them BESIDE
1232
+ * `objectName`, so the pair names one object (objectui#11122,
1233
+ * objectui#13022). When `callContext` names another object and brings no
1234
+ * definitions of its own, the base's are dropped: the page object's field
1235
+ * types and declarations must not answer for the child's record. That is
1236
+ * what a dispatch with no `callContext` already gets for a retargeted
1237
+ * action, so the update path's capture sees the same input either way.
1238
+ */
1239
+ private scopedTo;
1096
1240
  /**
1097
1241
  * Get the expression evaluator (for components that need to evaluate visibility, etc.)
1098
1242
  */
@@ -1119,4 +1263,3 @@ export interface OnSuccessNavigation {
1119
1263
  * shape GUARD on unparsed data, not a discriminator between two meanings.
1120
1264
  */
1121
1265
  export declare function readOnSuccessNavigation(value: unknown): OnSuccessNavigation | null;
1122
- export {};