From f500f4de9f13a1e702c12b295c72704f650ff08b Mon Sep 17 00:00:00 2001 From: dvcolomban Date: Wed, 26 Aug 2026 17:54:42 +0200 Subject: [PATCH 1/2] fix(hub-ui): support persistent toast notifications --- .../hub-ui/src/client/state/toasts.test.ts | 69 +++++++++++++++++++ packages/hub-ui/src/client/state/toasts.ts | 10 ++- packages/hub/src/types/messages.ts | 2 +- .../tsnapi/@devframes/hub/index.snapshot.d.ts | 2 +- 4 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 packages/hub-ui/src/client/state/toasts.test.ts diff --git a/packages/hub-ui/src/client/state/toasts.test.ts b/packages/hub-ui/src/client/state/toasts.test.ts new file mode 100644 index 00000000..757e3d34 --- /dev/null +++ b/packages/hub-ui/src/client/state/toasts.test.ts @@ -0,0 +1,69 @@ +import type { DevframeMessageEntry } from '@devframes/hub' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { addToast, dismissToast, useToasts } from './toasts' + +function createMessage(id: string, autoDismiss: number | false): DevframeMessageEntry { + return { + id, + message: id, + level: 'info', + from: 'browser', + timestamp: 0, + autoDismiss, + } +} + +function dismissAllToasts(): void { + for (const toast of [...useToasts()]) + dismissToast(toast.id) +} + +describe('toast auto-dismiss', () => { + beforeEach(() => { + vi.useFakeTimers() + dismissAllToasts() + }) + + afterEach(() => { + dismissAllToasts() + vi.useRealTimers() + }) + + it('keeps a persistent toast visible until it is explicitly dismissed', () => { + addToast(createMessage('persistent', false)) + + expect(vi.getTimerCount()).toBe(0) + + vi.advanceTimersByTime(60_000) + + expect(useToasts()).toHaveLength(1) + + dismissToast('persistent') + + expect(useToasts()).toHaveLength(0) + }) + + it('cancels an existing auto-dismiss timer when its toast becomes persistent', () => { + addToast(createMessage('updated', 1_000)) + + expect(vi.getTimerCount()).toBe(1) + + addToast(createMessage('updated', false)) + + expect(vi.getTimerCount()).toBe(0) + + vi.advanceTimersByTime(60_000) + + expect(useToasts()).toHaveLength(1) + }) + + it('continues to auto-dismiss timed toasts', () => { + addToast(createMessage('timed', 1_000)) + + vi.advanceTimersByTime(999) + expect(useToasts()).toHaveLength(1) + + vi.advanceTimersByTime(1) + expect(useToasts()).toHaveLength(0) + }) +}) diff --git a/packages/hub-ui/src/client/state/toasts.ts b/packages/hub-ui/src/client/state/toasts.ts index 9c8ec24f..de2d8341 100644 --- a/packages/hub-ui/src/client/state/toasts.ts +++ b/packages/hub-ui/src/client/state/toasts.ts @@ -21,10 +21,13 @@ export function addToast(entry: DevframeMessageEntry): void { existing.entry = entry // Reset auto-dismiss timer const timer = timers.get(entry.id) - if (timer) + if (timer) { clearTimeout(timer) + timers.delete(entry.id) + } const timeout = entry.autoDismiss ?? 5000 - timers.set(entry.id, setTimeout(dismissToast, timeout, entry.id)) + if (timeout !== false) + timers.set(entry.id, setTimeout(dismissToast, timeout, entry.id)) return } @@ -32,7 +35,8 @@ export function addToast(entry: DevframeMessageEntry): void { toasts.push(item) const timeout = entry.autoDismiss ?? 5000 - timers.set(entry.id, setTimeout(dismissToast, timeout, entry.id)) + if (timeout !== false) + timers.set(entry.id, setTimeout(dismissToast, timeout, entry.id)) } export function dismissToast(id: string): void { diff --git a/packages/hub/src/types/messages.ts b/packages/hub/src/types/messages.ts index 28603377..fb9bf216 100644 --- a/packages/hub/src/types/messages.ts +++ b/packages/hub/src/types/messages.ts @@ -111,7 +111,7 @@ export interface DevframeMessageEntry { /** * Time in ms to auto-dismiss the toast notification (client-side) */ - autoDismiss?: number + autoDismiss?: number | false /** * Time in ms to auto-delete this message entry (server-side) */ diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts index 76e049e8..96010d69 100644 --- a/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts @@ -173,7 +173,7 @@ export interface DevframeMessageEntry { category?: string; labels?: string[]; actions?: DevframeMessageAction[]; - autoDismiss?: number; + autoDismiss?: number | false; autoDelete?: number; timestamp: number; status?: 'loading' | 'idle'; From 71628344d15159c10c0c13bfeb220e923847c6ac Mon Sep 17 00:00:00 2001 From: Dinh-Van Colomban Date: Wed, 26 Aug 2026 18:52:47 +0200 Subject: [PATCH 2/2] chore: fix jsdoc --- packages/hub/src/types/messages.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/hub/src/types/messages.ts b/packages/hub/src/types/messages.ts index fb9bf216..17f9a2fd 100644 --- a/packages/hub/src/types/messages.ts +++ b/packages/hub/src/types/messages.ts @@ -109,7 +109,8 @@ export interface DevframeMessageEntry { */ actions?: DevframeMessageAction[] /** - * Time in ms to auto-dismiss the toast notification (client-side) + * Time in ms to auto-dismiss the toast notification (client-side) or + * `false` to keep it indefinitely. */ autoDismiss?: number | false /**