Added persistence and localization of notifications to each workflow

This commit is contained in:
Emir Karabeg
2025-01-29 13:49:35 -08:00
parent e336aea733
commit 47dc2e78dc
6 changed files with 84 additions and 34 deletions

View File

@@ -115,6 +115,8 @@ function WorkflowCanvas() {
} = useWorkflowStore()
const { addNotification } = useNotificationStore()
const params = useParams()
// Convert blocks to ReactFlow nodes using local selectedBlockId
const nodes = Object.values(blocks).map((block) => ({
id: block.id,
@@ -256,10 +258,6 @@ function WorkflowCanvas() {
return () => window.removeEventListener('keydown', handleKeyDown)
}, [canUndo, canRedo, undo, redo])
// useEffect(() => {
// addNotification('console', 'Welcome to the workflow editor!')
// }, [])
return (
<div className="relative w-full h-[calc(100vh-4rem)]">
<NotificationList />

View File

@@ -8,6 +8,7 @@ import {
import { useNotificationStore } from '@/stores/notifications/notifications-store'
import { cn } from '@/lib/utils'
import { ErrorIcon } from '@/components/icons'
import { useState, useEffect } from 'react'
interface NotificationDropdownItemProps {
id: string
@@ -34,6 +35,14 @@ export function NotificationDropdownItem({
}: NotificationDropdownItemProps) {
const { showNotification } = useNotificationStore()
const Icon = NotificationIcon[type]
const [, forceUpdate] = useState({})
// Update the time display every minute
useEffect(() => {
const interval = setInterval(() => forceUpdate({}), 60000)
return () => clearInterval(interval)
}, [])
const timeAgo = formatDistanceToNow(timestamp, { addSuffix: true })
// Truncate message if it's too long

View File

@@ -19,13 +19,18 @@ import { useWorkflowRegistry } from '@/stores/workflow/workflow-registry'
import { useRouter } from 'next/navigation'
export function ControlBar() {
const { notifications } = useNotificationStore()
const { notifications, getWorkflowNotifications } = useNotificationStore()
const { history, undo, redo } = useWorkflowStore()
const [, forceUpdate] = useState({})
const { isExecuting, handleRunWorkflow } = useWorkflowExecution()
const { workflows, removeWorkflow, activeWorkflowId } = useWorkflowRegistry()
const router = useRouter()
// Get notifications for current workflow
const workflowNotifications = activeWorkflowId
? getWorkflowNotifications(activeWorkflowId)
: notifications // Show all if no workflow is active
const handleDeleteWorkflow = () => {
if (!activeWorkflowId) return
const newWorkflows = { ...workflows }
@@ -120,7 +125,7 @@ export function ControlBar() {
</Button>
</DropdownMenuTrigger>
{notifications.length === 0 ? (
{workflowNotifications.length === 0 ? (
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem className="text-sm text-muted-foreground">
No new notifications
@@ -128,15 +133,12 @@ export function ControlBar() {
</DropdownMenuContent>
) : (
<DropdownMenuContent align="end" className="w-60">
{[...notifications]
{[...workflowNotifications]
.sort((a, b) => b.timestamp - a.timestamp)
.map((notification) => (
<NotificationDropdownItem
id={notification.id}
key={notification.id}
type={notification.type}
message={notification.message}
timestamp={notification.timestamp}
{...notification}
/>
))}
</DropdownMenuContent>

View File

@@ -4,11 +4,13 @@ import { Serializer } from '@/serializer'
import { Executor } from '@/executor'
import { ExecutionResult } from '@/executor/types'
import { useNotificationStore } from '@/stores/notifications/notifications-store'
import { useWorkflowRegistry } from '@/stores/workflow/workflow-registry'
export function useWorkflowExecution() {
const [isExecuting, setIsExecuting] = useState(false)
const [executionResult, setExecutionResult] = useState<ExecutionResult | null>(null)
const { blocks, edges } = useWorkflowStore()
const { activeWorkflowId } = useWorkflowRegistry()
const { addNotification } = useNotificationStore()
const handleRunWorkflow = useCallback(async () => {
@@ -31,12 +33,13 @@ export function useWorkflowExecution() {
const result = await executor.execute(crypto.randomUUID())
setExecutionResult(result)
// Show execution result
// Show execution result with workflowId
addNotification(
result.success ? 'console' : 'error',
result.success
? 'Workflow completed successfully'
: `Failed to execute workflow: ${result.error}`
: `Failed to execute workflow: ${result.error}`,
activeWorkflowId
)
// Log detailed result to console
@@ -58,11 +61,11 @@ export function useWorkflowExecution() {
data: {},
error: errorMessage
})
addNotification('error', `Failed to execute workflow: ${errorMessage}`)
addNotification('error', `Failed to execute workflow: ${errorMessage}`, activeWorkflowId)
} finally {
setIsExecuting(false)
}
}, [blocks, edges, addNotification])
}, [blocks, edges, addNotification, activeWorkflowId])
return { isExecuting, executionResult, handleRunWorkflow }
}

View File

@@ -3,39 +3,75 @@ import { devtools } from 'zustand/middleware'
import { NotificationType, Notification, NotificationStore } from './types'
import { getTimestamp } from '@/lib/utils'
const STORAGE_KEY = 'workflow-notifications'
// Helper to load persisted notifications
const loadPersistedNotifications = (): Notification[] => {
if (typeof window === 'undefined') return []
const saved = localStorage.getItem(STORAGE_KEY)
return saved ? JSON.parse(saved) : []
}
// Helper to save notifications to localStorage
const persistNotifications = (notifications: Notification[]) => {
if (typeof window === 'undefined') return
localStorage.setItem(STORAGE_KEY, JSON.stringify(notifications))
}
export const useNotificationStore = create<NotificationStore>()(
devtools(
(set) => ({
notifications: [],
addNotification: (type, message) => {
(set, get) => ({
notifications: loadPersistedNotifications(),
addNotification: (type, message, workflowId) => {
const notification: Notification = {
id: typeof window === 'undefined' ? '1' : crypto.randomUUID(),
type,
message,
timestamp: getTimestamp(),
isVisible: true,
workflowId
}
set((state) => ({
notifications: [...state.notifications, notification],
}))
set((state) => {
const newNotifications = [...state.notifications, notification]
persistNotifications(newNotifications)
return { notifications: newNotifications }
})
},
hideNotification: (id) =>
set((state) => ({
notifications: state.notifications.map((n) =>
set((state) => {
const newNotifications = state.notifications.map((n) =>
n.id === id ? { ...n, isVisible: false } : n
),
})),
)
persistNotifications(newNotifications)
return { notifications: newNotifications }
}),
showNotification: (id) =>
set((state) => ({
notifications: state.notifications.map((n) =>
set((state) => {
const newNotifications = state.notifications.map((n) =>
n.id === id ? { ...n, isVisible: true } : n
),
})),
)
persistNotifications(newNotifications)
return { notifications: newNotifications }
}),
removeNotification: (id) =>
set((state) => ({
notifications: state.notifications.filter((n) => n.id !== id),
})),
clearNotifications: () => set({ notifications: [] }),
set((state) => {
const newNotifications = state.notifications.filter((n) => n.id !== id)
persistNotifications(newNotifications)
return { notifications: newNotifications }
}),
clearNotifications: () => {
persistNotifications([])
set({ notifications: [] })
},
getWorkflowNotifications: (workflowId) => {
return get().notifications.filter(n => n.workflowId === workflowId)
}
}),
{ name: 'notification-store' }
)

View File

@@ -6,13 +6,15 @@ export interface Notification {
message: string
timestamp: number
isVisible: boolean
workflowId: string | null
}
export interface NotificationStore {
notifications: Notification[]
addNotification: (type: NotificationType, message: string) => void
addNotification: (type: NotificationType, message: string, workflowId: string | null) => void
hideNotification: (id: string) => void
showNotification: (id: string) => void
removeNotification: (id: string) => void
clearNotifications: () => void
getWorkflowNotifications: (workflowId: string) => Notification[]
}