[Admin] Support clickable navbar notifications with translation parameters (#19015)
Some checks failed
Continuous Integration (Minimal) / Static checks (push) Has been cancelled
Continuous Integration (Minimal) / Javascript Tests (MySQL) (push) Has been cancelled
Continuous Integration (Minimal) / Tests (PostgreSQL) (push) Has been cancelled
Continuous Integration (Minimal) / Frontend (push) Has been cancelled
Continuous Integration (Minimal) / Packages (push) Has been cancelled
Continuous Integration (Minimal) / Tests (MariaDB) (push) Has been cancelled
Continuous Integration (Minimal) / Tests (MySQL) (push) Has been cancelled

| Q               | A
|-----------------|-----
| Branch?         | 2.3
| Bug fix?        | no
| New feature?    | yes
| BC breaks?      | no
| Deprecations?   | no
| Related tickets | 
| License         | MIT

Extends the admin navbar notifications contract so plugins can surface
clickable, parameterized notifications without overriding the navbar
template.

Each notification returned by a `NotificationProviderInterface` may now
carry five optional keys honored by the default template:

  - `message_parameters` — passed to `|trans`
- `uri` (string, optional) — plain URI used as the notification link,
takes precedence over `route` when both are present
- `route` + `route_parameters` — when present, the item renders as `<a
href="{{ path(...) }}">`
  - `translation_domain` — passed to `|trans`
- `type` (`info` | `warning` | `danger`, default `danger`) — drives
status-dot color

The loop body in `notifications.html.twig` is now emitted through a twig
hook (`sylius_admin.common.component.navbar.items.notifications.item`,
default node), so plugins can also fully replace the item rendering by
registering their own node with higher priority.

Fully backward compatible: existing providers returning only `message`
render identically.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Navbar notifications are now rendered via pluggable items, supporting
clickable links (URI or route), translated messages with parameters, and
configurable status types (info, warning, danger; default danger).

* **Tests**
* Added tests to ensure notification payloads—including optional fields
and routing—are preserved end-to-end.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/Sylius/Sylius/pull/19015?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Karol Wojdyła 2026-05-21 15:00:30 +02:00 committed by GitHub
commit 5bac4c62cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 67 additions and 12 deletions

View file

@ -16,6 +16,19 @@ namespace Sylius\Bundle\AdminBundle\Notification;
interface NotificationProviderInterface
{
/**
* Each notification is identified by a string key. The value is a map with the following keys honored by the
* default admin navbar notifications template:
*
* - `message` (string, required): translation key passed to the `trans` filter.
* - `message_parameters` (array, optional): parameters passed to the `trans` filter.
* - `uri` (string, optional): plain URI used as the notification link. Takes precedence over `route` when both are present.
* - `route` (string, optional): when present (and `uri` is not), the notification renders as a link to this route.
* - `route_parameters` (array, optional): parameters passed to `path()` together with `route`.
* - `translation_domain` (string, optional): translation domain passed to the `trans` filter.
* - `type` (string, optional): one of `info`, `warning`, `danger`. Defaults to `danger`.
*
* Providers MAY include additional keys for their own purposes; the default template ignores them.
*
* @param array<mixed> $context
*
* @return array<array-key, mixed>

View file

@ -27,3 +27,8 @@ sylius_twig_hooks:
search:
template: '@SyliusAdmin/shared/crud/common/navbar/menu/search.html.twig'
priority: 0
'sylius_admin.common.component.navbar.items.notifications.item':
default:
template: '@SyliusAdmin/shared/components/navbar/notifications/item.html.twig'
priority: 0

View file

@ -25,18 +25,7 @@
{% else %}
<div class="list-group list-group-flush list-group-hoverable">
{% for notification in notifications %}
<div class="list-group-item">
<div class="row align-items-center">
<div class="col-auto">
<span class="status-dot status-dot-animated bg-red d-block"></span>
</div>
<div class="col">
<div class="d-block mt-n1">
{{ notification.message|trans }}
</div>
</div>
</div>
</div>
{% hook 'notifications.item' with { notification } %}
{% endfor %}
</div>
{% endif %}

View file

@ -0,0 +1,28 @@
{% set notification = hookable_metadata.context.notification %}
{% set type = notification.type|default('danger') %}
{% set href = null %}
{% if notification.uri is defined and notification.uri %}
{% set href = notification.uri %}
{% elseif notification.route is defined and notification.route %}
{% set href = path(notification.route, notification.route_parameters|default({})) %}
{% endif %}
{% set has_link = href is not null %}
{% set element = has_link ? 'a' : 'div' %}
<{{ element }}
class="list-group-item{% if has_link %} list-group-item-action{% endif %}"
{% if has_link %}href="{{ href }}"{% endif %}
>
<div class="row align-items-center">
<div class="col-auto">
<span class="status-dot status-dot-animated bg-{{ type }} d-block"></span>
</div>
<div class="col">
<div class="d-block mt-n1">
{{ notification.message|trans(notification.message_parameters|default({}), notification.translation_domain|default(null)) }}
</div>
</div>
</div>
</{{ element }}>

View file

@ -48,4 +48,24 @@ final class NotificationsComponentTest extends TestCase
'message' => 'sylius.ui.notifications.new_version_of_sylius_available',
]);
}
#[Test]
public function it_propagates_optional_notification_fields_unchanged(): void
{
$notification = [
'message' => 'app.notification.legacy_secret_key',
'message_parameters' => ['%gateway_name%' => 'Stripe EU'],
'uri' => 'https://dashboard.stripe.com/apikeys',
'route' => 'sylius_admin_payment_method_update',
'route_parameters' => ['id' => 42],
'translation_domain' => 'messages',
'type' => 'warning',
];
$this->notificationProvider->method('getNotifications')->willReturn(['legacy_secret_key.42' => $notification]);
$notifications = $this->notificationsComponent->getNotifications();
$this->assertSame($notification, $notifications['legacy_secret_key.42']);
}
}