Let an assignee ask someone else to work a task, and confirm what they submit

Implements #3613915. No longer a draft: this carries the whole issue.

An assignee can ask somebody else to work their task without giving up the decision. The person asked works it on the task's own surface, their submission is stored instead of signalled, and the assignee confirms it, sends it back, decides differently, or takes the task back. What is held is the token advance and nothing else: the work itself is real by then.

The engine

WorkItem gains worked_by, pending_completion and the awaiting_confirmation state. Pre-1.0, so no update hook: a fresh install picks the fields up, an existing site installs the two field storage definitions.

WorkItemManagerInterface gains askToWork(), sendForConfirmation(), confirm(), sendBack(), withdrawWorkRequest() and submitCompletion(). confirm() is the ordinary complete() call with the stored payload, which is why no task type learns that a confirmation exists: the completion payload was already the task-type-agnostic currency of every doorway. sendForConfirmation() validates through the same OutcomeSignaler::resolve() completion uses and discards the resolved variables, so an unconfigured outcome is refused when it is chosen rather than at confirmation time.

One rule, one home, three times over

Three things could each have been restated per surface, and are not:

  • Whether a submission binds or waits. Five surfaces submit a completion (the inbox form, the one-click outcome, the content task, the interaction resume, the bulk action). They all call submitCompletion(), which decides and reports what it did through a small CompletionSubmission enum so each can word its own message. The predicate itself is submissionWouldWait(), asked by the router and by both listing surfaces.
  • Whether a step allows being handed out, and whether its surface can be worked twice. WorkRequestPolicy answers both for the operations, the route access and the notice. The first is the workflow author's, per step (HumanNodeInterface::mayBeWorkedByAnother()); the second is the surface's own (RepeatableWorkInterface), forwarded by an interaction step to the interaction it renders.
  • What a submission on this page will actually do. One service plus a request subscriber keyed on any route carrying a work item, rather than a banner written into each surface, so the inbox form, the comment form and the interaction pages are covered at once. Queued as status messages, which carry the right semantics for a screen reader.

Defects found while wiring it, fixed here

  • release() accepted only a claimed task, so an unclaim escalation timer would have silently done nothing while unconfirmed work sat there.
  • The live-state list existed in four places (both listing queries, TaskActions and the entity), so a new state had to be added to each; they now share WorkItemInterface::STATES_ACTIONABLE.
  • The content task completed with no actor, recording the assignee as the completer even when somebody else acted.
  • Being asked granted the right to act but not the row, so the task never reached the inbox it was handed to. Caught by the functional test, fixed in the one shared visibility condition.
  • Completion::outcomeOf() now holds the "a payload is either the outcome or a map carrying it" test that was inline and about to be copied.

What narrows it

A per-step May be worked by someone else checkbox (allow_work_request, boolean schema, on by default), enforced on the route and not only in the list. The surface's own answer on whether a second pass is safe: a content step yes with a caution about the extra revision, a webform step yes in every mode since it reopens the party's own submission, the redirect step no. And these operations belong to the decision holder alone: the reassign permission deliberately does not open them, since a request obliges the assignee to confirm.

The pending-actions surface, which lists pull-based operations, offers no task-level operations at all today, reassignment included, so it offers none of these either; it does honor the same withholding of one-click outcomes.

Notifications and audit

Four notifications, each to the one person concerned: asked, work submitted, sent back, and request discarded (the one that matters most, since it fires when a reassign or a timeout throws somebody's work away). They ride the existing dispatcher, inheriting its guard and its workspace context, on one notification type carrying the sentence rather than four templates. The same transitions are audit events, on the audit spine, which stays separate from notifications.

Tests

ConfirmWorkTest (15), AskToWorkLinksTest (8), AskToWorkTest (3, functional, the round trip through both inboxes), plus a case in TaskNotificationMailTest. The clearing, gating and visibility assertions were each confirmed to fail against code with those guards removed, so none of them passes by construction.

Docs and the French translations are in the same commits as the behavior.

AI-Generated: Yes (Claude Code was used to write this change, its tests and this description. I reviewed them; the kernel tests were run locally, several assertions were confirmed to fail against code with their guards removed, and CI is green on both lanes.)


Also folded in (unrelated to the task-delegation change): drupal/audit_trail moves from ^1.0 to 1.x-dev in require-dev. Audit Trail has no stable release (alpha1 to alpha6, plus 1.x-dev), so the old constraint was already resolving against a pre-1.0 branch without saying so, and orchestra_audit_trail uses AuditTrailInterface and AuditTrailSubject directly. Kessai is pinned the same way in [#3618957], and pdv already pins drupal/audit_trail to 1.x-dev. Carried here rather than as its own issue, to keep the issue count down.

Edited by Frank Mably

Merge request reports

Loading
Loading