feat: UserNotificationService supports click-to-navigate toasts
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
This commit is contained in:
@@ -1,9 +1,14 @@
|
|||||||
@if (notifications().length > 0) {
|
@if (notifications().length > 0) {
|
||||||
<aside class="floating-notifications" aria-live="polite" aria-atomic="true">
|
<aside class="floating-notifications" aria-live="polite" aria-atomic="true">
|
||||||
@for (note of notifications(); track note.id) {
|
@for (note of notifications(); track note.id) {
|
||||||
<article class="floating-note" [class]="'floating-note floating-note-' + note.type">
|
<article
|
||||||
|
class="floating-note"
|
||||||
|
[class]="'floating-note floating-note-' + note.type"
|
||||||
|
[class.floating-note-clickable]="!!note.route"
|
||||||
|
(click)="note.route && navigate(note)"
|
||||||
|
>
|
||||||
<p>{{ note.message }}</p>
|
<p>{{ note.message }}</p>
|
||||||
<button type="button" (click)="dismiss(note.id)" [attr.aria-label]="'common.dismiss' | translate">×</button>
|
<button type="button" (click)="$event.stopPropagation(); dismiss(note.id)" [attr.aria-label]="'common.dismiss' | translate">×</button>
|
||||||
</article>
|
</article>
|
||||||
}
|
}
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||||
import { UserNotificationService } from '../../services/user-notification.service';
|
import { Router } from '@angular/router';
|
||||||
|
import { UserNotification, UserNotificationService } from '../../services/user-notification.service';
|
||||||
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
|
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -12,10 +13,18 @@ import { TranslatePipe } from '../../../../../i18n/translate.pipe';
|
|||||||
})
|
})
|
||||||
export class FloatingNotificationsComponent {
|
export class FloatingNotificationsComponent {
|
||||||
private readonly notificationsService = inject(UserNotificationService);
|
private readonly notificationsService = inject(UserNotificationService);
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
|
||||||
readonly notifications = this.notificationsService.notifications;
|
readonly notifications = this.notificationsService.notifications;
|
||||||
|
|
||||||
dismiss(id: string): void {
|
dismiss(id: string): void {
|
||||||
this.notificationsService.dismiss(id);
|
this.notificationsService.dismiss(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
navigate(note: UserNotification): void {
|
||||||
|
if (note.route) {
|
||||||
|
void this.router.navigate(note.route);
|
||||||
|
}
|
||||||
|
this.dismiss(note.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { UserNotificationService } from './user-notification.service';
|
||||||
|
|
||||||
|
describe('UserNotificationService', () => {
|
||||||
|
let service: UserNotificationService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(UserNotificationService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stores the route on the notification when provided', () => {
|
||||||
|
service.show('New order #1042', 'info', 4000, ['en', 'backoffice', 'orders', 'ord_1']);
|
||||||
|
|
||||||
|
const [note] = service.notifications();
|
||||||
|
expect(note.message).toBe('New order #1042');
|
||||||
|
expect(note.route).toEqual(['en', 'backoffice', 'orders', 'ord_1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves route undefined when not provided', () => {
|
||||||
|
service.show('Saved');
|
||||||
|
|
||||||
|
const [note] = service.notifications();
|
||||||
|
expect(note.route).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,6 +6,8 @@ export interface UserNotification {
|
|||||||
id: string;
|
id: string;
|
||||||
message: string;
|
message: string;
|
||||||
type: UserNotificationType;
|
type: UserNotificationType;
|
||||||
|
/** Route to navigate to when the notification is clicked. Absent means not clickable. */
|
||||||
|
route?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
@@ -14,11 +16,12 @@ export class UserNotificationService {
|
|||||||
|
|
||||||
readonly notifications = this.state.asReadonly();
|
readonly notifications = this.state.asReadonly();
|
||||||
|
|
||||||
show(message: string, type: UserNotificationType = 'info', durationMs: number = 2500): void {
|
show(message: string, type: UserNotificationType = 'info', durationMs: number = 2500, route?: string[]): void {
|
||||||
const next: UserNotification = {
|
const next: UserNotification = {
|
||||||
id: `note-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
id: `note-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
message,
|
message,
|
||||||
type
|
type,
|
||||||
|
...(route ? { route } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.state.update(items => [next, ...items].slice(0, 4));
|
this.state.update(items => [next, ...items].slice(0, 4));
|
||||||
|
|||||||
Reference in New Issue
Block a user