Follow-up to #3610522. The instance trace now offers an "Open" link on a parked human node (its operator surface). After the operator processes that task, they are returned to the surface's own default (the inbox for a user task, the pending-actions list for an operation), not back to the trace they opened it from.

For an operator driving an instance from the trace (an admin debugging, or a support agent), returning to the trace would keep them in context.

Idea

Thread a return target through the "Open" URL the trace builds (a destination or a per-handler return query), so completion redirects back to the trace. Some completion surfaces already honor a return (the interaction task reads orchestra_return), but the operation and inbox forms redirect to their own fixed landing, so this needs a per-handler look, not a single switch.

Low priority: a convenience, not a correctness issue.

Issue fork orchestra-3610524

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

mably created an issue. See original summary.

mably’s picture

Design notes for a future implementation.

The tempting one-liner, adding ?destination= to the trace "Open" link, is not safe. Core's RedirectResponseSubscriber overrides any redirect with destination, including the open-time redirect some interactions perform: a webform-collect step or a payment step redirects the operator off to the form or PSP on open, so with destination set that redirect is hijacked back to the trace and the operator never reaches the task. It only works for surfaces that render in place (an order_review).

A correct fix must return only after completion, not on the open-time redirect, and the completion surfaces each handle return differently:

  • the review's embedded CommentForm redirects to the context returnUrl (the dispatcher);
  • InteractionTaskController::signal honors orchestra_return;
  • the inbox task form and the operation form redirect to their own fixed landings.

So the implementation should thread a "return after completion" target through each completion path (orchestra_interaction_task, orchestra_ui operations, orchestra_inbox), rather than a blanket destination. Worth a functional test driving open-from-trace to complete to land-on-trace.

mably’s picture

Design for the record. Two layers: a stateless return parameter for the direct completion surfaces, and a durable server-side entry for the collect round-trips (webform edit/collect and payment), where a query parameter cannot survive the hop.

Shared helper

A single OrchestraReturn helper in the base module owns the convention: an orchestra_return query argument, an internal-only guard (UrlHelper::isExternal), and a LocalRedirectResponse, never a destination parameter (which RedirectResponseSubscriber would override). It replaces the two divergent copies that already exist: InteractionTaskController::signal (orchestra_return, falls back to the inbox) and CommentForm (its RETURN_ARG "return", falls back to the front page). Both standardize on orchestra_return.

Direct surfaces

The trace is built by OrchestraUiController (the instance canonical page), which resolves each parked token's Open URL through PendingActionsFinder::actUrlsFor(). Thread an optional return argument through actUrlFor/actUrlsFor, PendingActionHandlerInterface::actUrl/actUrls, and the three handlers (OperationActionHandler, InboxTaskHandler, InteractionTaskActionHandler), each calling OrchestraReturn::embed(), so every Open link carries the current trace URL. Honor it at completion, each surface keeping its current landing as the fallback:

  • InteractionTaskController::signal: resolve to the inbox fallback.
  • OperationController::signal: resolve to my_actions on a direct signal, and forward orchestra_return to UserOperationForm on a guarded outcome.
  • UserOperationForm and the inbox UserTaskForm: carry the value from the GET link into the POST via a value element, then resolve on submit.
  • InboxController::complete: resolve to the inbox fallback.
  • CommentForm: resolve to the front page fallback, keeping its existing dual source (embedded argument or query).

The interface change is internal to Orchestra (no external consumers).

Collect round-trip (webform edit/collect and payment)

A stateless parameter cannot survive here: InteractionToken is a minimal signed capability with no payload room, ReturnsToWorkflowTrait::returnToWorkflow() deliberately reads nothing from the request, and the dispatcher terminal renders a message rather than redirecting. So the return is stored server-side, keyed by the opened token and the operator:

  • Store: a keyvalue.expirable entry keyed by the opened token id and operator uid (token id and uid, uid above 0), with a TTL matching the interaction token, holding the internal return path. OrchestraReturn gains remember(), recall() and forget().
  • Key by the token, not the instance: the return belongs to the specific opened task. An instance key would collide across concurrent branches, and would force honoring at the dispatcher terminal (the whole instance finished), which for a multi-step branch drags the operator through the rest of the chain before returning them. The issue asks to return after the opened task is processed.
  • Set at open: InteractionController::step reads the orchestra_return query and remembers it, keyed by the opened token, before handing off to the interaction (which, for collect, redirects to the webform, or off-site for payment). The query is only the trace-to-open hop; from there it lives server-side.
  • Honor when the opened token is resumed, not at the terminal: for webform collect, honor in OrchestraInteractionHandler right after resume(). If a return is remembered for this token and operator, forget() and redirect to the trace instead of returnToWorkflow(). returnToWorkflow() stays untouched and stateless; the handler chooses trace versus dispatcher by reading server-side state keyed by token and uid, not the request, so its invariant holds. A visitor resuming the same step has no entry and chains normally.

Payment

Payment is best-effort. Settlement can be asynchronous and server-side while the browser returns separately to the dispatcher, so the opened token may already be consumed when the operator's browser lands. It likely honors on the browser return, keyed by the branch token carried in the return capability and guarded by uid. It degrades gracefully to the dispatcher message, and uid-keying structurally prevents a real payer from ever being bounced to an operator trace.

Not this issue

InteractionContext::returnUrl() and branchReturnUrl() are the dispatcher continuation layer (token-scoped capability URLs that chain to the next interaction); they are left untouched.

Boundaries

  • A webform with return_to_workflow disabled (its own inline or modal confirmation) never returns to the dispatcher, so it cannot return to the trace.
  • Payment return-to-trace is best-effort, as above.

Tests

Functional per surface: open a parked task from the trace, complete it, assert the redirect lands back on the trace rather than the default landing; plus the webform collect case with return_to_workflow on and off. Kernel coverage for OrchestraReturn (embed coexists with an existing query, resolve honors internal, rejects external, empty falls back) and the durable trio (remember, recall, forget round-trip, TTL, per token-and-uid isolation).

mably’s picture

Status: Active » Needs review

  • mably committed 9091726a on 1.x
    feat: #3610524 Return to the process trace after an opened task is...
mably’s picture

Status: Needs review » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.