Skip to content

Commit fb28a63

Browse files
committed
fix(notifications): clear on workflow load; remove from DOM after fade
1 parent 02e7a96 commit fb28a63

File tree

4 files changed

+130
-28
lines changed

4 files changed

+130
-28
lines changed

sim/app/w/[id]/components/notifications/notifications.tsx

Lines changed: 94 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,43 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store'
2424
const logger = createLogger('Notifications')
2525

2626
// Constants
27-
const NOTIFICATION_TIMEOUT = 4000
28-
const FADE_DURATION = 300
27+
const NOTIFICATION_TIMEOUT = 4000 // Show notification for 4 seconds
28+
const FADE_DURATION = 500 // Fade out over 500ms
29+
30+
// Define keyframes for the animations in a style tag
31+
const AnimationStyles = () => (
32+
<style jsx global>{`
33+
@keyframes notification-slide {
34+
0% {
35+
opacity: 0;
36+
transform: translateY(-100%);
37+
}
38+
100% {
39+
opacity: 1;
40+
transform: translateY(0);
41+
}
42+
}
43+
44+
@keyframes notification-fade-out {
45+
0% {
46+
opacity: 1;
47+
transform: translateY(0);
48+
}
49+
100% {
50+
opacity: 0;
51+
transform: translateY(-10%);
52+
}
53+
}
54+
55+
.animate-notification-slide {
56+
animation: notification-slide 300ms ease forwards;
57+
}
58+
59+
.animate-notification-fade-out {
60+
animation: notification-fade-out ${FADE_DURATION}ms ease forwards;
61+
}
62+
`}</style>
63+
)
2964

3065
// Icon mapping for notification types
3166
const NotificationIcon = {
@@ -92,15 +127,23 @@ function DeleteApiConfirmation({
92127
*/
93128
export function NotificationList() {
94129
// Store access
95-
const { notifications, hideNotification } = useNotificationStore()
130+
const { notifications, hideNotification, markAsRead, removeNotification } = useNotificationStore()
96131
const { activeWorkflowId } = useWorkflowRegistry()
97132

98133
// Local state
99134
const [fadingNotifications, setFadingNotifications] = useState<Set<string>>(new Set())
135+
const [removedIds, setRemovedIds] = useState<Set<string>>(new Set())
100136

101-
// Filter to only show visible notifications for the current workflow
137+
// Filter to only show:
138+
// 1. Visible notifications for the current workflow
139+
// 2. That are either unread OR marked as persistent
140+
// 3. And have not been marked for removal
102141
const visibleNotifications = notifications.filter(
103-
(n) => n.isVisible && n.workflowId === activeWorkflowId
142+
(n) =>
143+
n.isVisible &&
144+
n.workflowId === activeWorkflowId &&
145+
(!n.read || n.options?.isPersistent) &&
146+
!removedIds.has(n.id)
104147
)
105148

106149
// Handle auto-dismissal of non-persistent notifications
@@ -117,9 +160,14 @@ export function NotificationList() {
117160
setFadingNotifications((prev) => new Set([...prev, notification.id]))
118161
}, NOTIFICATION_TIMEOUT)
119162

120-
// Hide notification after fade completes
163+
// Hide notification after fade completes and mark for removal from DOM
121164
const hideTimer = setTimeout(() => {
122165
hideNotification(notification.id)
166+
markAsRead(notification.id)
167+
168+
// Mark this notification ID as removed to exclude it from rendering
169+
setRemovedIds((prev) => new Set([...prev, notification.id]))
170+
123171
setFadingNotifications((prev) => {
124172
const next = new Set(prev)
125173
next.delete(notification.id)
@@ -132,28 +180,45 @@ export function NotificationList() {
132180

133181
// Cleanup timers on unmount or when notifications change
134182
return () => timers.forEach(clearTimeout)
135-
}, [visibleNotifications, hideNotification])
183+
}, [visibleNotifications, hideNotification, markAsRead])
136184

137185
// Early return if no notifications to show
138186
if (visibleNotifications.length === 0) return null
139187

140188
return (
141-
<div
142-
className="absolute left-1/2 z-50 space-y-2 max-w-lg w-full"
143-
style={{
144-
top: '30px',
145-
transform: 'translateX(-50%)',
146-
}}
147-
>
148-
{visibleNotifications.map((notification) => (
149-
<NotificationAlert
150-
key={notification.id}
151-
notification={notification}
152-
isFading={fadingNotifications.has(notification.id)}
153-
onHide={hideNotification}
154-
/>
155-
))}
156-
</div>
189+
<>
190+
<AnimationStyles />
191+
<div
192+
className="absolute left-1/2 z-50 space-y-2 max-w-lg w-full pointer-events-none"
193+
style={{
194+
top: '30px',
195+
transform: 'translateX(-50%)',
196+
}}
197+
>
198+
{visibleNotifications.map((notification) => (
199+
<NotificationAlert
200+
key={notification.id}
201+
notification={notification}
202+
isFading={fadingNotifications.has(notification.id)}
203+
onHide={(id) => {
204+
hideNotification(id)
205+
markAsRead(id)
206+
// Start the fade out animation
207+
setFadingNotifications((prev) => new Set([...prev, id]))
208+
// Remove from DOM after animation completes
209+
setTimeout(() => {
210+
setRemovedIds((prev) => new Set([...prev, id]))
211+
setFadingNotifications((prev) => {
212+
const next = new Set(prev)
213+
next.delete(id)
214+
return next
215+
})
216+
}, FADE_DURATION)
217+
}}
218+
/>
219+
))}
220+
</div>
221+
</>
157222
)
158223
}
159224

@@ -168,13 +233,14 @@ interface NotificationAlertProps {
168233

169234
function NotificationAlert({ notification, isFading, onHide }: NotificationAlertProps) {
170235
const { id, type, message, options, workflowId } = notification
171-
const Icon = NotificationIcon[type]
172236
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
173237
const { setDeploymentStatus } = useWorkflowStore()
174238
const { isDeployed } = useWorkflowStore((state) => ({
175239
isDeployed: state.isDeployed,
176240
}))
177241

242+
const Icon = NotificationIcon[type]
243+
178244
const handleDeleteApi = async () => {
179245
if (!workflowId) return
180246

@@ -202,8 +268,10 @@ function NotificationAlert({ notification, isFading, onHide }: NotificationAlert
202268
<>
203269
<Alert
204270
className={cn(
205-
'transition-all duration-300 ease-in-out opacity-0 translate-y-[-100%]',
206-
isFading ? 'animate-notification-fade-out' : 'animate-notification-slide',
271+
'transition-all duration-300 ease-in-out opacity-0 translate-y-[-100%] pointer-events-auto',
272+
isFading
273+
? 'animate-notification-fade-out pointer-events-none'
274+
: 'animate-notification-slide',
207275
NotificationColors[type]
208276
)}
209277
>

sim/app/w/[id]/workflow.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import ReactFlow, {
1212
} from 'reactflow'
1313
import 'reactflow/dist/style.css'
1414
import { createLogger } from '@/lib/logs/console-logger'
15+
import { useNotificationStore } from '@/stores/notifications/store'
1516
import { useGeneralStore } from '@/stores/settings/general/store'
1617
import { initializeSyncManagers, isSyncInitialized } from '@/stores/sync-registry'
1718
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -51,6 +52,7 @@ function WorkflowContent() {
5152
const { blocks, edges, loops, addBlock, updateBlockPosition, addEdge, removeEdge } =
5253
useWorkflowStore()
5354
const { setValue: setSubBlockValue } = useSubBlockStore()
55+
const { markAllAsRead } = useNotificationStore()
5456

5557
// Initialize workflow
5658
useEffect(() => {
@@ -157,16 +159,26 @@ function WorkflowContent() {
157159
if (!isActivelyLoadingFromDB()) {
158160
clearInterval(checkInterval)
159161
setActiveWorkflow(currentId)
162+
markAllAsRead(currentId)
160163
}
161164
}, 100)
162165
return
163166
}
164167

165168
setActiveWorkflow(currentId)
169+
markAllAsRead(currentId)
166170
}
167171

168172
validateAndNavigate()
169-
}, [params.id, workflows, setActiveWorkflow, createWorkflow, router, isInitialized])
173+
}, [
174+
params.id,
175+
workflows,
176+
setActiveWorkflow,
177+
createWorkflow,
178+
router,
179+
isInitialized,
180+
markAllAsRead,
181+
])
170182

171183
// Transform blocks and loops into ReactFlow nodes
172184
const nodes = useMemo(() => {

sim/stores/notifications/store.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export const useNotificationStore = create<NotificationStore>()(
3434
message,
3535
timestamp: Date.now(),
3636
isVisible: true,
37+
read: false,
3738
workflowId,
3839
options,
3940
}
@@ -52,7 +53,7 @@ export const useNotificationStore = create<NotificationStore>()(
5253
hideNotification: (id) =>
5354
set((state) => {
5455
const newNotifications = state.notifications.map((n) =>
55-
n.id === id ? { ...n, isVisible: false } : n
56+
n.id === id ? { ...n, isVisible: false, read: true } : n
5657
)
5758
persistNotifications(newNotifications)
5859
return { notifications: newNotifications }
@@ -67,6 +68,24 @@ export const useNotificationStore = create<NotificationStore>()(
6768
return { notifications: newNotifications }
6869
}),
6970

71+
markAsRead: (id) =>
72+
set((state) => {
73+
const newNotifications = state.notifications.map((n) =>
74+
n.id === id ? { ...n, read: true } : n
75+
)
76+
persistNotifications(newNotifications)
77+
return { notifications: newNotifications }
78+
}),
79+
80+
markAllAsRead: (workflowId) =>
81+
set((state) => {
82+
const newNotifications = state.notifications.map((n) =>
83+
n.workflowId === workflowId ? { ...n, read: true } : n
84+
)
85+
persistNotifications(newNotifications)
86+
return { notifications: newNotifications }
87+
}),
88+
7089
removeNotification: (id) =>
7190
set((state) => {
7291
const newNotifications = state.notifications.filter((n) => n.id !== id)

sim/stores/notifications/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface Notification {
77
timestamp: number
88
isVisible: boolean
99
workflowId: string | null
10+
read: boolean
1011
options?: NotificationOptions
1112
}
1213

@@ -31,6 +32,8 @@ export interface NotificationStore {
3132
) => void
3233
hideNotification: (id: string) => void
3334
showNotification: (id: string) => void
35+
markAsRead: (id: string) => void
36+
markAllAsRead: (workflowId: string) => void
3437
removeNotification: (id: string) => void
3538
clearNotifications: () => void
3639
getWorkflowNotifications: (workflowId: string) => Notification[]

0 commit comments

Comments
 (0)