diff --git a/sim/app/w/[id]/components/notifications/notifications.tsx b/sim/app/w/[id]/components/notifications/notifications.tsx index c477be9777..ea4c961c59 100644 --- a/sim/app/w/[id]/components/notifications/notifications.tsx +++ b/sim/app/w/[id]/components/notifications/notifications.tsx @@ -24,8 +24,43 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store' const logger = createLogger('Notifications') // Constants -const NOTIFICATION_TIMEOUT = 4000 -const FADE_DURATION = 300 +const NOTIFICATION_TIMEOUT = 4000 // Show notification for 4 seconds +const FADE_DURATION = 500 // Fade out over 500ms + +// Define keyframes for the animations in a style tag +const AnimationStyles = () => ( + +) // Icon mapping for notification types const NotificationIcon = { @@ -92,15 +127,23 @@ function DeleteApiConfirmation({ */ export function NotificationList() { // Store access - const { notifications, hideNotification } = useNotificationStore() + const { notifications, hideNotification, markAsRead, removeNotification } = useNotificationStore() const { activeWorkflowId } = useWorkflowRegistry() // Local state const [fadingNotifications, setFadingNotifications] = useState>(new Set()) + const [removedIds, setRemovedIds] = useState>(new Set()) - // Filter to only show visible notifications for the current workflow + // Filter to only show: + // 1. Visible notifications for the current workflow + // 2. That are either unread OR marked as persistent + // 3. And have not been marked for removal const visibleNotifications = notifications.filter( - (n) => n.isVisible && n.workflowId === activeWorkflowId + (n) => + n.isVisible && + n.workflowId === activeWorkflowId && + (!n.read || n.options?.isPersistent) && + !removedIds.has(n.id) ) // Handle auto-dismissal of non-persistent notifications @@ -117,9 +160,14 @@ export function NotificationList() { setFadingNotifications((prev) => new Set([...prev, notification.id])) }, NOTIFICATION_TIMEOUT) - // Hide notification after fade completes + // Hide notification after fade completes and mark for removal from DOM const hideTimer = setTimeout(() => { hideNotification(notification.id) + markAsRead(notification.id) + + // Mark this notification ID as removed to exclude it from rendering + setRemovedIds((prev) => new Set([...prev, notification.id])) + setFadingNotifications((prev) => { const next = new Set(prev) next.delete(notification.id) @@ -132,28 +180,45 @@ export function NotificationList() { // Cleanup timers on unmount or when notifications change return () => timers.forEach(clearTimeout) - }, [visibleNotifications, hideNotification]) + }, [visibleNotifications, hideNotification, markAsRead]) // Early return if no notifications to show if (visibleNotifications.length === 0) return null return ( -
- {visibleNotifications.map((notification) => ( - - ))} -
+ <> + +
+ {visibleNotifications.map((notification) => ( + { + hideNotification(id) + markAsRead(id) + // Start the fade out animation + setFadingNotifications((prev) => new Set([...prev, id])) + // Remove from DOM after animation completes + setTimeout(() => { + setRemovedIds((prev) => new Set([...prev, id])) + setFadingNotifications((prev) => { + const next = new Set(prev) + next.delete(id) + return next + }) + }, FADE_DURATION) + }} + /> + ))} +
+ ) } @@ -168,13 +233,14 @@ interface NotificationAlertProps { function NotificationAlert({ notification, isFading, onHide }: NotificationAlertProps) { const { id, type, message, options, workflowId } = notification - const Icon = NotificationIcon[type] const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) const { setDeploymentStatus } = useWorkflowStore() const { isDeployed } = useWorkflowStore((state) => ({ isDeployed: state.isDeployed, })) + const Icon = NotificationIcon[type] + const handleDeleteApi = async () => { if (!workflowId) return @@ -202,8 +268,10 @@ function NotificationAlert({ notification, isFading, onHide }: NotificationAlert <> diff --git a/sim/app/w/[id]/workflow.tsx b/sim/app/w/[id]/workflow.tsx index f9180af53b..64dc04f2de 100644 --- a/sim/app/w/[id]/workflow.tsx +++ b/sim/app/w/[id]/workflow.tsx @@ -12,6 +12,7 @@ import ReactFlow, { } from 'reactflow' import 'reactflow/dist/style.css' import { createLogger } from '@/lib/logs/console-logger' +import { useNotificationStore } from '@/stores/notifications/store' import { useGeneralStore } from '@/stores/settings/general/store' import { initializeSyncManagers, isSyncInitialized } from '@/stores/sync-registry' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' @@ -51,6 +52,7 @@ function WorkflowContent() { const { blocks, edges, loops, addBlock, updateBlockPosition, addEdge, removeEdge } = useWorkflowStore() const { setValue: setSubBlockValue } = useSubBlockStore() + const { markAllAsRead } = useNotificationStore() // Initialize workflow useEffect(() => { @@ -157,16 +159,26 @@ function WorkflowContent() { if (!isActivelyLoadingFromDB()) { clearInterval(checkInterval) setActiveWorkflow(currentId) + markAllAsRead(currentId) } }, 100) return } setActiveWorkflow(currentId) + markAllAsRead(currentId) } validateAndNavigate() - }, [params.id, workflows, setActiveWorkflow, createWorkflow, router, isInitialized]) + }, [ + params.id, + workflows, + setActiveWorkflow, + createWorkflow, + router, + isInitialized, + markAllAsRead, + ]) // Transform blocks and loops into ReactFlow nodes const nodes = useMemo(() => { diff --git a/sim/stores/notifications/store.ts b/sim/stores/notifications/store.ts index 6f12a3161b..578ef6a6a7 100644 --- a/sim/stores/notifications/store.ts +++ b/sim/stores/notifications/store.ts @@ -34,6 +34,7 @@ export const useNotificationStore = create()( message, timestamp: Date.now(), isVisible: true, + read: false, workflowId, options, } @@ -52,7 +53,7 @@ export const useNotificationStore = create()( hideNotification: (id) => set((state) => { const newNotifications = state.notifications.map((n) => - n.id === id ? { ...n, isVisible: false } : n + n.id === id ? { ...n, isVisible: false, read: true } : n ) persistNotifications(newNotifications) return { notifications: newNotifications } @@ -67,6 +68,24 @@ export const useNotificationStore = create()( return { notifications: newNotifications } }), + markAsRead: (id) => + set((state) => { + const newNotifications = state.notifications.map((n) => + n.id === id ? { ...n, read: true } : n + ) + persistNotifications(newNotifications) + return { notifications: newNotifications } + }), + + markAllAsRead: (workflowId) => + set((state) => { + const newNotifications = state.notifications.map((n) => + n.workflowId === workflowId ? { ...n, read: true } : n + ) + persistNotifications(newNotifications) + return { notifications: newNotifications } + }), + removeNotification: (id) => set((state) => { const newNotifications = state.notifications.filter((n) => n.id !== id) diff --git a/sim/stores/notifications/types.ts b/sim/stores/notifications/types.ts index 64aa7f8abb..d94a17fd80 100644 --- a/sim/stores/notifications/types.ts +++ b/sim/stores/notifications/types.ts @@ -7,6 +7,7 @@ export interface Notification { timestamp: number isVisible: boolean workflowId: string | null + read: boolean options?: NotificationOptions } @@ -31,6 +32,8 @@ export interface NotificationStore { ) => void hideNotification: (id: string) => void showNotification: (id: string) => void + markAsRead: (id: string) => void + markAllAsRead: (workflowId: string) => void removeNotification: (id: string) => void clearNotifications: () => void getWorkflowNotifications: (workflowId: string) => Notification[]