From cadf6c4c8ec8e25e375c4b53745fa8b2d8c6c29a Mon Sep 17 00:00:00 2001 From: Brandon Kelly Date: Sat, 7 Nov 2026 22:08:47 +0000 Subject: [PATCH] Name untitled drafts in workflow notifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workflow notifications interpolated the draft's title, which is empty for drafts that don't have one yet, such as new entries submitted for review before they were titled. Use the draft's UI label instead, which falls back to “Untitled entry” and respects UI label formats. Amp-Thread-ID: https://ampcode.com/threads/T-01a1b7ff-d0c0-77c2-9643-6ab03a5ec1ad Co-authored-by: Amp --- CHANGELOG.md | 1 + .../UserReview/SendUserReviewNotifications.php | 12 ++++++------ tests/Feature/Workflow/WorkflowsTest.php | 11 +++++++++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac413c39b96..4cf9d540b0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -759,6 +759,7 @@ - Updated Fabric.js to v7. ([#19628](https://github.com/craftcms/cms/pull/19628)) ### Bug Fixes +- Fixed a bug where workflow notifications showed an empty title (“”) for untitled drafts, such as new entries submitted for review before they had a title. - Fixed a bug where spreadsheet cells containing line breaks or tabs were split across several Table field cells when pasted, with their quotes left in. - Fixed a bug where site error templates weren’t rendered, and unknown site URLs returned an empty 404 response. ([#20448](https://github.com/craftcms/cms/pull/20448)) - Fixed a bug where `config/redirects.php` rules weren’t applied. ([#20448](https://github.com/craftcms/cms/pull/20448)) diff --git a/src/Workflow/UserReview/SendUserReviewNotifications.php b/src/Workflow/UserReview/SendUserReviewNotifications.php index 2fcfb6c0f44..0ecdf1c8cca 100644 --- a/src/Workflow/UserReview/SendUserReviewNotifications.php +++ b/src/Workflow/UserReview/SendUserReviewNotifications.php @@ -60,7 +60,7 @@ private function sendApproved(WorkflowTransitioned $event, ?WorkflowStageData $s $recipients, 'workflow_approved', t('Draft approved'), - t('“{entry}” completed its approval workflow.', ['entry' => $event->draft->title]), + t('“{entry}” completed its approval workflow.', ['entry' => $event->draft->getUiLabel()]), ); } @@ -72,7 +72,7 @@ private function sendChangesRequested(WorkflowTransitioned $event, ?WorkflowStag collect([$event->run->author]), 'workflow_changes_requested', t('Changes requested'), - t('{user} requested changes to “{entry}”.', ['user' => $this->actorName($event), 'entry' => $event->draft->title]), + t('{user} requested changes to “{entry}”.', ['user' => $this->actorName($event), 'entry' => $event->draft->getUiLabel()]), ); } @@ -92,7 +92,7 @@ private function sendReviewRequested(WorkflowTransitioned $event, ?WorkflowStage 'workflow_review_requested', t('Review requested'), t('“{entry}” is awaiting your approval in {stage}.', [ - 'entry' => $event->draft->title, + 'entry' => $event->draft->getUiLabel(), 'stage' => t($stage->name), ]), ); @@ -110,7 +110,7 @@ private function sendCommented(WorkflowCommented $event): void $recipients, 'workflow_review_commented', t('New review comment'), - t('{user} commented on the review for “{entry}”.', ['user' => $this->actorName($event), 'entry' => $event->draft->title]), + t('{user} commented on the review for “{entry}”.', ['user' => $this->actorName($event), 'entry' => $event->draft->getUiLabel()]), ); } @@ -122,7 +122,7 @@ private function sendInvalidated(WorkflowTransitioned $event, ?WorkflowStageData collect([$event->run->author]), 'workflow_invalidated', t('Approval reset'), - t('The review for “{entry}” was reset because its workflow changed.', ['entry' => $event->draft->title]), + t('The review for “{entry}” was reset because its workflow changed.', ['entry' => $event->draft->getUiLabel()]), ); } @@ -171,7 +171,7 @@ private function send(WorkflowTransitioned|WorkflowCommented $event, ?WorkflowSt title: $title, message: $message, workflowName: (string) $event->run->activityRootEvent->data['workflow']['name'], - entryTitle: (string) $event->draft->title, + entryTitle: $event->draft->getUiLabel(), entryUrl: Uri::of($event->draft->getCpEditUrl())->withFragment('workflow')->value(), stageName: $stage === null ? null : t($stage->name), note: $event->note, diff --git a/tests/Feature/Workflow/WorkflowsTest.php b/tests/Feature/Workflow/WorkflowsTest.php index 8087b734186..838a99c2105 100644 --- a/tests/Feature/Workflow/WorkflowsTest.php +++ b/tests/Feature/Workflow/WorkflowsTest.php @@ -946,6 +946,17 @@ function (ElementInterface $draft) use (&$applied): ElementInterface { ->and($notification->data['url'])->toEndWith('#workflow'); }); +it('names untitled drafts in review notifications', function () { + $this->draft->title = null; + expect(Elements::saveElement($this->draft, runValidation: false, updateSearchIndex: false))->toBeTrue(); + $group = reviewerGroup([$this->reviewers[0]]); + workflowFor($this->entry, [userReviewStage('Review', $group)]); + submitAs($this->workflows, $this->draft, $this->author); + + expect($this->reviewers[0]->notifications()->latest()->firstOrFail()->data['message']) + ->toBe('“Untitled entry” is awaiting your approval in Review.'); +}); + it('requests user review after an asynchronous stage advances', function () { app(WorkflowStageTypes::class)->register(TestAutomatedWorkflowStage::class); $group = reviewerGroup([$this->reviewers[0]]);