|
1 | 1 | 'use client' |
2 | 2 |
|
3 | | -import { createContext, type ReactNode, useContext, useState } from 'react' |
| 3 | +import { createContext, type ReactNode, useContext, useEffect, useState } from 'react' |
| 4 | +import { generateId } from '@sim/utils/id' |
4 | 5 | import type { ShellNotice } from '@/platform/app/(shell)/_components/model' |
5 | 6 | import styles from '@/platform/app/(shell)/_components/notification-tray/notification-tray.module.css' |
6 | 7 | import { ActivityBody } from '@/platform/design-system/components/activity-row/activity-body' |
7 | 8 | import { Button, IconButton } from '@/platform/design-system/components/button/button' |
8 | 9 | import { useFeedbackExpiry } from '@/platform/design-system/components/feedback/use-feedback-expiry' |
9 | 10 | import { Icon } from '@/platform/design-system/components/icon/icon' |
10 | 11 | import { useScrollEdges } from '@/platform/design-system/components/scroll-area/use-scroll-edges' |
| 12 | +import { connectToastSink } from '@/platform/lib/data/toast' |
11 | 13 |
|
12 | | -type Notice = Omit<ShellNotice, 'href'> & { onAction: () => void } |
| 14 | +type Notice = Omit<ShellNotice, 'href'> & { onAction?: () => void } |
13 | 15 | function useNoticeState() { |
14 | 16 | const [edits, setEdits] = useState(() => new Map<string, Notice | null>()) |
15 | 17 | const [display, setDisplay] = useState<'hidden' | 'latest' | 'expanded' | 'minimized'>('hidden') |
@@ -41,11 +43,37 @@ export function NotificationSession({ children }: { children: ReactNode }) { |
41 | 43 | return <Notices.Provider value={state}>{children}</Notices.Provider> |
42 | 44 | } |
43 | 45 |
|
| 46 | +/** Delivers the toasts existing query hooks raise, as action-free notices unless they carry one. */ |
| 47 | +function useToastNotices() { |
| 48 | + const { notify, dismiss } = usePlatformNotices() |
| 49 | + useEffect( |
| 50 | + () => |
| 51 | + connectToastSink({ |
| 52 | + show({ message, description, variant, action }) { |
| 53 | + const id = generateId() |
| 54 | + notify({ |
| 55 | + id, |
| 56 | + title: message, |
| 57 | + description, |
| 58 | + icon: variant === 'error' || variant === 'warning' ? 'I047' : 'I106', |
| 59 | + action: action?.label, |
| 60 | + onAction: action?.onClick, |
| 61 | + }) |
| 62 | + return id |
| 63 | + }, |
| 64 | + dismiss, |
| 65 | + }), |
| 66 | + // notify and dismiss only write state, so the first render's pair stays correct. |
| 67 | + [] |
| 68 | + ) |
| 69 | +} |
| 70 | + |
44 | 71 | interface NotificationTrayProps { |
45 | 72 | history: readonly ShellNotice[] |
46 | 73 | navigate: (href: string) => void |
47 | 74 | } |
48 | 75 | export function NotificationTray({ history, navigate }: NotificationTrayProps) { |
| 76 | + useToastNotices() |
49 | 77 | const scrollEdges = useScrollEdges<HTMLDivElement>() |
50 | 78 | const { edits, display, setDisplay, dismiss } = usePlatformNotices() |
51 | 79 | const items = [ |
@@ -119,16 +147,18 @@ export function NotificationTray({ history, navigate }: NotificationTrayProps) { |
119 | 147 | )} |
120 | 148 | </ActivityBody> |
121 | 149 | <div className={styles.actions}> |
122 | | - <Button |
123 | | - size='compact' |
124 | | - onClick={() => { |
125 | | - dismiss(item.id) |
126 | | - if ('onAction' in item) item.onAction() |
127 | | - else if (item.href) navigate(item.href) |
128 | | - }} |
129 | | - > |
130 | | - {item.action} |
131 | | - </Button> |
| 150 | + {item.action && ( |
| 151 | + <Button |
| 152 | + size='compact' |
| 153 | + onClick={() => { |
| 154 | + dismiss(item.id) |
| 155 | + if ('onAction' in item) item.onAction?.() |
| 156 | + else if (item.href) navigate(item.href) |
| 157 | + }} |
| 158 | + > |
| 159 | + {item.action} |
| 160 | + </Button> |
| 161 | + )} |
132 | 162 | <IconButton |
133 | 163 | size='compact' |
134 | 164 | label={`Dismiss ${item.title}`} |
|
0 commit comments