Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './event-display.service';
export * from './extras/event-data-import';
export * from './extras/attribute.pipe';
export * from './notification.service';
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { NotificationService } from './notification.service';

describe('NotificationService', () => {
let service: NotificationService;
let received: any[];

beforeEach(() => {
service = new NotificationService();
received = [];
service.subscribeToNotifications((n) => received.push(n));
});

it('should emit success notification', () => {
service.success('Operation complete');
expect(received[0].severity).toBe('success');
expect(received[0].message).toBe('Operation complete');
expect(received[0].duration).toBe(5000);
});

it('should emit info notification', () => {
service.info('Loading file');
expect(received[0].severity).toBe('info');
expect(received[0].duration).toBe(5000);
});

it('should emit warning notification', () => {
service.warning('File may be malformed');
expect(received[0].severity).toBe('warning');
expect(received[0].duration).toBe(8000);
});

it('should emit error notification with no auto-dismiss', () => {
service.error('Could not parse event file');
expect(received[0].severity).toBe('error');
expect(received[0].duration).toBe(0);
});

it('should respect custom duration', () => {
service.success('Custom', 3000);
expect(received[0].duration).toBe(3000);
});

it('should support multiple subscribers', () => {
const second: any[] = [];
const unsub = service.subscribeToNotifications((n) => second.push(n));

service.info('Broadcast test');
expect(received.length).toBe(1);
expect(second.length).toBe(1);

unsub();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Injectable } from '@angular/core';
import { ActiveVariable } from 'phoenix-event-display';

/** Severity levels for notifications. */
export type NotificationSeverity = 'success' | 'info' | 'warning' | 'error';

/** A notification message with severity and content. */
export interface Notification {
/** The message to display to the user. */
message: string;
/** The severity level of the notification. */
severity: NotificationSeverity;
/** Duration in milliseconds before auto-dismiss. 0 means no auto-dismiss. */
duration: number;
}

/**
* Service for displaying user-facing notifications with severity levels.
* Provides success, info, warning, and error notifications with
* configurable auto-dismiss durations. Replaces silent console errors
* with actionable user feedback across all Phoenix components.
*/
@Injectable({
providedIn: 'root',
})
export class NotificationService {
/** Default auto-dismiss durations in milliseconds per severity level. */
private readonly defaultDurations: Record<NotificationSeverity, number> = {
success: 5000,
info: 5000,
warning: 8000,
error: 0,
};

/** Active variable that stores the latest notification. */
private notification = new ActiveVariable<Notification>();

/**
* Subscribe to incoming notifications.
* @param callback Function called when a notification is emitted.
* @returns A function that can be called to unsubscribe.
*/
subscribeToNotifications(
callback: (notification: Notification) => void,
): () => void {
return this.notification.onUpdate(callback);
}

/**
* Display a success notification.
* Auto-dismisses after 5 seconds by default.
* @param message The message to display.
* @param duration Optional custom duration in ms. 0 means no auto-dismiss.
*/
success(message: string, duration?: number): void {
this.notify(message, 'success', duration);
}

/**
* Display an info notification.
* Auto-dismisses after 5 seconds by default.
* @param message The message to display.
* @param duration Optional custom duration in ms. 0 means no auto-dismiss.
*/
info(message: string, duration?: number): void {
this.notify(message, 'info', duration);
}

/**
* Display a warning notification.
* Auto-dismisses after 8 seconds by default.
* @param message The message to display.
* @param duration Optional custom duration in ms. 0 means no auto-dismiss.
*/
warning(message: string, duration?: number): void {
this.notify(message, 'warning', duration);
}

/**
* Display an error notification.
* Does not auto-dismiss by default — requires manual dismissal.
* @param message The message to display.
* @param duration Optional custom duration in ms. 0 means no auto-dismiss.
*/
error(message: string, duration?: number): void {
this.notify(message, 'error', duration);
}

/**
* Internal method to emit a notification.
* @param message The message to display.
* @param severity The severity level.
* @param duration Optional custom duration override.
*/
private notify(
message: string,
severity: NotificationSeverity,
duration?: number,
): void {
this.notification.update({
message,
severity,
duration: duration ?? this.defaultDurations[severity],
});
}
}
Loading