fix(deployed-workflow): fixed the deployment status changes across workflows. (#382)

* fixed deployment state

* fix: moved the deployment status logic to registry store

* removed head

* fix: changes detected for existing deployment

* fix: auto changes detected

* fix: rid of debugging

* fix: added greptile comments

* removed logic for change detection to hook to reduce load on control bar

---------

Co-authored-by: Adam Gough <adamgough@Adams-MacBook-Pro.local>
Co-authored-by: Adam Gough <adamgough@Mac.lan>
Co-authored-by: Waleed Latif <walif6@gmail.com>
This commit is contained in:
Adam Gough
2025-05-21 20:19:05 -07:00
committed by GitHub
parent afcc66afc6
commit 328d361a70
12 changed files with 695 additions and 259 deletions

View File

@@ -23,6 +23,7 @@ import { createLogger } from '@/lib/logs/console-logger'
import { cn } from '@/lib/utils'
import { useNotificationStore } from '@/stores/notifications/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import { ChatDeploy } from '@/app/w/[id]/components/control-bar/components/deploy-modal/components/chat-deploy/chat-deploy'
import { DeployForm } from '@/app/w/[id]/components/control-bar/components/deploy-modal/components/deploy-form/deploy-form'
@@ -72,7 +73,11 @@ export function DeployModal({
}: DeployModalProps) {
// Store hooks
const { addNotification } = useNotificationStore()
const { isDeployed, setDeploymentStatus } = useWorkflowStore()
// Use registry store for deployment-related functions
const deploymentStatus = useWorkflowRegistry(state => state.getWorkflowDeploymentStatus(workflowId))
const isDeployed = deploymentStatus?.isDeployed || false
const setDeploymentStatus = useWorkflowRegistry(state => state.setDeploymentStatus)
// Local state
const [isSubmitting, setIsSubmitting] = useState(false)
@@ -80,7 +85,6 @@ export function DeployModal({
const [deploymentInfo, setDeploymentInfo] = useState<DeploymentInfo | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [apiKeys, setApiKeys] = useState<ApiKey[]>([])
const [isCreatingKey, setIsCreatingKey] = useState(false)
const [keysLoaded, setKeysLoaded] = useState(false)
const [activeTab, setActiveTab] = useState<TabView>('api')
const [isChatDeploying, setIsChatDeploying] = useState(false)
@@ -273,10 +277,13 @@ export function DeployModal({
const { isDeployed: newDeployStatus, deployedAt } = await response.json()
// Update the store with the deployment status
setDeploymentStatus(newDeployStatus, deployedAt ? new Date(deployedAt) : undefined)
setDeploymentStatus(workflowId, newDeployStatus, deployedAt ? new Date(deployedAt) : undefined, data.apiKey)
// Reset the needs redeployment flag
setNeedsRedeployment(false)
if (workflowId) {
useWorkflowRegistry.getState().setWorkflowNeedsRedeployment(workflowId, false)
}
// Update the local deployment info
const endpoint = `${env.NEXT_PUBLIC_APP_URL}/api/workflows/${workflowId}/execute`
@@ -322,7 +329,7 @@ export function DeployModal({
}
// Update deployment status in the store
setDeploymentStatus(false)
setDeploymentStatus(workflowId, false)
// Reset chat deployment info
setDeployedChatUrl(null)
@@ -366,13 +373,16 @@ export function DeployModal({
throw new Error(errorData.error || 'Failed to redeploy workflow')
}
const { isDeployed: newDeployStatus, deployedAt } = await response.json()
const { isDeployed: newDeployStatus, deployedAt, apiKey } = await response.json()
// Update deployment status in the store
setDeploymentStatus(newDeployStatus, deployedAt ? new Date(deployedAt) : undefined)
setDeploymentStatus(workflowId, newDeployStatus, deployedAt ? new Date(deployedAt) : undefined, apiKey)
// Reset the needs redeployment flag
setNeedsRedeployment(false)
if (workflowId) {
useWorkflowRegistry.getState().setWorkflowNeedsRedeployment(workflowId, false)
}
// Add a success notification
addNotification('info', 'Workflow successfully redeployed', workflowId)
@@ -475,10 +485,10 @@ export function DeployModal({
throw new Error(errorData.error || 'Failed to deploy workflow')
}
const { isDeployed: newDeployStatus, deployedAt } = await response.json()
const { isDeployed: newDeployStatus, deployedAt, apiKey } = await response.json()
// Update the store with the deployment status
setDeploymentStatus(newDeployStatus, deployedAt ? new Date(deployedAt) : undefined)
setDeploymentStatus(workflowId, newDeployStatus, deployedAt ? new Date(deployedAt) : undefined, apiKey)
logger.info('Workflow automatically deployed for chat deployment')
} catch (error: any) {

View File

@@ -7,19 +7,16 @@ import { cn } from '@/lib/utils'
import { WorkflowPreview } from '@/app/w/components/workflow-preview/generic-workflow-preview'
interface DeployedWorkflowCardProps {
// Current workflow state (if any)
currentWorkflowState?: {
blocks: Record<string, any>
edges: Array<any>
loops: Record<string, any>
}
// Deployed workflow state from Supabase
deployedWorkflowState: {
blocks: Record<string, any>
edges: Array<any>
loops: Record<string, any>
}
// Optional className for styling
className?: string
}
@@ -28,15 +25,20 @@ export function DeployedWorkflowCard({
deployedWorkflowState,
className,
}: DeployedWorkflowCardProps) {
// State for toggling between deployed and current workflow
const [showingDeployed, setShowingDeployed] = useState(true)
// Determine which workflow state to show
const workflowToShow = showingDeployed ? deployedWorkflowState : currentWorkflowState
return (
<Card className={cn('overflow-hidden', className)}>
<CardHeader className="space-y-4 p-4">
<Card className={cn('overflow-hidden relative', className)}>
<CardHeader
className={cn(
'space-y-4 p-4 sticky top-0 z-10',
'backdrop-blur-xl',
'bg-background/70 dark:bg-background/50',
'border-b border-border/30 dark:border-border/20',
'shadow-sm'
)}
>
<div className="flex items-center justify-between">
<h3 className="font-medium">
{showingDeployed ? 'Deployed Workflow' : 'Current Workflow'}

View File

@@ -13,13 +13,7 @@ import {
AlertDialogTrigger,
} from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { mergeSubblockState } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
@@ -62,8 +56,6 @@ export function DeployedWorkflowModal({
<DialogContent
className="sm:max-w-[1100px] max-h-[100vh] overflow-y-auto"
style={{ zIndex: 1000 }}
onInteractOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => e.preventDefault()}
hideCloseButton={true}
>
<div className="sr-only">

View File

@@ -1,12 +1,12 @@
'use client'
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { Loader2, Rocket } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { createLogger } from '@/lib/logs/console-logger'
import { cn } from '@/lib/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { DeployModal } from '../deploy-modal/deploy-modal'
const logger = createLogger('DeploymentControls')
@@ -22,10 +22,27 @@ export function DeploymentControls({
needsRedeployment,
setNeedsRedeployment,
}: DeploymentControlsProps) {
const { isDeployed } = useWorkflowStore()
// Use workflow-specific deployment status
const deploymentStatus = useWorkflowRegistry(state =>
state.getWorkflowDeploymentStatus(activeWorkflowId))
const isDeployed = deploymentStatus?.isDeployed || false
// Prioritize workflow-specific needsRedeployment flag, but fall back to prop if needed
const workflowNeedsRedeployment = deploymentStatus?.needsRedeployment !== undefined
? deploymentStatus.needsRedeployment
: needsRedeployment
const [isDeploying, setIsDeploying] = useState(false)
const [isModalOpen, setIsModalOpen] = useState(false)
// Update parent component when workflow-specific status changes
useEffect(() => {
if (deploymentStatus?.needsRedeployment !== undefined &&
deploymentStatus.needsRedeployment !== needsRedeployment) {
setNeedsRedeployment(deploymentStatus.needsRedeployment)
}
}, [deploymentStatus?.needsRedeployment, needsRedeployment, setNeedsRedeployment, deploymentStatus])
return (
<>
<Tooltip>
@@ -46,7 +63,7 @@ export function DeploymentControls({
<span className="sr-only">Deploy API</span>
</Button>
{isDeployed && needsRedeployment && (
{isDeployed && workflowNeedsRedeployment && (
<div className="absolute top-0.5 right-0.5 flex items-center justify-center">
<div className="relative">
<div className="absolute inset-0 w-2 h-2 rounded-full bg-amber-500/50 animate-ping"></div>
@@ -60,7 +77,7 @@ export function DeploymentControls({
<TooltipContent>
{isDeploying
? 'Deploying...'
: isDeployed && needsRedeployment
: isDeployed && workflowNeedsRedeployment
? 'Workflow changes detected'
: isDeployed
? 'Deployment Settings'
@@ -72,7 +89,7 @@ export function DeploymentControls({
open={isModalOpen}
onOpenChange={setIsModalOpen}
workflowId={activeWorkflowId}
needsRedeployment={needsRedeployment}
needsRedeployment={workflowNeedsRedeployment}
setNeedsRedeployment={setNeedsRedeployment}
/>
</>

View File

@@ -8,13 +8,11 @@ import {
Bug,
ChevronDown,
Copy,
CreditCard,
History,
Loader2,
Play,
SkipForward,
StepForward,
Store,
Trash2,
X,
} from 'lucide-react'
@@ -45,14 +43,13 @@ import { useExecutionStore } from '@/stores/execution/store'
import { useNotificationStore } from '@/stores/notifications/store'
import { usePanelStore } from '@/stores/panel/store'
import { useGeneralStore } from '@/stores/settings/general/store'
import { useSidebarStore } from '@/stores/sidebar/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import {
getKeyboardShortcutText,
useKeyboardShortcuts,
} from '../../../hooks/use-keyboard-shortcuts'
import { useDeploymentChangeDetection } from '../../hooks/use-deployment-change-detection'
import { useWorkflowExecution } from '../../hooks/use-workflow-execution'
import { DeploymentControls } from './components/deployment-controls/deployment-controls'
import { HistoryDropdownItem } from './components/history-dropdown-item/history-dropdown-item'
@@ -88,10 +85,15 @@ export function ControlBar() {
showNotification,
removeNotification,
} = useNotificationStore()
const { history, revertToHistoryState, lastSaved, isDeployed, setDeploymentStatus } =
useWorkflowStore()
const { workflows, updateWorkflow, activeWorkflowId, removeWorkflow, duplicateWorkflow } =
useWorkflowRegistry()
const { history, revertToHistoryState, lastSaved } = useWorkflowStore()
const {
workflows,
updateWorkflow,
activeWorkflowId,
removeWorkflow,
duplicateWorkflow,
setDeploymentStatus,
} = useWorkflowRegistry()
const { isExecuting, handleRunWorkflow } = useWorkflowExecution()
const { setActiveTab } = usePanelStore()
@@ -112,11 +114,6 @@ export function ControlBar() {
const [historyOpen, setHistoryOpen] = useState(false)
const [notificationsOpen, setNotificationsOpen] = useState(false)
// Status states
const [isDeploying, setIsDeploying] = useState(false)
const [isPublishing, setIsPublishing] = useState(false)
const [needsRedeployment, setNeedsRedeployment] = useState(false)
// Marketplace modal state
const [isMarketplaceModalOpen, setIsMarketplaceModalOpen] = useState(false)
@@ -153,9 +150,9 @@ export function ControlBar() {
)
// Get notifications for current workflow
const workflowNotifications = activeWorkflowId
? getWorkflowNotifications(activeWorkflowId)
: notifications // Show all if no workflow is active
// const workflowNotifications = activeWorkflowId
// ? getWorkflowNotifications(activeWorkflowId)
// : notifications // Show all if no workflow is active
// Get the marketplace data from the workflow registry if available
const getMarketplaceData = () => {
@@ -169,11 +166,21 @@ export function ControlBar() {
return !!marketplaceData
}
// Check if the current user is the owner of the published workflow
const isWorkflowOwner = () => {
const marketplaceData = getMarketplaceData()
return marketplaceData?.status === 'owner'
}
// // Check if the current user is the owner of the published workflow
// const isWorkflowOwner = () => {
// const marketplaceData = getMarketplaceData()
// return marketplaceData?.status === 'owner'
// }
// Get deployment status from registry
const deploymentStatus = useWorkflowRegistry((state) =>
state.getWorkflowDeploymentStatus(activeWorkflowId)
)
const isDeployed = deploymentStatus?.isDeployed || false
// Custom hook for deployment change detection
const { needsRedeployment, setNeedsRedeployment, clearNeedsRedeployment } =
useDeploymentChangeDetection(activeWorkflowId, isDeployed)
// Client-side only rendering for the timestamp
useEffect(() => {
@@ -186,154 +193,6 @@ export function ControlBar() {
return () => clearInterval(interval)
}, [])
// Listen for workflow changes and check if redeployment is needed
useEffect(() => {
if (!activeWorkflowId || !isDeployed) return
// Create a debounced function to check for changes
let debounceTimer: NodeJS.Timeout | null = null
let lastCheckTime = 0
let pendingChanges = 0
const DEBOUNCE_DELAY = 1000
const THROTTLE_INTERVAL = 3000
// Function to check if redeployment is needed
const checkForChanges = async () => {
// Skip if we're already showing needsRedeployment
if (needsRedeployment) return
// Reset the pending changes counter
pendingChanges = 0
lastCheckTime = Date.now()
try {
// Get the deployed state from the API
const response = await fetch(`/api/workflows/${activeWorkflowId}/status`)
if (response.ok) {
const data = await response.json()
// If the API says we need redeployment, update our state and the store
if (data.needsRedeployment) {
setNeedsRedeployment(true)
// Also update the store state so other components can access this flag
useWorkflowStore.getState().setNeedsRedeploymentFlag(true)
}
}
} catch (error) {
logger.error('Failed to check workflow change status:', { error })
}
}
// Debounced check function
const debouncedCheck = () => {
// Increment the pending changes counter
pendingChanges++
// Clear any existing timer
if (debounceTimer) {
clearTimeout(debounceTimer)
}
// If we recently checked, and it's within throttle interval, wait longer
const timeElapsed = Date.now() - lastCheckTime
if (timeElapsed < THROTTLE_INTERVAL && lastCheckTime > 0) {
// Wait until the throttle interval has passed
const adjustedDelay = Math.max(THROTTLE_INTERVAL - timeElapsed, DEBOUNCE_DELAY)
debounceTimer = setTimeout(() => {
// Only check if we have pending changes
if (pendingChanges > 0) {
checkForChanges()
}
}, adjustedDelay)
} else {
// Standard debounce delay if we haven't checked recently
debounceTimer = setTimeout(() => {
// Only check if we have pending changes
if (pendingChanges > 0) {
checkForChanges()
}
}, DEBOUNCE_DELAY)
}
}
// Subscribe to workflow store changes
const workflowUnsubscribe = useWorkflowStore.subscribe(debouncedCheck)
// Also subscribe to subblock store changes
const subBlockUnsubscribe = useSubBlockStore.subscribe((state) => {
// Only check for the active workflow
if (!activeWorkflowId || !isDeployed || needsRedeployment) return
// Only trigger when there is an update to the current workflow's subblocks
const workflowSubBlocks = state.workflowValues[activeWorkflowId]
if (workflowSubBlocks && Object.keys(workflowSubBlocks).length > 0) {
debouncedCheck()
}
})
return () => {
if (debounceTimer) {
clearTimeout(debounceTimer)
}
workflowUnsubscribe()
subBlockUnsubscribe()
}
}, [activeWorkflowId, isDeployed, needsRedeployment])
// Check deployment and publication status on mount or when activeWorkflowId changes
useEffect(() => {
async function checkStatus() {
if (!activeWorkflowId) return
try {
const response = await fetch(`/api/workflows/${activeWorkflowId}/status`)
if (response.ok) {
const data = await response.json()
// Update the store with the status from the API
setDeploymentStatus(
data.isDeployed,
data.deployedAt ? new Date(data.deployedAt) : undefined
)
setNeedsRedeployment(data.needsRedeployment)
useWorkflowStore.getState().setNeedsRedeploymentFlag(data.needsRedeployment)
}
} catch (error) {
logger.error('Failed to check workflow status:', { error })
}
}
checkStatus()
}, [activeWorkflowId, setDeploymentStatus])
// Listen for deployment status changes
useEffect(() => {
// When deployment status changes and isDeployed becomes true,
// that means a deployment just occurred, so reset the needsRedeployment flag
if (isDeployed) {
setNeedsRedeployment(false)
useWorkflowStore.getState().setNeedsRedeploymentFlag(false)
}
}, [isDeployed])
// Add a listener for the needsRedeployment flag in the workflow store
useEffect(() => {
const unsubscribe = useWorkflowStore.subscribe((state) => {
// Update local state when the store flag changes
if (state.needsRedeployment !== undefined) {
setNeedsRedeployment(state.needsRedeployment)
}
})
return () => unsubscribe()
}, [])
// Add a manual method to update the deployment status and clear the needsRedeployment flag
const updateDeploymentStatusAndClearFlag = (isDeployed: boolean, deployedAt?: Date) => {
setDeploymentStatus(isDeployed, deployedAt)
setNeedsRedeployment(false)
useWorkflowStore.getState().setNeedsRedeploymentFlag(false)
}
// Update existing API notifications when needsRedeployment changes
useEffect(() => {
if (!activeWorkflowId) return
@@ -450,22 +309,22 @@ export function ControlBar() {
removeWorkflow(activeWorkflowId)
}
/**
* Handle opening marketplace modal or showing published status
*/
const handlePublishWorkflow = async () => {
if (!activeWorkflowId) return
// /**
// * Handle opening marketplace modal or showing published status
// */
// const handlePublishWorkflow = async () => {
// if (!activeWorkflowId) return
// If already published, show marketplace modal with info instead of notifications
const isPublished = isPublishedToMarketplace()
if (isPublished) {
setIsMarketplaceModalOpen(true)
return
}
// // If already published, show marketplace modal with info instead of notifications
// const isPublished = isPublishedToMarketplace()
// if (isPublished) {
// setIsMarketplaceModalOpen(true)
// return
// }
// If not published, open the modal to start the publishing process
setIsMarketplaceModalOpen(true)
}
// // If not published, open the modal to start the publishing process
// setIsMarketplaceModalOpen(true)
// }
/**
* Handle multiple workflow runs
@@ -740,7 +599,6 @@ export function ControlBar() {
* Render notifications dropdown
*/
const renderNotificationsDropdown = () => {
// Ensure we're only showing notifications for the current workflow
const currentWorkflowNotifications = activeWorkflowId
? notifications.filter((n) => n.workflowId === activeWorkflowId)
: []
@@ -845,7 +703,6 @@ export function ControlBar() {
* Render debug mode controls
*/
const renderDebugControls = () => {
// Display debug controls only when in debug mode and actively debugging
if (!isDebugModeEnabled || !isDebugging) return null
const pendingCount = pendingBlocks.length
@@ -908,9 +765,7 @@ export function ControlBar() {
*/
const renderDebugModeToggle = () => {
const handleToggleDebugMode = () => {
// If turning off debug mode, make sure to clean up any debug state
if (isDebugModeEnabled) {
// Only clean up if we're not actively executing
if (!isExecuting) {
useExecutionStore.getState().setIsDebugging(false)
useExecutionStore.getState().setPendingBlocks([])
@@ -942,7 +797,6 @@ export function ControlBar() {
// Helper function to open subscription settings
const openSubscriptionSettings = () => {
// Dispatch custom event to open settings modal with subscription tab
if (typeof window !== 'undefined') {
window.dispatchEvent(
new CustomEvent('open-settings', {

View File

@@ -307,15 +307,17 @@ export function NotificationAlert({ notification, isFading, onHide }: Notificati
const { id, type, message, options, workflowId } = notification
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
const [showApiKey, setShowApiKey] = useState(false)
const { setDeploymentStatus } = useWorkflowStore()
const { isDeployed } = useWorkflowStore((state) => ({
isDeployed: state.isDeployed,
}))
const { setDeploymentStatus } = useWorkflowRegistry()
// Get deployment status from registry using notification's workflowId, not activeWorkflowId
const deploymentStatus = useWorkflowRegistry(state =>
state.getWorkflowDeploymentStatus(workflowId || null))
const isDeployed = deploymentStatus?.isDeployed || false
// Create a function to clear the redeployment flag and update deployment status
const updateDeploymentStatus = (isDeployed: boolean, deployedAt?: Date) => {
// Update deployment status in workflow store
setDeploymentStatus(isDeployed, deployedAt)
setDeploymentStatus(workflowId || null, isDeployed, deployedAt)
// Manually update the needsRedeployment flag in workflow store
useWorkflowStore.getState().setNeedsRedeploymentFlag(false)

View File

@@ -0,0 +1,285 @@
import { useEffect, useState } from 'react'
import { createLogger } from '@/lib/logs/console-logger'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
const logger = createLogger('useDeploymentChangeDetection')
/**
* Hook to detect when a deployed workflow needs redeployment due to changes
* Handles debouncing, API checks, and state synchronization
*/
export function useDeploymentChangeDetection(activeWorkflowId: string | null, isDeployed: boolean) {
const [needsRedeployment, setNeedsRedeployment] = useState(false)
// Listen for workflow changes and check if redeployment is needed
useEffect(() => {
if (!activeWorkflowId || !isDeployed) return
// Create a debounced function to check for changes
let debounceTimer: NodeJS.Timeout | null = null
let lastCheckTime = 0
let pendingChanges = 0
const DEBOUNCE_DELAY = 1000
const THROTTLE_INTERVAL = 3000
// Store the current workflow ID when the effect runs
const effectWorkflowId = activeWorkflowId
// Function to check if redeployment is needed
const checkForChanges = async () => {
// No longer skip if we're already showing needsRedeployment
// This allows us to detect when changes have been reverted
// Reset the pending changes counter
pendingChanges = 0
lastCheckTime = Date.now()
// Store the current workflow ID to check for race conditions
const requestedWorkflowId = activeWorkflowId
logger.debug(`Checking for changes in workflow ${requestedWorkflowId}`)
try {
// Get the deployed state from the API
const response = await fetch(`/api/workflows/${requestedWorkflowId}/status`)
if (response.ok) {
const data = await response.json()
// Verify the active workflow hasn't changed while fetching
if (requestedWorkflowId !== activeWorkflowId) {
logger.debug(
`Ignoring changes response for ${requestedWorkflowId} - no longer the active workflow`
)
return
}
logger.debug(
`API needsRedeployment response for workflow ${requestedWorkflowId}: ${data.needsRedeployment}`
)
// Always update the needsRedeployment flag based on API response to handle both true and false
// This ensures it's updated when changes are detected and when changes are no longer detected
if (data.needsRedeployment) {
logger.info(
`Setting needsRedeployment flag to TRUE for workflow ${requestedWorkflowId}`
)
// Update local state
setNeedsRedeployment(true)
// Use the workflow-specific method to update the registry
useWorkflowRegistry.getState().setWorkflowNeedsRedeployment(requestedWorkflowId, true)
} else {
// Only update to false if the current state is true to avoid unnecessary updates
const currentStatus = useWorkflowRegistry
.getState()
.getWorkflowDeploymentStatus(requestedWorkflowId)
if (currentStatus?.needsRedeployment) {
logger.info(
`Setting needsRedeployment flag to FALSE for workflow ${requestedWorkflowId}`
)
// Update local state
setNeedsRedeployment(false)
// Use the workflow-specific method to update the registry
useWorkflowRegistry
.getState()
.setWorkflowNeedsRedeployment(requestedWorkflowId, false)
}
}
}
} catch (error) {
logger.error('Failed to check workflow change status:', { error })
}
}
// Debounced check function
const debouncedCheck = () => {
// Skip if the active workflow has changed
if (effectWorkflowId !== activeWorkflowId) {
return
}
// Increment the pending changes counter
pendingChanges++
// Clear any existing timer
if (debounceTimer) {
clearTimeout(debounceTimer)
}
// If we recently checked, and it's within throttle interval, wait longer
const timeElapsed = Date.now() - lastCheckTime
if (timeElapsed < THROTTLE_INTERVAL && lastCheckTime > 0) {
// Wait until the throttle interval has passed
const adjustedDelay = Math.max(THROTTLE_INTERVAL - timeElapsed, DEBOUNCE_DELAY)
debounceTimer = setTimeout(() => {
// Only check if we have pending changes and workflow ID hasn't changed
if (pendingChanges > 0 && effectWorkflowId === activeWorkflowId) {
checkForChanges()
}
}, adjustedDelay)
} else {
// Standard debounce delay if we haven't checked recently
debounceTimer = setTimeout(() => {
// Only check if we have pending changes and workflow ID hasn't changed
if (pendingChanges > 0 && effectWorkflowId === activeWorkflowId) {
checkForChanges()
}
}, DEBOUNCE_DELAY)
}
}
// Subscribe to workflow store changes
const workflowUnsubscribe = useWorkflowStore.subscribe(debouncedCheck)
// Also subscribe to subblock store changes
const subBlockUnsubscribe = useSubBlockStore.subscribe((state) => {
// Only check for the active workflow when it's deployed
if (!activeWorkflowId || !isDeployed) return
// Skip if the workflow ID has changed since this effect started
if (effectWorkflowId !== activeWorkflowId) {
return
}
// Only trigger when there is an update to the current workflow's subblocks
const workflowSubBlocks = state.workflowValues[effectWorkflowId]
if (workflowSubBlocks && Object.keys(workflowSubBlocks).length > 0) {
debouncedCheck()
}
})
// Set up a periodic check when needsRedeployment is true to ensure it gets set back to false
// when changes are reverted
let periodicCheckTimer: NodeJS.Timeout | null = null
if (needsRedeployment) {
// Check every 5 seconds when needsRedeployment is true to catch reverted changes
const PERIODIC_CHECK_INTERVAL = 5000 // 5 seconds
periodicCheckTimer = setInterval(() => {
// Only perform the check if this is still the active workflow
if (effectWorkflowId === activeWorkflowId) {
checkForChanges()
} else {
// Clear the interval if the workflow has changed
if (periodicCheckTimer) {
clearInterval(periodicCheckTimer)
}
}
}, PERIODIC_CHECK_INTERVAL)
}
return () => {
if (debounceTimer) {
clearTimeout(debounceTimer)
}
if (periodicCheckTimer) {
clearInterval(periodicCheckTimer)
}
workflowUnsubscribe()
subBlockUnsubscribe()
}
}, [activeWorkflowId, isDeployed, needsRedeployment])
// Initial check on mount or when active workflow changes
useEffect(() => {
async function checkDeploymentStatus() {
if (!activeWorkflowId) return
try {
// Store the current workflow ID to check for race conditions
const requestedWorkflowId = activeWorkflowId
const response = await fetch(`/api/workflows/${requestedWorkflowId}/status`)
if (response.ok) {
const data = await response.json()
// Verify the active workflow hasn't changed while fetching
if (requestedWorkflowId !== activeWorkflowId) {
return
}
// Update the store with the status from the API
useWorkflowRegistry
.getState()
.setDeploymentStatus(
requestedWorkflowId,
data.isDeployed,
data.deployedAt ? new Date(data.deployedAt) : undefined
)
// Update local state
setNeedsRedeployment(data.needsRedeployment)
// Use the workflow-specific method to update the registry
useWorkflowRegistry
.getState()
.setWorkflowNeedsRedeployment(requestedWorkflowId, data.needsRedeployment)
}
} catch (error) {
logger.error('Failed to check workflow status:', { error })
}
}
checkDeploymentStatus()
}, [activeWorkflowId])
// Listen for deployment status changes
useEffect(() => {
// When deployment status changes and isDeployed becomes true,
// that means a deployment just occurred, so reset the needsRedeployment flag
if (isDeployed) {
// Update local state
setNeedsRedeployment(false)
// Use the workflow-specific method to update the registry
if (activeWorkflowId) {
useWorkflowRegistry.getState().setWorkflowNeedsRedeployment(activeWorkflowId, false)
}
}
}, [isDeployed, activeWorkflowId])
// Add a listener for the needsRedeployment flag in the workflow store
useEffect(() => {
const unsubscribe = useWorkflowStore.subscribe((state) => {
// Only update local state when it's for the currently active workflow
if (state.needsRedeployment !== undefined) {
// Get the workflow-specific needsRedeployment flag for the current workflow
const currentWorkflowStatus = useWorkflowRegistry
.getState()
.getWorkflowDeploymentStatus(activeWorkflowId)
// Only set local state based on current workflow's status
if (currentWorkflowStatus?.needsRedeployment !== undefined) {
setNeedsRedeployment(currentWorkflowStatus.needsRedeployment)
} else {
// Fallback to global state only if we don't have workflow-specific status
setNeedsRedeployment(state.needsRedeployment)
}
}
})
return () => unsubscribe()
}, [activeWorkflowId])
// Function to clear the redeployment flag
const clearNeedsRedeployment = () => {
// Update local state
setNeedsRedeployment(false)
// Use the workflow-specific method to update the registry
if (activeWorkflowId) {
useWorkflowRegistry.getState().setWorkflowNeedsRedeployment(activeWorkflowId, false)
}
}
return {
needsRedeployment,
setNeedsRedeployment,
clearNeedsRedeployment,
}
}

View File

@@ -25,6 +25,9 @@ export function getWorkflowWithValues(workflowId: string) {
const metadata = workflows[workflowId]
// Get deployment status from registry
const deploymentStatus = useWorkflowRegistry.getState().getWorkflowDeploymentStatus(workflowId)
// Load the specific state for this workflow
let workflowState: WorkflowState
@@ -34,8 +37,8 @@ export function getWorkflowWithValues(workflowId: string) {
blocks: currentState.blocks,
edges: currentState.edges,
loops: currentState.loops,
isDeployed: currentState.isDeployed,
deployedAt: currentState.deployedAt,
isDeployed: deploymentStatus?.isDeployed || false,
deployedAt: deploymentStatus?.deployedAt,
lastSaved: currentState.lastSaved,
}
} else {
@@ -45,7 +48,13 @@ export function getWorkflowWithValues(workflowId: string) {
logger.warn(`No saved state found for workflow ${workflowId}`)
return null
}
workflowState = savedState
// Use registry deployment status instead of relying on saved state
workflowState = {
...savedState,
isDeployed: deploymentStatus?.isDeployed || savedState.isDeployed || false,
deployedAt: deploymentStatus?.deployedAt || savedState.deployedAt,
}
}
// Merge the subblock values for this specific workflow
@@ -103,6 +112,9 @@ export function getAllWorkflowsWithValues() {
continue
}
// Get deployment status from registry
const deploymentStatus = useWorkflowRegistry.getState().getWorkflowDeploymentStatus(id)
// Load the specific state for this workflow
let workflowState: WorkflowState
@@ -112,8 +124,8 @@ export function getAllWorkflowsWithValues() {
blocks: currentState.blocks,
edges: currentState.edges,
loops: currentState.loops,
isDeployed: currentState.isDeployed,
deployedAt: currentState.deployedAt,
isDeployed: deploymentStatus?.isDeployed || false,
deployedAt: deploymentStatus?.deployedAt,
lastSaved: currentState.lastSaved,
}
} else {
@@ -124,12 +136,21 @@ export function getAllWorkflowsWithValues() {
logger.warn(`No saved state found for workflow ${id}`)
continue
}
workflowState = savedState
// Use registry deployment status instead of relying on saved state
workflowState = {
...savedState,
isDeployed: deploymentStatus?.isDeployed || savedState.isDeployed || false,
deployedAt: deploymentStatus?.deployedAt || savedState.deployedAt,
}
}
// Merge the subblock values for this specific workflow
const mergedBlocks = mergeSubblockState(workflowState.blocks, id)
// Include the API key in the state if it exists in the deployment status
const apiKey = deploymentStatus?.apiKey
result[id] = {
id,
name: metadata.name,
@@ -145,6 +166,8 @@ export function getAllWorkflowsWithValues() {
isDeployed: workflowState.isDeployed,
deployedAt: workflowState.deployedAt,
},
// Include API key if available
apiKey,
}
}

View File

@@ -18,7 +18,7 @@ import {
workflowSync,
} from '../sync'
import { useWorkflowStore } from '../workflow/store'
import { WorkflowMetadata, WorkflowRegistry } from './types'
import { DeploymentStatus, WorkflowMetadata, WorkflowRegistry } from './types'
import { generateUniqueName, getNextWorkflowColor } from './utils'
const logger = createLogger('WorkflowRegistry')
@@ -69,7 +69,6 @@ function cleanupLocalStorageForWorkspace(workspaceId: string): void {
// Only remove if it belongs to the current workspace
if (parsed.workspaceId === workspaceId) {
localStorage.removeItem(key)
logger.debug(`Removed stale localStorage data for workflow ${workflowId}`)
}
} catch (e) {
// Skip if we can't parse the data
@@ -78,7 +77,6 @@ function cleanupLocalStorageForWorkspace(workspaceId: string): void {
} else {
// If we can't determine the workspace, remove it to be safe
localStorage.removeItem(key)
logger.debug(`Removed stale localStorage data for workflow ${workflowId}`)
}
}
// Case 2: Clean up workflows that reference deleted workspaces
@@ -99,9 +97,6 @@ function cleanupLocalStorageForWorkspace(workspaceId: string): void {
// Workspace doesn't exist, update the workflow to use current workspace
parsed.workspaceId = workspaceId
localStorage.setItem(`workflow-${workflowId}`, JSON.stringify(parsed))
logger.debug(
`Updated workflow ${workflowId} to use current workspace ${workspaceId}`
)
}
} catch (e) {
// Skip if we can't parse workspaces data
@@ -132,6 +127,7 @@ function resetWorkflowStores() {
loops: {},
isDeployed: false,
deployedAt: undefined,
deploymentStatuses: {}, // Reset deployment statuses map
hasActiveSchedule: false,
history: {
past: [],
@@ -195,6 +191,8 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
typeof window !== 'undefined' ? localStorage.getItem(ACTIVE_WORKSPACE_KEY) : null,
isLoading: false,
error: null,
// Initialize deployment statuses
deploymentStatuses: {},
// Set loading state
setLoading: (loading: boolean) => {
@@ -322,7 +320,173 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
})
},
// Switch to a different workflow and manage state persistence
// Method to get deployment status for a specific workflow
getWorkflowDeploymentStatus: (workflowId: string | null): DeploymentStatus | null => {
if (!workflowId) {
// If no workflow ID provided, check the active workflow
workflowId = get().activeWorkflowId
if (!workflowId) return null
}
const { deploymentStatuses = {} } = get()
// First try to get from the workflow-specific deployment statuses
if (deploymentStatuses[workflowId]) {
return deploymentStatuses[workflowId]
}
// For backward compatibility, check the workflow state in workflow store
// This will only be relevant during the transition period
const workflowState = loadWorkflowState(workflowId)
if (workflowState) {
// Check workflow-specific status in the workflow state
if (workflowState.deploymentStatuses?.[workflowId]) {
return workflowState.deploymentStatuses[workflowId]
}
// Fallback to legacy fields if needed
if (workflowState.isDeployed) {
return {
isDeployed: workflowState.isDeployed || false,
deployedAt: workflowState.deployedAt,
}
}
}
// No deployment status found
return null
},
// Method to set deployment status for a specific workflow
setDeploymentStatus: (
workflowId: string | null,
isDeployed: boolean,
deployedAt?: Date,
apiKey?: string
) => {
if (!workflowId) {
workflowId = get().activeWorkflowId
if (!workflowId) return
}
// Update the deployment statuses in the registry
set((state) => ({
deploymentStatuses: {
...state.deploymentStatuses,
[workflowId as string]: {
isDeployed,
deployedAt: deployedAt || (isDeployed ? new Date() : undefined),
apiKey,
// Preserve existing needsRedeployment flag if available, but reset if newly deployed
needsRedeployment: isDeployed
? false
: ((state.deploymentStatuses?.[workflowId as string] as any)?.needsRedeployment ??
false),
},
},
}))
// Also update the workflow store if this is the active workflow
const { activeWorkflowId } = get()
if (workflowId === activeWorkflowId) {
// Update the workflow store for backward compatibility
useWorkflowStore.setState((state) => ({
isDeployed,
deployedAt: deployedAt || (isDeployed ? new Date() : undefined),
needsRedeployment: isDeployed ? false : state.needsRedeployment,
deploymentStatuses: {
...state.deploymentStatuses,
[workflowId as string]: {
isDeployed,
deployedAt: deployedAt || (isDeployed ? new Date() : undefined),
apiKey,
needsRedeployment: isDeployed
? false
: ((state.deploymentStatuses?.[workflowId as string] as any)?.needsRedeployment ??
false),
},
},
}))
}
// Save the deployment status in the workflow state
const workflowState = loadWorkflowState(workflowId)
if (workflowState) {
saveWorkflowState(workflowId, {
...workflowState,
// Update both legacy and new fields for compatibility
isDeployed: workflowId === activeWorkflowId ? isDeployed : workflowState.isDeployed,
deployedAt:
workflowId === activeWorkflowId
? deployedAt || (isDeployed ? new Date() : undefined)
: workflowState.deployedAt,
deploymentStatuses: {
...(workflowState.deploymentStatuses || {}),
[workflowId]: {
isDeployed,
deployedAt: deployedAt || (isDeployed ? new Date() : undefined),
apiKey,
needsRedeployment: isDeployed
? false
: ((workflowState.deploymentStatuses?.[workflowId] as any)?.needsRedeployment ??
false),
},
},
})
}
// Trigger workflow sync to update server state
workflowSync.sync()
},
// Method to set the needsRedeployment flag for a specific workflow
setWorkflowNeedsRedeployment: (workflowId: string | null, needsRedeployment: boolean) => {
if (!workflowId) {
workflowId = get().activeWorkflowId
if (!workflowId) return
}
// Update the registry's deployment status for this specific workflow
set((state) => {
const deploymentStatuses = state.deploymentStatuses || {}
const currentStatus = deploymentStatuses[workflowId as string] || { isDeployed: false }
return {
deploymentStatuses: {
...deploymentStatuses,
[workflowId as string]: {
...currentStatus,
needsRedeployment,
},
},
}
})
// Only update the global flag if this is the active workflow
const { activeWorkflowId } = get()
if (workflowId === activeWorkflowId) {
useWorkflowStore.getState().setNeedsRedeploymentFlag(needsRedeployment)
}
// Save to persistent storage
const workflowState = loadWorkflowState(workflowId)
if (workflowState) {
const deploymentStatuses = workflowState.deploymentStatuses || {}
const currentStatus = deploymentStatuses[workflowId] || { isDeployed: false }
saveWorkflowState(workflowId, {
...workflowState,
deploymentStatuses: {
...deploymentStatuses,
[workflowId]: {
...currentStatus,
needsRedeployment,
},
},
})
}
},
// Modified setActiveWorkflow to load deployment statuses
setActiveWorkflow: async (id: string) => {
const { workflows } = get()
if (!workflows[id]) {
@@ -330,8 +494,9 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
return
}
// Save current workflow state before switching
// Get current workflow ID
const currentId = get().activeWorkflowId
// Save current workflow state before switching
if (currentId) {
const currentState = useWorkflowStore.getState()
@@ -343,6 +508,7 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
history: currentState.history,
isDeployed: currentState.isDeployed,
deployedAt: currentState.deployedAt,
deploymentStatuses: currentState.deploymentStatuses,
lastSaved: Date.now(),
})
@@ -356,7 +522,28 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
// Load workflow state for the new active workflow
const parsedState = loadWorkflowState(id)
if (parsedState) {
const { blocks, edges, history, loops, isDeployed, deployedAt } = parsedState
const {
blocks,
edges,
history,
loops,
isDeployed,
deployedAt,
deploymentStatuses,
needsRedeployment,
} = parsedState
// Get workflow-specific deployment status
let workflowIsDeployed = isDeployed
let workflowDeployedAt = deployedAt
let workflowNeedsRedeployment = needsRedeployment
// Check if we have a workflow-specific deployment status
if (deploymentStatuses && deploymentStatuses[id]) {
workflowIsDeployed = deploymentStatuses[id].isDeployed
workflowDeployedAt = deploymentStatuses[id].deployedAt
workflowNeedsRedeployment = deploymentStatuses[id].needsRedeployment
}
// Initialize subblock store with workflow values
useSubBlockStore.getState().initializeFromWorkflow(id, blocks)
@@ -366,8 +553,11 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
blocks,
edges,
loops,
isDeployed: isDeployed !== undefined ? isDeployed : false,
deployedAt: deployedAt ? new Date(deployedAt) : undefined,
isDeployed: workflowIsDeployed !== undefined ? workflowIsDeployed : false,
deployedAt: workflowDeployedAt ? new Date(workflowDeployedAt) : undefined,
needsRedeployment:
workflowNeedsRedeployment !== undefined ? workflowNeedsRedeployment : false,
deploymentStatuses: deploymentStatuses || {},
hasActiveSchedule: false,
history: history || {
past: [],
@@ -376,8 +566,8 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
blocks,
edges,
loops: {},
isDeployed: isDeployed !== undefined ? isDeployed : false,
deployedAt: deployedAt,
isDeployed: workflowIsDeployed !== undefined ? workflowIsDeployed : false,
deployedAt: workflowDeployedAt,
},
timestamp: Date.now(),
action: 'Initial state',
@@ -387,6 +577,30 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
},
lastSaved: parsedState.lastSaved || Date.now(),
})
// Update the deployment statuses in the registry
if (deploymentStatuses) {
set((state) => ({
deploymentStatuses: {
...state.deploymentStatuses,
...deploymentStatuses,
},
}))
} else if (workflowIsDeployed !== undefined) {
// If there's no deployment statuses object but we have legacy deployment status,
// create an entry in the deploymentStatuses map
set((state) => ({
deploymentStatuses: {
...state.deploymentStatuses,
[id]: {
isDeployed: workflowIsDeployed as boolean,
deployedAt: workflowDeployedAt ? new Date(workflowDeployedAt) : undefined,
},
},
}))
}
logger.info(`Switched to workflow ${id}`)
} else {
// If no saved state, initialize with empty state
useWorkflowStore.setState({
@@ -395,6 +609,7 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
loops: {},
isDeployed: false,
deployedAt: undefined,
deploymentStatuses: {},
hasActiveSchedule: false,
history: {
past: [],
@@ -459,6 +674,7 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
loops: options.marketplaceState.loops || {},
isDeployed: false,
deployedAt: undefined,
deploymentStatuses: {}, // Initialize empty deployment statuses map
workspaceId, // Include workspace ID in the state object
history: {
past: [],
@@ -582,6 +798,7 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
loops: {},
isDeployed: false,
deployedAt: undefined,
deploymentStatuses: {}, // Initialize empty deployment statuses map
workspaceId, // Include workspace ID in the state object
history: {
past: [],
@@ -776,6 +993,7 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
isDeployed: false, // Reset deployment status
deployedAt: undefined, // Reset deployment timestamp
workspaceId, // Include workspaceId in state
deploymentStatuses: {}, // Start with empty deployment statuses map
history: {
past: [],
present: {

View File

@@ -3,6 +3,13 @@ export interface MarketplaceData {
status: 'owner' | 'temp'
}
export interface DeploymentStatus {
isDeployed: boolean
deployedAt?: Date
apiKey?: string
needsRedeployment?: boolean
}
export interface WorkflowMetadata {
id: string
name: string
@@ -19,6 +26,7 @@ export interface WorkflowRegistryState {
activeWorkspaceId: string | null
isLoading: boolean
error: string | null
deploymentStatuses: Record<string, DeploymentStatus>
}
export interface WorkflowRegistryActions {
@@ -37,6 +45,14 @@ export interface WorkflowRegistryActions {
workspaceId?: string
}) => string
duplicateWorkflow: (sourceId: string) => string | null
getWorkflowDeploymentStatus: (workflowId: string | null) => DeploymentStatus | null
setDeploymentStatus: (
workflowId: string | null,
isDeployed: boolean,
deployedAt?: Date,
apiKey?: string
) => void
setWorkflowNeedsRedeployment: (workflowId: string | null, needsRedeployment: boolean) => void
}
export type WorkflowRegistry = WorkflowRegistryState & WorkflowRegistryActions

View File

@@ -3,6 +3,7 @@ import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { getBlock } from '@/blocks'
import { resolveOutputType } from '@/blocks/utils'
import { createLogger } from '@/lib/logs/console-logger'
import { pushHistory, withHistory, WorkflowStoreWithHistory } from '../middleware'
import { saveWorkflowState } from '../persistence'
import { useWorkflowRegistry } from '../registry/store'
@@ -12,13 +13,18 @@ import { mergeSubblockState } from '../utils'
import { Loop, Position, SubBlockState, SyncControl, WorkflowState } from './types'
import { detectCycle } from './utils'
const logger = createLogger('WorkflowStore')
const initialState = {
blocks: {},
edges: [],
loops: {},
lastSaved: undefined,
// Legacy deployment fields (keeping for compatibility but they will be deprecated)
isDeployed: false,
deployedAt: undefined,
// New field for per-workflow deployment tracking
deploymentStatuses: {},
needsRedeployment: false,
hasActiveSchedule: false,
hasActiveWebhook: false,
@@ -382,8 +388,10 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
edges: currentState.edges,
loops: currentState.loops,
history: currentState.history,
// Include both legacy and new deployment status fields
isDeployed: currentState.isDeployed,
deployedAt: currentState.deployedAt,
deploymentStatuses: currentState.deploymentStatuses,
lastSaved: Date.now(),
})
@@ -683,20 +691,6 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
}))
},
setDeploymentStatus: (isDeployed: boolean, deployedAt?: Date) => {
const newState = {
...get(),
isDeployed,
deployedAt: deployedAt || (isDeployed ? new Date() : undefined),
needsRedeployment: isDeployed ? false : get().needsRedeployment,
}
set(newState)
get().updateLastSaved()
get().sync.markDirty()
get().sync.forceSync()
},
setScheduleStatus: (hasActiveSchedule: boolean) => {
// Only update if the status has changed to avoid unnecessary rerenders
if (get().hasActiveSchedule !== hasActiveSchedule) {
@@ -721,20 +715,34 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
},
revertToDeployedState: (deployedState: WorkflowState) => {
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
// Preserving the workflow-specific deployment status if it exists
const deploymentStatus = activeWorkflowId
? useWorkflowRegistry.getState().getWorkflowDeploymentStatus(activeWorkflowId)
: null;
const newState = {
blocks: deployedState.blocks,
edges: deployedState.edges,
loops: deployedState.loops,
// Legacy fields for backward compatibility
isDeployed: true,
needsRedeployment: false,
hasActiveWebhook: false, // Reset webhook status
// Keep existing deployment statuses and update for the active workflow if needed
deploymentStatuses: {
...get().deploymentStatuses,
...(activeWorkflowId && deploymentStatus ? {
[activeWorkflowId]: deploymentStatus
} : {})
}
}
// Update the main workflow state
set(newState)
// Get the active workflow ID
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
if (!activeWorkflowId) return
// Initialize subblock store with values from deployed state

View File

@@ -34,14 +34,24 @@ export interface Loop {
forEachItems?: any[] | Record<string, any> | string // Items or expression
}
export interface DeploymentStatus {
isDeployed: boolean
deployedAt?: Date
apiKey?: string
needsRedeployment?: boolean
}
export interface WorkflowState {
blocks: Record<string, BlockState>
edges: Edge[]
lastSaved?: number
loops: Record<string, Loop>
lastUpdate?: number
// Legacy deployment fields (keeping for compatibility)
isDeployed?: boolean
deployedAt?: Date
// New field for per-workflow deployment status
deploymentStatuses?: Record<string, DeploymentStatus>
needsRedeployment?: boolean
hasActiveSchedule?: boolean
hasActiveWebhook?: boolean
@@ -76,7 +86,6 @@ export interface WorkflowActions {
updateLoopType: (loopId: string, loopType: Loop['loopType']) => void
updateLoopForEachItems: (loopId: string, items: string) => void
setNeedsRedeploymentFlag: (needsRedeployment: boolean) => void
setDeploymentStatus: (isDeployed: boolean, deployedAt?: Date) => void
setScheduleStatus: (hasActiveSchedule: boolean) => void
setWebhookStatus: (hasActiveWebhook: boolean) => void
toggleBlockAdvancedMode: (id: string) => void