diff --git a/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/deployment-info.tsx b/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/deployment-info.tsx index a85f81d7a6..518b96ea6c 100644 --- a/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/deployment-info.tsx +++ b/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/deployment-info.tsx @@ -21,14 +21,12 @@ import { DeployStatus } from '@/app/w/[id]/components/control-bar/components/dep import { ExampleCommand } from '@/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/components/example-command/example-command' import { useNotificationStore } from '@/stores/notifications/store' import { DeployedWorkflowModal } from '../../../deployment-controls/components/deployed-workflow-modal' -import { createLogger } from '@/lib/logs/console-logger' +import { WorkflowState } from '@/stores/workflows/workflow/types' -const logger = createLogger('DeploymentInfo') interface DeploymentInfoProps { isLoading?: boolean deploymentInfo: { - isDeployed: boolean deployedAt?: string apiKey: string endpoint: string @@ -40,7 +38,7 @@ interface DeploymentInfoProps { isSubmitting: boolean isUndeploying: boolean workflowId: string | null - deployedState: any + deployedState: WorkflowState isLoadingDeployedState: boolean } @@ -69,7 +67,6 @@ export function DeploymentInfo({ setIsViewingDeployed(true) return } else if (!isLoadingDeployedState) { - logger.debug(`No deployed state found`) addNotification('error', 'Cannot view deployment: No deployed state available', workflowId) } } diff --git a/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/deploy-modal.tsx b/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/deploy-modal.tsx index abbdf54b2b..dbb12d08c9 100644 --- a/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/deploy-modal.tsx +++ b/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/deploy-modal.tsx @@ -27,6 +27,7 @@ import { useNotificationStore } from '@/stores/notifications/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' +import { WorkflowState } from '@/stores/workflows/workflow/types' const logger = createLogger('DeployModal') @@ -36,7 +37,7 @@ interface DeployModalProps { workflowId: string | null needsRedeployment: boolean setNeedsRedeployment: (value: boolean) => void - deployedState: any + deployedState: WorkflowState isLoadingDeployedState: boolean refetchDeployedState: () => Promise } @@ -313,7 +314,6 @@ export function DeployModal({ setDeploymentInfo(newDeploymentInfo) // Fetch the updated deployed state after deployment - logger.info('Deployment successful, fetching initial deployed state') await refetchDeployedState() // No notification on successful deploy diff --git a/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-card.tsx b/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-card.tsx index 025e60e069..7f63fea38e 100644 --- a/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-card.tsx +++ b/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-card.tsx @@ -1,7 +1,6 @@ 'use client' -import { useState, useEffect, useMemo } from 'react' -import { Button } from '@/components/ui/button' +import { useState, useMemo } from 'react' import { Card, CardContent, CardHeader } from '@/components/ui/card' import { Switch } from '@/components/ui/switch' import { Label } from '@/components/ui/label' @@ -9,32 +8,13 @@ import { cn } from '@/lib/utils' import { createLogger } from '@/lib/logs/console-logger' import { WorkflowPreview } from '@/app/w/components/workflow-preview/workflow-preview' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +import { WorkflowState } from '@/stores/workflows/workflow/types' const logger = createLogger('DeployedWorkflowCard') interface DeployedWorkflowCardProps { - currentWorkflowState?: { - blocks: Record - edges: Array - loops: Record - _metadata?: { - workflowId?: string - fetchTimestamp?: number - requestId?: number - [key: string]: any - } - } - deployedWorkflowState: { - blocks: Record - edges: Array - loops: Record - _metadata?: { - workflowId?: string - fetchTimestamp?: number - requestId?: number - [key: string]: any - } - } + currentWorkflowState?: WorkflowState + deployedWorkflowState: WorkflowState className?: string } @@ -48,44 +28,44 @@ export function DeployedWorkflowCard({ const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId) // Create sanitized workflow state - const sanitizedWorkflowState = useMemo(() => { - if (!workflowToShow) return null; + // const sanitizedWorkflowState = useMemo(() => { + // if (!workflowToShow) return null; - // Verify the workflow ID matches if metadata exists - if (workflowToShow._metadata?.workflowId && - workflowToShow._metadata.workflowId !== activeWorkflowId) { - logger.warn('Workflow ID mismatch detected in card', { - stateWorkflowId: workflowToShow._metadata.workflowId, - activeWorkflowId, - isDeployed: showingDeployed - }); - } + // // Verify the workflow ID matches if metadata exists + // if (workflowToShow._metadata?.workflowId && + // workflowToShow._metadata.workflowId !== activeWorkflowId) { + // logger.warn('Workflow ID mismatch detected in card', { + // stateWorkflowId: workflowToShow._metadata.workflowId, + // activeWorkflowId, + // isDeployed: showingDeployed + // }); + // } - // Filter out invalid blocks and make deep clone to avoid reference issues - const result = { - blocks: Object.fromEntries( - Object.entries(workflowToShow.blocks || {}) - .filter(([_, block]) => block && block.type) // Filter out invalid blocks - .map(([id, block]) => { - // Deep clone the block to avoid any reference sharing - const clonedBlock = structuredClone(block); - return [id, clonedBlock]; - }) - ), - edges: workflowToShow.edges ? structuredClone(workflowToShow.edges) : [], - loops: workflowToShow.loops ? structuredClone(workflowToShow.loops) : {}, - _metadata: { - ...(workflowToShow._metadata || {}), - workflowId: activeWorkflowId, - viewType: showingDeployed ? 'deployed' : 'current', - sanitizedAt: Date.now() - } - }; + // // Filter out invalid blocks and make deep clone to avoid reference issues + // const result = { + // blocks: Object.fromEntries( + // Object.entries(workflowToShow.blocks || {}) + // .filter(([_, block]) => block && block.type) // Filter out invalid blocks + // .map(([id, block]) => { + // // Deep clone the block to avoid any reference sharing + // const clonedBlock = structuredClone(block); + // return [id, clonedBlock]; + // }) + // ), + // edges: workflowToShow.edges ? structuredClone(workflowToShow.edges) : [], + // loops: workflowToShow.loops ? structuredClone(workflowToShow.loops) : {}, + // _metadata: { + // ...(workflowToShow._metadata || {}), + // workflowId: activeWorkflowId, + // viewType: showingDeployed ? 'deployed' : 'current', + // sanitizedAt: Date.now() + // } + // }; - return result; - }, [workflowToShow, showingDeployed, activeWorkflowId]); + // return result; + // }, [workflowToShow, showingDeployed, activeWorkflowId]); - // Generate a unique key for the workflow preview + // // Generate a unique key for the workflow preview const previewKey = useMemo(() => { return `${showingDeployed ? 'deployed' : 'current'}-preview-${activeWorkflowId}}`; }, [showingDeployed, activeWorkflowId]); @@ -94,8 +74,7 @@ export function DeployedWorkflowCard({ - + {/* Workflow preview with fixed height */}
- {sanitizedWorkflowState ? ( + {/* {sanitizedWorkflowState ? ( */} - ) : ( -
- No workflow data available -
- )}
diff --git a/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-modal.tsx b/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-modal.tsx index 24759228f9..1ec70c9a39 100644 --- a/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-modal.tsx +++ b/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-modal.tsx @@ -25,6 +25,7 @@ import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { mergeSubblockState } from '@/stores/workflows/utils' import { useWorkflowStore } from '@/stores/workflows/workflow/store' import { DeployedWorkflowCard } from './deployed-workflow-card' +import { WorkflowState } from '@/stores/workflows/workflow/types' const logger = createLogger('DeployedWorkflowModal') @@ -32,18 +33,7 @@ interface DeployedWorkflowModalProps { isOpen: boolean onClose: () => void needsRedeployment: boolean - deployedWorkflowState: { - blocks: Record - edges: Array - loops: Record - parallels: Record - _metadata?: { - workflowId?: string - fetchTimestamp?: number - requestId?: number - [key: string]: any - } - } + deployedWorkflowState: WorkflowState } export function DeployedWorkflowModal({ @@ -74,94 +64,95 @@ export function DeployedWorkflowModal({ })) // Sanitize states to ensure no invalid blocks are passed to components - const sanitizedCurrentState = useMemo(() => { - if (!currentWorkflowState) return undefined; + // const sanitizedCurrentState = useMemo(() => { + // if (!currentWorkflowState) return undefined; - const result = { - blocks: Object.fromEntries( - Object.entries(currentWorkflowState.blocks || {}) - .filter(([_, block]) => block && block.type) - .map(([id, block]) => { - // Deep clone the block to avoid any reference sharing - return [id, structuredClone(block)]; - }) - ), - edges: currentWorkflowState.edges ? [...currentWorkflowState.edges] : [], - loops: currentWorkflowState.loops ? {...currentWorkflowState.loops} : {}, - _metadata: { - workflowId: activeWorkflowId || undefined, - type: 'current', - timestamp: Date.now() - } - }; + // const result = { + // blocks: Object.fromEntries( + // Object.entries(currentWorkflowState.blocks || {}) + // .filter(([_, block]) => block && block.type) + // .map(([id, block]) => { + // // Deep clone the block to avoid any reference sharing + // return [id, structuredClone(block)]; + // }) + // ), + // edges: currentWorkflowState.edges ? [...currentWorkflowState.edges] : [], + // loops: currentWorkflowState.loops ? {...currentWorkflowState.loops} : {}, + // _metadata: { + // workflowId: activeWorkflowId || undefined, + // type: 'current', + // timestamp: Date.now() + // } + // }; - return result; - }, [currentWorkflowState, activeWorkflowId]); + // return result; + // }, [currentWorkflowState, activeWorkflowId]); - const sanitizedDeployedState = useMemo(() => { - if (!deployedWorkflowState) return { - blocks: {}, - edges: [], - loops: {}, - _metadata: { - workflowId: activeWorkflowId || undefined, - type: 'deployed-empty', - timestamp: Date.now() - } - }; + // const sanitizedDeployedState = useMemo(() => { + // if (!deployedWorkflowState) return { + // blocks: {}, + // edges: [], + // loops: {}, + // _metadata: { + // workflowId: activeWorkflowId || undefined, + // type: 'deployed-empty', + // timestamp: Date.now() + // } + // }; - const stateWorkflowId = deployedWorkflowState?._metadata?.workflowId; - const stateMatch = stateWorkflowId === activeWorkflowId; + // const stateWorkflowId = deployedWorkflowState?._metadata?.workflowId; + // const stateMatch = stateWorkflowId === activeWorkflowId; - // Check if the deployed state belongs to the current workflow - // This is a critical safety check to prevent showing the wrong workflow state - if (stateWorkflowId && !stateMatch) { - logger.error('Attempted to use deployed state from wrong workflow', { - stateWorkflowId, - activeWorkflowId, - }); + // // Check if the deployed state belongs to the current workflow + // // This is a critical safety check to prevent showing the wrong workflow state + // if (stateWorkflowId && !stateMatch) { + // logger.error('Attempted to use deployed state from wrong workflow', { + // stateWorkflowId, + // activeWorkflowId, + // }); - // Return empty state to prevent showing wrong workflow data - return { - blocks: {}, - edges: [], - loops: {}, - _metadata: { - workflowId: activeWorkflowId || undefined, - type: 'deployed-empty-mismatch', - originalWorkflowId: stateWorkflowId, - timestamp: Date.now() - } - }; - } + // // Return empty state to prevent showing wrong workflow data + // return { + // blocks: {}, + // edges: [], + // loops: {}, + // _metadata: { + // workflowId: activeWorkflowId || undefined, + // type: 'deployed-empty-mismatch', + // originalWorkflowId: stateWorkflowId, + // timestamp: Date.now() + // } + // }; + // } - const result = { - blocks: Object.fromEntries( - Object.entries(deployedWorkflowState.blocks || {}) - .filter(([_, block]) => block && block.type) - .map(([id, block]) => { - // Deep clone the block to avoid any reference sharing - return [id, structuredClone(block)]; - }) - ), - edges: deployedWorkflowState.edges ? [...deployedWorkflowState.edges] : [], - loops: deployedWorkflowState.loops ? {...deployedWorkflowState.loops} : {}, - _metadata: { - ...(deployedWorkflowState._metadata || {}), - workflowId: deployedWorkflowState._metadata?.workflowId || activeWorkflowId || undefined, - type: 'deployed-sanitized', - sanitizedAt: Date.now() - } - }; + // const result = { + // blocks: Object.fromEntries( + // Object.entries(deployedWorkflowState.blocks || {}) + // .filter(([_, block]) => block && block.type) + // .map(([id, block]) => { + // // Deep clone the block to avoid any reference sharing + // return [id, structuredClone(block)]; + // }) + // ), + // edges: deployedWorkflowState.edges ? [...deployedWorkflowState.edges] : [], + // loops: deployedWorkflowState.loops ? {...deployedWorkflowState.loops} : {}, + // _metadata: { + // ...(deployedWorkflowState._metadata || {}), + // workflowId: deployedWorkflowState._metadata?.workflowId || activeWorkflowId || undefined, + // type: 'deployed-sanitized', + // sanitizedAt: Date.now() + // } + // }; - return result; - }, [deployedWorkflowState, activeWorkflowId]); + // return result; + // }, [deployedWorkflowState, activeWorkflowId]); const handleRevert = () => { - // Revert to the deployed state - revertToDeployedState(deployedWorkflowState) - setShowRevertDialog(false) - onClose() + if (activeWorkflowId) { + revertToDeployedState(deployedWorkflowState) + setShowRevertDialog(false) + onClose() + } } return ( @@ -177,8 +168,8 @@ export function DeployedWorkflowModal({
diff --git a/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/deployment-controls.tsx b/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/deployment-controls.tsx index 6f7babd045..a5a4f17f8c 100644 --- a/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/deployment-controls.tsx +++ b/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/deployment-controls.tsx @@ -125,7 +125,7 @@ export function DeploymentControls({ workflowId={activeWorkflowId} needsRedeployment={workflowNeedsRedeployment} setNeedsRedeployment={setNeedsRedeployment} - deployedState={deployedState} + deployedState={deployedState as WorkflowState} isLoadingDeployedState={isLoadingDeployedState} refetchDeployedState={refetchWithErrorHandling} /> diff --git a/apps/sim/app/w/[id]/components/control-bar/control-bar.tsx b/apps/sim/app/w/[id]/components/control-bar/control-bar.tsx index ff5b9b0dbb..0ad7d3d828 100644 --- a/apps/sim/app/w/[id]/components/control-bar/control-bar.tsx +++ b/apps/sim/app/w/[id]/components/control-bar/control-bar.tsx @@ -46,6 +46,7 @@ import { usePanelStore } from '@/stores/panel/store' import { useGeneralStore } from '@/stores/settings/general/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' +import { WorkflowState } from '@/stores/workflows/workflow/types' import { getKeyboardShortcutText, useKeyboardShortcuts, @@ -56,7 +57,6 @@ import { DeploymentControls } from './components/deployment-controls/deployment- import { HistoryDropdownItem } from './components/history-dropdown-item/history-dropdown-item' import { MarketplaceModal } from './components/marketplace-modal/marketplace-modal' import { NotificationDropdownItem } from './components/notification-dropdown-item/notification-dropdown-item' -import { useSubBlockStore } from '@/stores/workflows/subblock/store' const logger = createLogger('ControlBar') @@ -108,14 +108,9 @@ export function ControlBar() { const [mounted, setMounted] = useState(false) const [, forceUpdate] = useState({}) - // Add deployedState management - const [deployedState, setDeployedState] = useState(null) + // Deployed state management + const [deployedState, setDeployedState] = useState(null) const [isLoadingDeployedState, setIsLoadingDeployedState] = useState(false) - - // Add refs to manage fetch state and prevent race conditions - const abortControllerRef = useRef(null) - const lastFetchedWorkflowIdRef = useRef(null) - const lastDeployedStateRef = useRef(false) // Workflow name editing state const [isEditing, setIsEditing] = useState(false) @@ -160,11 +155,6 @@ export function ControlBar() { isExecuting || isMultiRunning || isCancelling ) - // Get notifications for current workflow - // 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 = () => { if (!activeWorkflowId || !workflows[activeWorkflowId]) return null @@ -177,12 +167,6 @@ 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' - // } - // Get deployment status from registry const deploymentStatus = useWorkflowRegistry((state) => state.getWorkflowDeploymentStatus(activeWorkflowId) @@ -204,217 +188,109 @@ 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 - - // 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) - } else { - // Add this else branch to handle the case when changes are reverted - setNeedsRedeployment(false) - useWorkflowStore.getState().setNeedsRedeploymentFlag(false) - } - } - } 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]) - /** * Fetches the deployed state of the workflow from the server * This is the single source of truth for deployed workflow state - * @param options.forceRefetch Force a refetch even if conditions wouldn't normally trigger it - * @returns Promise that resolves when the deployed state is fetched */ const fetchDeployedState = async (options = { forceRefetch: false }) => { - // Cancel any in-flight requests - if (abortControllerRef.current) { - abortControllerRef.current.abort(); + if (!activeWorkflowId) { + setDeployedState(null) + return } - - // Create new abort controller for this request - abortControllerRef.current = new AbortController(); - const { signal } = abortControllerRef.current; - - const requestId = Date.now(); - const currentWorkflowId = activeWorkflowId; - - // Skip fetching if we don't have an active workflow ID or it's not deployed - // unless we're explicitly forcing a refetch - if ((!currentWorkflowId || !isDeployed) && !options.forceRefetch) { - setDeployedState(null); - return; + + // Skip fetching if not deployed unless forcing a refetch + if (!isDeployed && !options.forceRefetch) { + setDeployedState(null) + return } + // Store the workflow ID at the start of the request to prevent race conditions + const requestWorkflowId = activeWorkflowId + try { - setIsLoadingDeployedState(true); + setIsLoadingDeployedState(true) - // Pass the abort signal to the fetch call - const response = await fetch( - `/api/workflows/${currentWorkflowId}/deployed`, - { signal } - ); + const response = await fetch(`/api/workflows/${requestWorkflowId}/deployed`) + + // Check if the workflow ID changed during the request (user navigated away) + if (requestWorkflowId !== useWorkflowRegistry.getState().activeWorkflowId) { + logger.debug('Workflow changed during deployed state fetch, ignoring response') + return + } if (!response.ok) { - throw new Error(`Failed to fetch deployed state: ${response.status}`); + if (response.status === 404) { + // No deployed state found + setDeployedState(null) + return + } + throw new Error(`Failed to fetch deployed state: ${response.statusText}`) } - const data = await response.json(); + const data = await response.json() - // Final workflow ID check before updating state - if (currentWorkflowId !== activeWorkflowId) { - return; - } - - if (data.deployedState) { - // Create a single deep clone with metadata - const deployedStateWithMetadata = { - ...JSON.parse(JSON.stringify(data.deployedState)), - _metadata: { - workflowId: currentWorkflowId, - fetchTimestamp: Date.now(), - requestId - } - }; - - setDeployedState(deployedStateWithMetadata); + // Final check to ensure we're still on the same workflow + if (requestWorkflowId === useWorkflowRegistry.getState().activeWorkflowId) { + setDeployedState(data.deployedState || null) } else { - setDeployedState(null); + logger.debug('Workflow changed after deployed state response, ignoring result') } - } catch (error: unknown) { - // Don't log AbortError as it's expected when cancelling requests - if (error instanceof Error && error.name === 'AbortError') { - // Silently ignore abort errors - } else { - logger.error(`Error fetching deployed state:`, { error }); - setDeployedState(null); + + } catch (error) { + logger.error('Error fetching deployed state:', { error }) + // Only set error state if we're still on the same workflow + if (requestWorkflowId === useWorkflowRegistry.getState().activeWorkflowId) { + setDeployedState(null) } } finally { - setIsLoadingDeployedState(false); + // Only clear loading state if we're still on the same workflow + if (requestWorkflowId === useWorkflowRegistry.getState().activeWorkflowId) { + setIsLoadingDeployedState(false) + } } - }; - - // Alias for clarity when explicitly triggering a refetch - const refetchDeployedState = () => fetchDeployedState({ forceRefetch: true }); + } // Fetch deployed state when the workflow ID changes or deployment status changes useEffect(() => { - // Only fetch if the workflow ID or deployed status has actually changed - if (activeWorkflowId !== lastFetchedWorkflowIdRef.current || - isDeployed !== lastDeployedStateRef.current) { - - // Update refs to track what we're fetching for - lastFetchedWorkflowIdRef.current = activeWorkflowId; - lastDeployedStateRef.current = isDeployed; - - fetchDeployedState(); + // Immediately clear deployed state when workflow changes to prevent mixup + if (activeWorkflowId) { + setDeployedState(null) + setIsLoadingDeployedState(false) } - }, [activeWorkflowId, isDeployed]); + + // Then fetch the new deployed state + fetchDeployedState() + }, [activeWorkflowId, isDeployed]) // 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 + // Clear deployed state immediately when workflow changes + if (!activeWorkflowId) { + setDeployedState(null) + setIsLoadingDeployedState(false) + return + } + if (isDeployed) { + // When deployment status becomes true, reset the needsRedeployment flag setNeedsRedeployment(false) useWorkflowStore.getState().setNeedsRedeploymentFlag(false) + // Fetch the latest deployed state + fetchDeployedState() } else { // If workflow is undeployed, clear the deployed state setDeployedState(null) + setIsLoadingDeployedState(false) } }, [isDeployed, activeWorkflowId]) // 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() }, []) @@ -427,14 +303,11 @@ export function ControlBar() { ) if (apiNotification && apiNotification.options?.needsRedeployment !== needsRedeployment) { - // If there's an existing API notification and its state doesn't match, update it if (apiNotification.isVisible) { - // Only update if it's currently showing to the user removeNotification(apiNotification.id) - // The DeploymentControls component will handle showing the appropriate notification } } - }, [needsRedeployment, activeWorkflowId, notifications, removeNotification, addNotification]) + }, [needsRedeployment, activeWorkflowId, notifications, removeNotification]) // Check usage limits when component mounts and when user executes a workflow useEffect(() => { @@ -536,23 +409,6 @@ export function ControlBar() { removeWorkflow(activeWorkflowId) } - // /** - // * 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 not published, open the modal to start the publishing process - // setIsMarketplaceModalOpen(true) - // } - /** * Handle multiple workflow runs */ @@ -760,7 +616,7 @@ export function ControlBar() { setNeedsRedeployment={setNeedsRedeployment} deployedState={deployedState} isLoadingDeployedState={isLoadingDeployedState} - refetchDeployedState={refetchDeployedState} + refetchDeployedState={fetchDeployedState} /> ) diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/checkbox-list.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/checkbox-list.tsx index 725a4d86ab..c393c3ea1c 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/checkbox-list.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/checkbox-list.tsx @@ -11,7 +11,7 @@ interface CheckboxListProps { options: { label: string; id: string }[] layout?: 'full' | 'half' isPreview?: boolean - value?: Record + subBlockValues?: Record } export function CheckboxList({ @@ -21,25 +21,36 @@ export function CheckboxList({ options, layout, isPreview = false, - value: propValues + subBlockValues }: CheckboxListProps) { return (
{options.map((option) => { - const [value, setValue] = useSubBlockValue( + const [storeValue, setStoreValue] = useSubBlockValue( blockId, - option.id, - false, - isPreview, - propValues?.[option.id] + option.id ) + // Get preview value for this specific option + const previewValue = isPreview && subBlockValues ? subBlockValues[option.id]?.value : undefined + + // Use preview value when in preview mode, otherwise use store value + const value = isPreview ? previewValue : storeValue + + const handleChange = (checked: boolean) => { + // Only update store when not in preview mode + if (!isPreview) { + setStoreValue(checked) + } + } + return (
setValue(checked as boolean)} + onCheckedChange={handleChange} + disabled={isPreview} />
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx index 41bc89cd27..386a5cb62b 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx @@ -24,17 +24,17 @@ import { TeamsMessageSelector } from './components/teams-message-selector' interface FileSelectorInputProps { blockId: string subBlock: SubBlockConfig - disabled?: boolean + disabled: boolean isPreview?: boolean - value?: string + previewValue?: any | null } export function FileSelectorInput({ blockId, subBlock, - disabled = false, + disabled, isPreview = false, - value: propValue + previewValue }: FileSelectorInputProps) { const { getValue, setValue } = useSubBlockStore() const { activeWorkflowId } = useWorkflowRegistry() @@ -60,10 +60,13 @@ export function FileSelectorInput({ const botToken = isDiscord ? (getValue(blockId, 'botToken') as string) || '' : '' const serverId = isDiscord ? (getValue(blockId, 'serverId') as string) || '' : '' + // Use preview value when in preview mode, otherwise use store value + const value = isPreview ? previewValue : getValue(blockId, subBlock.id) + // Get the current value from the store or prop value if in preview mode useEffect(() => { - if (isPreview && propValue !== undefined) { - const value = propValue; + if (isPreview && previewValue !== undefined) { + const value = previewValue; if (value && typeof value === 'string') { if (isJira) { setSelectedIssueId(value); @@ -87,7 +90,7 @@ export function FileSelectorInput({ } } } - }, [blockId, subBlock.id, getValue, isJira, isDiscord, isMicrosoftTeams, isPreview, propValue]); + }, [blockId, subBlock.id, getValue, isJira, isDiscord, isPreview, previewValue]); // Handle file selection const handleFileChange = (fileId: string, info?: any) => { diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-upload.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-upload.tsx index 0d09c800b3..860f2ac79d 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-upload.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-upload.tsx @@ -18,7 +18,7 @@ interface FileUploadProps { acceptedTypes?: string // comma separated MIME types multiple?: boolean // whether to allow multiple file uploads isPreview?: boolean - value?: UploadedFile | UploadedFile[] | null + previewValue?: any | null } interface UploadedFile { @@ -41,16 +41,10 @@ export function FileUpload({ acceptedTypes = '*', multiple = false, // Default to single file for backward compatibility isPreview = false, - value: propValue + previewValue }: FileUploadProps) { // State management - handle both single file and array of files - const [value, setValue] = useSubBlockValue( - blockId, - subBlockId, - true, - isPreview, - propValue - ) + const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) const [uploadingFiles, setUploadingFiles] = useState([]) const [uploadProgress, setUploadProgress] = useState(0) @@ -64,6 +58,9 @@ export function FileUpload({ const { addNotification } = useNotificationStore() const { activeWorkflowId } = useWorkflowRegistry() + // Use preview value when in preview mode, otherwise use store value + const value = isPreview ? previewValue : storeValue + /** * Opens file dialog * Prevents event propagation to avoid ReactFlow capturing the event @@ -91,6 +88,8 @@ export function FileUpload({ * Handles file upload when new file(s) are selected */ const handleFileChange = async (e: React.ChangeEvent) => { + if (isPreview) return + e.stopPropagation() const files = e.target.files @@ -287,14 +286,14 @@ export function FileUpload({ // Convert map values back to array const newFiles = Array.from(uniqueFiles.values()) - setValue(newFiles) + setStoreValue(newFiles) // Make sure to update the subblock store value for the workflow execution useSubBlockStore.getState().setValue(blockId, subBlockId, newFiles) useWorkflowStore.getState().triggerUpdate() } else { // For single file: Replace with last uploaded file - setValue(uploadedFiles[0] || null) + setStoreValue(uploadedFiles[0] || null) // Make sure to update the subblock store value for the workflow execution useSubBlockStore.getState().setValue(blockId, subBlockId, uploadedFiles[0] || null) @@ -352,7 +351,7 @@ export function FileUpload({ // For multiple files: Remove the specific file const filesArray = Array.isArray(value) ? value : value ? [value] : [] const updatedFiles = filesArray.filter((f) => f.path !== file.path) - setValue(updatedFiles.length > 0 ? updatedFiles : null) + setStoreValue(updatedFiles.length > 0 ? updatedFiles : null) // Make sure to update the subblock store value for the workflow execution useSubBlockStore @@ -360,7 +359,7 @@ export function FileUpload({ .setValue(blockId, subBlockId, updatedFiles.length > 0 ? updatedFiles : null) } else { // For single file: Clear the value - setValue(null) + setStoreValue(null) // Make sure to update the subblock store useSubBlockStore.getState().setValue(blockId, subBlockId, null) @@ -403,7 +402,7 @@ export function FileUpload({ setDeletingFiles(deletingStatus) // Clear input state immediately for better UX - setValue(null) + setStoreValue(null) useSubBlockStore.getState().setValue(blockId, subBlockId, null) useWorkflowStore.getState().triggerUpdate() diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/components/folder-selector-input.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/components/folder-selector-input.tsx index 828c8edc69..2be4a5953f 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/components/folder-selector-input.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/components/folder-selector-input.tsx @@ -14,7 +14,7 @@ interface FolderSelectorInputProps { subBlock: SubBlockConfig disabled?: boolean isPreview?: boolean - value?: string + previewValue?: any | null } export function FolderSelectorInput({ @@ -22,7 +22,7 @@ export function FolderSelectorInput({ subBlock, disabled = false, isPreview = false, - value: propValue + previewValue }: FolderSelectorInputProps) { const { getValue, setValue } = useSubBlockStore() const [selectedFolderId, setSelectedFolderId] = useState('') @@ -30,8 +30,8 @@ export function FolderSelectorInput({ // Get the current value from the store or prop value if in preview mode useEffect(() => { - if (isPreview && propValue !== undefined) { - setSelectedFolderId(propValue); + if (isPreview && previewValue !== undefined) { + setSelectedFolderId(previewValue); } else { const value = getValue(blockId, subBlock.id); if (value && typeof value === 'string') { @@ -44,7 +44,7 @@ export function FolderSelectorInput({ } } } - }, [blockId, subBlock.id, getValue, setValue, isPreview, propValue]); + }, [blockId, subBlock.id, getValue, setValue, isPreview, previewValue]); // Handle folder selection const handleFolderChange = (folderId: string, info?: FolderInfo) => { diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/long-input.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/long-input.tsx index 80870f36cd..fc3f86034d 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/long-input.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/long-input.tsx @@ -20,7 +20,9 @@ interface LongInputProps { config: SubBlockConfig rows?: number isPreview?: boolean + previewValue?: string | null value?: string + onChange?: (value: string) => void } // Constants @@ -36,9 +38,11 @@ export function LongInput({ config, rows, isPreview = false, + previewValue, value: propValue, + onChange, }: LongInputProps) { - const [value, setValue] = useSubBlockValue(blockId, subBlockId, false, isPreview, propValue) + const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) const [showEnvVars, setShowEnvVars] = useState(false) const [showTags, setShowTags] = useState(false) const [searchTerm, setSearchTerm] = useState('') @@ -48,6 +52,9 @@ export function LongInput({ const [activeSourceBlockId, setActiveSourceBlockId] = useState(null) const containerRef = useRef(null) + // Use preview value when in preview mode, otherwise use store value or prop value + const value = isPreview ? previewValue : (propValue !== undefined ? propValue : storeValue) + // Calculate initial height based on rows prop with reasonable defaults const getInitialHeight = () => { // Use provided rows or default, then convert to pixels with a minimum @@ -76,7 +83,14 @@ export function LongInput({ const handleChange = (e: React.ChangeEvent) => { const newValue = e.target.value const newCursorPosition = e.target.selectionStart ?? 0 - setValue(newValue) + + if (onChange) { + onChange(newValue) + } else if (!isPreview) { + // Only update store when not in preview mode + setStoreValue(newValue) + } + setCursorPosition(newCursorPosition) // Check for environment variables trigger @@ -171,7 +185,9 @@ export function LongInput({ // Update all state in a single batch Promise.resolve().then(() => { - setValue(newValue) + if (!isPreview) { + setStoreValue(newValue) + } setCursorPosition(dropPosition + 1) setShowTags(true) @@ -272,6 +288,7 @@ export function LongInput({ setShowTags(false) setSearchTerm('') }} + disabled={isPreview} style={{ fontFamily: 'inherit', lineHeight: 'inherit', @@ -302,29 +319,43 @@ export function LongInput({
- { - setShowEnvVars(false) - setSearchTerm('') - }} - /> - { - setShowTags(false) - setActiveSourceBlockId(null) - }} - /> +
+ { + if (onChange) { + onChange(newValue) + } else if (!isPreview) { + setStoreValue(newValue) + } + }} + searchTerm={searchTerm} + inputValue={value?.toString() ?? ''} + cursorPosition={cursorPosition} + onClose={() => { + setShowEnvVars(false) + setSearchTerm('') + }} + /> + { + if (onChange) { + onChange(newValue) + } else if (!isPreview) { + setStoreValue(newValue) + } + }} + blockId={blockId} + activeSourceBlockId={activeSourceBlockId} + inputValue={value?.toString() ?? ''} + cursorPosition={cursorPosition} + onClose={() => { + setShowTags(false) + setActiveSourceBlockId(null) + }} + /> +
) } diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx index 8ad73ca47b..f74e1cc185 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx @@ -20,7 +20,7 @@ interface ProjectSelectorInputProps { disabled?: boolean onProjectSelect?: (projectId: string) => void isPreview?: boolean - value?: string + previewValue?: any | null } export function ProjectSelectorInput({ @@ -29,7 +29,7 @@ export function ProjectSelectorInput({ disabled = false, onProjectSelect, isPreview = false, - value: propValue + previewValue }: ProjectSelectorInputProps) { const { getValue, setValue } = useSubBlockStore() const [selectedProjectId, setSelectedProjectId] = useState('') @@ -46,15 +46,15 @@ export function ProjectSelectorInput({ // Get the current value from the store or prop value if in preview mode useEffect(() => { - if (isPreview && propValue !== undefined) { - setSelectedProjectId(propValue); + if (isPreview && previewValue !== undefined) { + setSelectedProjectId(previewValue); } else { const value = getValue(blockId, subBlock.id); if (value && typeof value === 'string') { setSelectedProjectId(value); } } - }, [blockId, subBlock.id, getValue, isPreview, propValue]); + }, [blockId, subBlock.id, getValue, isPreview, previewValue]); // Handle project selection const handleProjectChange = ( diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/schedule/schedule-config.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/schedule/schedule-config.tsx index 1634673390..a5be0e3792 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/schedule/schedule-config.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/schedule/schedule-config.tsx @@ -17,18 +17,18 @@ const logger = createLogger('ScheduleConfig') interface ScheduleConfigProps { blockId: string - subBlockId?: string + subBlockId: string isConnecting: boolean isPreview?: boolean - value?: any + previewValue?: any | null } export function ScheduleConfig({ blockId, subBlockId, - isConnecting, + isConnecting, isPreview = false, - value: propValue + previewValue }: ScheduleConfigProps) { const [error, setError] = useState(null) const [scheduleId, setScheduleId] = useState(null) @@ -50,13 +50,18 @@ export function ScheduleConfig({ const setScheduleStatus = useWorkflowStore((state) => state.setScheduleStatus) // Get the schedule type from the block state - const [scheduleType] = useSubBlockValue(blockId, 'scheduleType', false, isPreview, propValue?.scheduleType) + const [scheduleType] = useSubBlockValue(blockId, 'scheduleType') // Get the startWorkflow value to determine if scheduling is enabled // and expose the setter so we can update it - const [startWorkflow, setStartWorkflow] = useSubBlockValue(blockId, 'startWorkflow', false, isPreview, propValue?.startWorkflow) + const [startWorkflow, setStartWorkflow] = useSubBlockValue(blockId, 'startWorkflow') const isScheduleEnabled = startWorkflow === 'schedule' + const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) + + // Use preview value when in preview mode, otherwise use store value + const value = isPreview ? previewValue : storeValue + // Function to check if schedule exists in the database const checkSchedule = async () => { setIsLoading(true) @@ -132,6 +137,7 @@ export function ScheduleConfig({ } const handleOpenModal = () => { + if (isPreview) return setIsModalOpen(true) } @@ -145,6 +151,8 @@ export function ScheduleConfig({ } const handleSaveSchedule = async (): Promise => { + if (isPreview) return false + setIsSaving(true) setError(null) @@ -247,7 +255,7 @@ export function ScheduleConfig({ } const handleDeleteSchedule = async (): Promise => { - if (!scheduleId) return false + if (isPreview || !scheduleId) return false setIsDeleting(true) try { @@ -320,7 +328,7 @@ export function ScheduleConfig({ size='icon' className='h-8 w-8 shrink-0' onClick={handleOpenModal} - disabled={isDeleting || isConnecting} + disabled={isPreview || isDeleting || isConnecting} > {isDeleting ? (
@@ -336,7 +344,7 @@ export function ScheduleConfig({ size='sm' className='flex h-10 w-full items-center bg-background font-normal text-sm' onClick={handleOpenModal} - disabled={isConnecting || isSaving || isDeleting} + disabled={isPreview || isConnecting || isSaving || isDeleting} > {isLoading ? (
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/short-input.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/short-input.tsx index 3a837677ec..d4af8abece 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/short-input.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/short-input.tsx @@ -21,6 +21,7 @@ interface ShortInputProps { value?: string onChange?: (value: string) => void isPreview?: boolean + previewValue?: string | null } export function ShortInput({ @@ -30,9 +31,10 @@ export function ShortInput({ password, isConnecting, config, - value: propValue, onChange, + value: propValue, isPreview = false, + previewValue }: ShortInputProps) { const [isFocused, setIsFocused] = useState(false) const [showEnvVars, setShowEnvVars] = useState(false) @@ -48,10 +50,7 @@ export function ShortInput({ } const [storeValue, setStoreValue] = useSubBlockValue( blockId, - subBlockId, - false, // No workflow update needed - isPreview, - validatePropValue(propValue) + subBlockId ) const [searchTerm, setSearchTerm] = useState('') const [cursorPosition, setCursorPosition] = useState(0) @@ -62,10 +61,8 @@ export function ShortInput({ // Get ReactFlow instance for zoom control const reactFlowInstance = useReactFlow() - // Use either controlled or uncontrolled value, prioritizing the direct value if in preview mode - const value = isPreview && propValue !== undefined - ? propValue - : (propValue !== undefined ? propValue : storeValue) + // Use preview value when in preview mode, otherwise use store value or prop value + const value = isPreview ? previewValue : (propValue !== undefined ? propValue : storeValue) // Check if this input is API key related const isApiKeyField = useMemo(() => { @@ -103,7 +100,8 @@ export function ShortInput({ if (onChange) { onChange(newValue) - } else { + } else if (!isPreview) { + // Only update store when not in preview mode setStoreValue(newValue) } @@ -284,7 +282,8 @@ export function ShortInput({ if (onChange) { onChange(newValue) - } else { + } else if (!isPreview) { + // Only update store when not in preview mode setStoreValue(newValue) } } @@ -332,6 +331,7 @@ export function ShortInput({ onKeyDown={handleKeyDown} autoComplete='off' style={{ overflowX: 'auto' }} + disabled={isPreview} />
(blockId, subBlockId, false, isPreview, propValue) + const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) + + // Use preview value when in preview mode, otherwise use store value + const value = isPreview ? previewValue : storeValue // Clamp the value within bounds while preserving relative position when possible - const normalizedValue = useMemo(() => { - if (value === null) return defaultValue + const normalizedValue = value !== null && value !== undefined + ? Math.max(min, Math.min(max, value)) + : defaultValue - // If value exceeds max, scale it down proportionally - if (value > max) { - const prevMax = Math.max(max * 2, value) // Assume previous max was at least the current value - const scaledValue = (value / prevMax) * max - return integer ? Math.round(scaledValue) : scaledValue - } + const displayValue = normalizedValue ?? defaultValue - // Otherwise just clamp it - const clampedValue = Math.min(Math.max(value, min), max) - return integer ? Math.round(clampedValue) : clampedValue - }, [value, min, max, defaultValue, integer]) - - // Update the value if it needs normalization + // Ensure the normalized value is set if it differs from the current value useEffect(() => { - if (value !== null && value !== normalizedValue) { - setValue(normalizedValue) + if (!isPreview && value !== null && value !== undefined && value !== normalizedValue) { + setStoreValue(normalizedValue) } - }, [normalizedValue, value, setValue]) + }, [normalizedValue, value, setStoreValue, isPreview]) + + const handleValueChange = (newValue: number[]) => { + if (!isPreview) { + const processedValue = integer ? Math.round(newValue[0]) : newValue[0] + setStoreValue(processedValue) + } + } return ( -
- setValue(integer ? Math.round(value[0]) : value[0])} - className='[&_[class*=SliderTrack]]:h-1 [&_[role=slider]]:h-4 [&_[role=slider]]:w-4' - /> -
{ - const percentage = ((normalizedValue - min) / (max - min)) * 100 - const bias = -25 * Math.sin((percentage * Math.PI) / 50) - return percentage === 0 ? 0 : percentage === 100 ? 100 : 50 + bias - })()}%)`, - top: '24px', - }} - > - {integer ? Math.round(normalizedValue).toString() : Number(normalizedValue).toFixed(1)} +
+
+ +
+
+ {displayValue}
) diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/starter/input-format.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/starter/input-format.tsx index 62c835224a..a94b4d5615 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/starter/input-format.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/starter/input-format.tsx @@ -24,7 +24,7 @@ interface InputFormatProps { blockId: string subBlockId: string isPreview?: boolean - value?: InputField[] + previewValue?: InputField[] | null } // Default values @@ -35,32 +35,43 @@ const DEFAULT_FIELD: InputField = { collapsed: true, } -export function InputFormat({ blockId, subBlockId, isPreview = false, value: propValue }: InputFormatProps) { - // State hooks - const [value, setValue] = useSubBlockValue(blockId, subBlockId, false, isPreview, propValue) - const fields = value || [DEFAULT_FIELD] +export function InputFormat({ + blockId, + subBlockId, + isPreview = false, + previewValue +}: InputFormatProps) { + const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) + + // Use preview value when in preview mode, otherwise use store value + const value = isPreview ? previewValue : storeValue + const fields: InputField[] = value || [DEFAULT_FIELD] // Field operations const addField = () => { + if (isPreview) return + const newField: InputField = { ...DEFAULT_FIELD, id: crypto.randomUUID(), } - setValue([...fields, newField]) + setStoreValue([...fields, newField]) } const removeField = (id: string) => { - if (fields.length === 1) return - setValue(fields.filter((field) => field.id !== id)) + if (isPreview || fields.length === 1) return + setStoreValue(fields.filter((field: InputField) => field.id !== id)) } // Update handlers const updateField = (id: string, field: keyof InputField, value: any) => { - setValue(fields.map((f) => (f.id === id ? { ...f, [field]: value } : f))) + if (isPreview) return + setStoreValue(fields.map((f: InputField) => (f.id === id ? { ...f, [field]: value } : f))) } const toggleCollapse = (id: string) => { - setValue(fields.map((f) => (f.id === id ? { ...f, collapsed: !f.collapsed } : f))) + if (isPreview) return + setStoreValue(fields.map((f: InputField) => (f.id === id ? { ...f, collapsed: !f.collapsed } : f))) } // Field header @@ -87,18 +98,18 @@ export function InputFormat({ blockId, subBlockId, isPreview = false, value: pro )}
-
e.stopPropagation()}> -
@@ -146,8 +158,8 @@ export function InputFormat({ blockId, subBlockId, isPreview = false, value: pro -
- - - - - No tools found - - + + + + No tools found. + {toolBlocks.map((block) => ( + + { + if (!isPreview) { + handleSelectTool(block) + setOpen(false) + } + }} + disabled={isPreview || isToolAlreadySelected(block.type)} + > +
+
+ + {block.name} +
+
+
+
+ ))} + + + { - setOpen(false) - setCustomToolModalOpen(true) + if (!isPreview) { + setCustomToolModalOpen(true) + setOpen(false) + } }} - className='mb-1 flex cursor-pointer items-center gap-2' + disabled={isPreview} > -
- +
+ + Create Custom Tool
- Create Tool - - - {/* Display saved custom tools at the top */} - {customTools.length > 0 && ( - <> - -
- Custom Tools -
- - {customTools.map((customTool) => ( - { - const newTool: StoredTool = { - type: 'custom-tool', - title: customTool.title, - params: {}, - isExpanded: true, - schema: customTool.schema, - code: customTool.code, - usageControl: 'auto', - } - - if (isWide) { - setValue([ - ...selectedTools.map((tool, index) => ({ - ...tool, - isExpanded: - Math.floor(selectedTools.length / 2) === - Math.floor(index / 2), - })), - newTool, - ]) - } else { - setValue([ - ...selectedTools.map((tool) => ({ - ...tool, - isExpanded: false, - })), - newTool, - ]) - } - setOpen(false) - }} - className='flex cursor-pointer items-center gap-2' - > -
- -
- {customTool.title} -
- ))} -
- - - )} - - {/* Display built-in tools */} - {toolBlocks.some((block) => customFilter(block.name, searchQuery || '') > 0) && ( - <> -
- Built-in Tools -
- - {toolBlocks.map((block) => ( - handleSelectTool(block)} - className='flex cursor-pointer items-center gap-2' - > -
- -
- {block.name} -
- ))} -
- - )} - - - + + + + ) : ( @@ -935,50 +888,52 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV } return ( -
-
- {formatParamId(param.id)} - {param.optionalToolInput && !param.requiredForToolCall && ( - - (Optional) - - )} -
-
- {useChannelSelector && channelSelectorConfig ? ( - { - handleParamChange(toolIndex, param.id, channelId) - }} - /> - ) : ( - - handleParamChange(toolIndex, param.id, value) - } - /> - )} +
+
+
+ {formatParamId(param.id)} + {param.optionalToolInput && !param.requiredForToolCall && ( + + (Optional) + + )} +
+
+ {useChannelSelector && channelSelectorConfig ? ( + { + handleParamChange(toolIndex, param.id, channelId) + }} + /> + ) : ( + + handleParamChange(toolIndex, param.id, value) + } + /> + )} +
) @@ -989,6 +944,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
) })} +
@@ -565,7 +581,7 @@ export function WebhookConfig({ size='sm' className='flex h-10 w-full items-center bg-background font-normal text-sm' onClick={handleOpenModal} - disabled={isConnecting || isSaving || isDeleting || !gmailCredentialId} + disabled={isConnecting || isSaving || isDeleting || !gmailCredentialId || isPreview} > {isLoading ? (
@@ -617,7 +633,7 @@ export function WebhookConfig({ size='sm' className='flex h-10 w-full items-center bg-background font-normal text-sm' onClick={handleOpenModal} - disabled={isConnecting || isSaving || isDeleting} + disabled={isConnecting || isSaving || isDeleting || isPreview} > {isLoading ? (
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts index d578a62eaa..9f5bf2fa4a 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts @@ -1,14 +1,10 @@ import { useCallback, useEffect, useRef } from 'react' import { isEqual } from 'lodash' import { getProviderFromModel } from '@/providers/utils' -import { createLogger } from '@/lib/logs/console-logger' import { useGeneralStore } from '@/stores/settings/general/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' -// Add logger for diagnostic logging -const logger = createLogger('useSubBlockValue') - /** * Helper to handle API key auto-fill for provider-based blocks * Used for agent, router, evaluator, and any other blocks that use LLM providers @@ -152,8 +148,6 @@ function storeApiKeyValue( * @param blockId The ID of the block containing the sub-block * @param subBlockId The ID of the sub-block * @param triggerWorkflowUpdate Whether to trigger a workflow update when the value changes - * @param isPreview Whether this is being used in preview mode - * @param directValue The direct value to use when in preview mode * @returns A tuple containing the current value and a setter function */ export function useSubBlockValue( @@ -179,36 +173,12 @@ export function useSubBlockValue( // Previous model reference for detecting model changes const prevModelRef = useRef(null) - - // Ref to track if we're in preview mode and have direct values - const previewDataRef = useRef<{ - isInPreview: boolean, - directValue: T | null - }>({ - isInPreview: isPreview, - directValue: directValue as T || null - }) // Get value from subblock store - always call this hook unconditionally const storeValue = useSubBlockStore( useCallback((state) => state.getValue(blockId, subBlockId), [blockId, subBlockId]) ) - // Directly update preview data when props change - useEffect(() => { - if (isPreview && directValue !== undefined) { - previewDataRef.current = { - isInPreview: true, - directValue: directValue as T || null - }; - valueRef.current = directValue as T || null; - - } else if (!isPreview && previewDataRef.current.isInPreview) { - // Reset preview flag when isPreview prop changes to false - previewDataRef.current.isInPreview = false; - } - }, [isPreview, directValue, blockId, subBlockId]); - // Check if this is an API key field that could be auto-filled const isApiKey = subBlockId === 'apiKey' || (subBlockId?.toLowerCase().includes('apikey') ?? false) @@ -228,46 +198,10 @@ export function useSubBlockValue( // Compute the modelValue based on block type const modelValue = isProviderBasedBlock ? (modelSubBlockValue as string) : null - // Initialize valueRef on first render - useEffect(() => { - // If we're in preview mode with direct values, use those - if (previewDataRef.current.isInPreview) { - valueRef.current = previewDataRef.current.directValue; - } else { - // Otherwise use the store value or initial value - valueRef.current = storeValue !== undefined ? storeValue : initialValue; - } - }, [storeValue, initialValue, isPreview]) - - // Update the ref if the store value changes - // This ensures we're always working with the latest value - useEffect(() => { - // Skip updates from global store if we're using preview values - if (previewDataRef.current.isInPreview) return; - - // Use deep comparison for objects to prevent unnecessary updates - if (!isEqual(valueRef.current, storeValue)) { - valueRef.current = storeValue !== undefined ? storeValue : initialValue - } - }, [storeValue, initialValue]) - - // Create a preview-aware setValue function - const setValueWithPreview = useCallback( + // Hook to set a value in the subblock store + const setValue = useCallback( (newValue: T) => { - // If we're in preview mode, just update the local valueRef for display - // but don't update the global store - if (previewDataRef.current.isInPreview) { - // Only update if the value has changed - if (!isEqual(valueRef.current, newValue)) { - valueRef.current = newValue; - // Update the ref as well - previewDataRef.current.directValue = newValue; - } - // Return early without updating global state - return; - } - - // For non-preview mode, use the normal setValue logic + // Use deep comparison to avoid unnecessary updates for complex objects if (!isEqual(valueRef.current, newValue)) { valueRef.current = newValue @@ -296,6 +230,11 @@ export function useSubBlockValue( [blockId, subBlockId, blockType, isApiKey, storeValue, triggerWorkflowUpdate, modelValue] ) + // Initialize valueRef on first render + useEffect(() => { + valueRef.current = storeValue !== undefined ? storeValue : initialValue + }, []) + // When component mounts, check for existing API key in toolParamsStore useEffect(() => { // Skip autofill if the feature is disabled in settings @@ -351,5 +290,14 @@ export function useSubBlockValue( isProviderBasedBlock, ]) - return [valueRef.current as T | null, setValueWithPreview] as const -} + // Update the ref if the store value changes + // This ensures we're always working with the latest value + useEffect(() => { + // Use deep comparison for objects to prevent unnecessary updates + if (!isEqual(valueRef.current, storeValue)) { + valueRef.current = storeValue !== undefined ? storeValue : initialValue + } + }, [storeValue, initialValue]) + + return [valueRef.current as T | null, setValue] as const +} \ No newline at end of file diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx index 76e98c53b9..f954a04d13 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx @@ -1,7 +1,7 @@ -import { useEffect } from 'react' import { Info } from 'lucide-react' import { Label } from '@/components/ui/label' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' import { getBlock } from '@/blocks/index' import type { SubBlockConfig } from '@/blocks/types' @@ -27,26 +27,27 @@ import { TimeInput } from './components/time-input' import { ToolInput } from './components/tool-input/tool-input' import { WebhookConfig } from './components/webhook/webhook' - interface SubBlockProps { blockId: string config: SubBlockConfig isConnecting: boolean isPreview?: boolean - previewValue?: any + subBlockValues?: Record } export function SubBlock({ blockId, config, - isConnecting, - isPreview = false, - previewValue = undefined + isConnecting, + isPreview = false, + subBlockValues }: SubBlockProps) { const handleMouseDown = (e: React.MouseEvent) => { e.stopPropagation() } + const { getValue } = useSubBlockStore() + const isFieldRequired = () => { const blockType = useWorkflowStore.getState().blocks[blockId]?.type if (!blockType) return false @@ -57,9 +58,14 @@ export function SubBlock({ return blockConfig.inputs[config.id]?.required === true } + // Get preview value for this specific sub-block + const getPreviewValue = () => { + if (!isPreview || !subBlockValues) return undefined + return subBlockValues[config.id]?.value ?? null + } + const renderInput = () => { - // Get the subblock value from the config if available - const directValue = isPreview ? previewValue : undefined; + const previewValue = getPreviewValue() switch (config.type) { case 'short-input': @@ -72,7 +78,7 @@ export function SubBlock({ isConnecting={isConnecting} config={config} isPreview={isPreview} - value={directValue} + previewValue={previewValue} /> ) case 'long-input': @@ -85,7 +91,7 @@ export function SubBlock({ rows={config.rows} config={config} isPreview={isPreview} - value={directValue} + previewValue={previewValue} /> ) case 'dropdown': @@ -96,7 +102,7 @@ export function SubBlock({ subBlockId={config.id} options={config.options as string[]} isPreview={isPreview} - value={directValue} + previewValue={previewValue} />
) @@ -111,7 +117,7 @@ export function SubBlock({ step={config.step} integer={config.integer} isPreview={isPreview} - value={directValue} + previewValue={previewValue} /> ) case 'table': @@ -120,7 +126,7 @@ export function SubBlock({ subBlockId={config.id} columns={config.columns ?? []} isPreview={isPreview} - value={directValue} + previewValue={previewValue} /> case 'code': return ( @@ -132,7 +138,7 @@ export function SubBlock({ language={config.language} generationType={config.generationType} isPreview={isPreview} - value={directValue} + previewValue={previewValue} /> ) case 'switch': @@ -141,14 +147,14 @@ export function SubBlock({ subBlockId={config.id} title={config.title ?? ''} isPreview={isPreview} - value={directValue} + previewValue={previewValue} /> case 'tool-input': return case 'checkbox-list': return ( @@ -159,7 +165,7 @@ export function SubBlock({ options={config.options as { label: string; id: string }[]} layout={config.layout} isPreview={isPreview} - value={directValue} + subBlockValues={subBlockValues} /> ) case 'condition-input': @@ -169,7 +175,7 @@ export function SubBlock({ subBlockId={config.id} isConnecting={isConnecting} isPreview={isPreview} - value={directValue} + previewValue={previewValue} /> ) case 'eval-input': @@ -177,7 +183,7 @@ export function SubBlock({ blockId={blockId} subBlockId={config.id} isPreview={isPreview} - value={directValue} + previewValue={previewValue} /> case 'date-input': return ( @@ -186,7 +192,7 @@ export function SubBlock({ subBlockId={config.id} placeholder={config.placeholder} isPreview={isPreview} - value={directValue} + previewValue={previewValue} /> ) case 'time-input': @@ -196,7 +202,7 @@ export function SubBlock({ subBlockId={config.id} placeholder={config.placeholder} isPreview={isPreview} - value={directValue} + previewValue={previewValue} /> ) case 'file-upload': @@ -208,7 +214,7 @@ export function SubBlock({ multiple={config.multiple === true} maxSize={config.maxSize} isPreview={isPreview} - value={directValue} + previewValue={previewValue} /> ) case 'webhook-config': @@ -216,9 +222,9 @@ export function SubBlock({ ) case 'schedule-config': @@ -228,60 +234,63 @@ export function SubBlock({ subBlockId={config.id} isConnecting={isConnecting} isPreview={isPreview} - value={directValue} + previewValue={previewValue} /> ) case 'oauth-input': return ( { - // Use the workflow store to update the value - const event = new CustomEvent('update-subblock-value', { - detail: { - blockId, - subBlockId: config.id, - value, - }, - }) - window.dispatchEvent(event) + // Only allow changes in non-preview mode + if (!isPreview) { + const event = new CustomEvent('update-subblock-value', { + detail: { + blockId, + subBlockId: config.id, + value, + }, + }) + window.dispatchEvent(event) + } }} provider={config.provider as any} requiredScopes={config.requiredScopes || []} label={config.placeholder || 'Select a credential'} serviceId={config.serviceId} + disabled={isPreview} /> ) case 'file-selector': return case 'project-selector': return case 'folder-selector': return case 'input-format': return default: return
Unknown input type: {config.type}
@@ -327,4 +336,4 @@ export function SubBlock({ {renderInput()}
) -} +} \ No newline at end of file diff --git a/apps/sim/app/w/[id]/components/workflow-block/workflow-block.tsx b/apps/sim/app/w/[id]/components/workflow-block/workflow-block.tsx index 6276e3aa8a..393c301eba 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/workflow-block.tsx @@ -16,7 +16,6 @@ import { ActionBar } from './components/action-bar/action-bar' import { ConnectionBlocks } from './components/connection-blocks/connection-blocks' import { SubBlock } from './components/sub-block/sub-block' - interface WorkflowBlockProps { type: string config: BlockConfig @@ -24,9 +23,7 @@ interface WorkflowBlockProps { isActive?: boolean isPending?: boolean isPreview?: boolean - isReadOnly?: boolean subBlockValues?: Record - blockState?: any } // Combine both interfaces into a single component @@ -42,11 +39,7 @@ export function WorkflowBlock({ id, data }: NodeProps) { nextRunAt: string | null lastRanAt: string | null timezone: string - status?: string - isDisabled?: boolean - id?: string } | null>(null) - const [isLoadingScheduleInfo, setIsLoadingScheduleInfo] = useState(false) const [webhookInfo, setWebhookInfo] = useState<{ webhookPath: string provider: string @@ -66,9 +59,8 @@ export function WorkflowBlock({ id, data }: NodeProps) { ) const isWide = useWorkflowStore((state) => state.blocks[id]?.isWide ?? false) const blockHeight = useWorkflowStore((state) => state.blocks[id]?.height ?? 0) + const hasActiveSchedule = useWorkflowStore((state) => state.hasActiveSchedule ?? false) const hasActiveWebhook = useWorkflowStore((state) => state.hasActiveWebhook ?? false) - const blockAdvancedMode = useWorkflowStore((state) => state.blocks[id]?.advancedMode ?? false) - const toggleBlockAdvancedMode = useWorkflowStore((state) => state.toggleBlockAdvancedMode) // Workflow store actions const updateBlockName = useWorkflowStore((state) => state.updateBlockName) @@ -79,106 +71,49 @@ export function WorkflowBlock({ id, data }: NodeProps) { const isActiveBlock = useExecutionStore((state) => state.activeBlockIds.has(id)) const isActive = dataIsActive || isActiveBlock - const reactivateSchedule = async (scheduleId: string) => { - try { - const response = await fetch(`/api/schedules/${scheduleId}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ action: 'reactivate' }), - }) - - if (response.ok) { - fetchScheduleInfo() - } else { - console.error('Failed to reactivate schedule') - } - } catch (error) { - console.error('Error reactivating schedule:', error) - } - } - - const fetchScheduleInfo = async () => { - try { - setIsLoadingScheduleInfo(true) - const workflowId = useWorkflowRegistry.getState().activeWorkflowId - if (!workflowId) return - - const response = await fetch(`/api/schedules?workflowId=${workflowId}&mode=schedule`, { - cache: 'no-store', - headers: { - 'Cache-Control': 'no-cache', - }, - }) - - if (!response.ok) { - setScheduleInfo(null) - return - } - - const data = await response.json() - - if (!data.schedule) { - setScheduleInfo(null) - return - } - - let scheduleTiming = 'Unknown schedule' - if (data.schedule.cronExpression) { - scheduleTiming = parseCronToHumanReadable(data.schedule.cronExpression) - } - - const baseInfo = { - scheduleTiming, - nextRunAt: data.schedule.nextRunAt as string | null, - lastRanAt: data.schedule.lastRanAt as string | null, - timezone: data.schedule.timezone || 'UTC', - status: data.schedule.status as string, - isDisabled: data.schedule.status === 'disabled', - id: data.schedule.id as string, - } - - try { - const statusRes = await fetch(`/api/schedules/${baseInfo.id}/status`, { - cache: 'no-store', - headers: { 'Cache-Control': 'no-cache' }, - }) - - if (statusRes.ok) { - const statusData = await statusRes.json() - setScheduleInfo({ - scheduleTiming: baseInfo.scheduleTiming, - nextRunAt: statusData.nextRunAt ?? baseInfo.nextRunAt, - lastRanAt: statusData.lastRanAt ?? baseInfo.lastRanAt, - timezone: baseInfo.timezone, - status: statusData.status ?? baseInfo.status, - isDisabled: statusData.isDisabled ?? baseInfo.isDisabled, - id: baseInfo.id, - }) - return - } - } catch (err) { - console.error('Error fetching schedule status:', err) - } - - setScheduleInfo(baseInfo) - } catch (error) { - console.error('Error fetching schedule info:', error) - setScheduleInfo(null) - } finally { - setIsLoadingScheduleInfo(false) - } - } - + // Get schedule information for the tooltip useEffect(() => { - if (type === 'starter') { + if (type === 'starter' && hasActiveSchedule) { + const fetchScheduleInfo = async () => { + try { + const workflowId = useWorkflowRegistry.getState().activeWorkflowId + if (!workflowId) return + + const response = await fetch(`/api/schedules?workflowId=${workflowId}&mode=schedule`, { + cache: 'no-store', + headers: { + 'Cache-Control': 'no-cache', + }, + }) + + if (response.ok) { + const data = await response.json() + if (data.schedule) { + let scheduleTiming = 'Unknown schedule' + if (data.schedule.cronExpression) { + scheduleTiming = parseCronToHumanReadable(data.schedule.cronExpression) + } + + setScheduleInfo({ + scheduleTiming, + nextRunAt: data.schedule.nextRunAt, + lastRanAt: data.schedule.lastRanAt, + timezone: data.schedule.timezone || 'UTC', + }) + } + } + } catch (error) { + console.error('Error fetching schedule info:', error) + } + } + fetchScheduleInfo() - } else { + } else if (!hasActiveSchedule) { setScheduleInfo(null) } - }, [type]) + }, [type, hasActiveSchedule]) + // Get webhook information for the tooltip useEffect(() => { if (type === 'starter' && hasActiveWebhook) { const fetchWebhookInfo = async () => { @@ -208,6 +143,7 @@ export function WorkflowBlock({ id, data }: NodeProps) { } }, [type, hasActiveWebhook]) + // Update node internals when handles change useEffect(() => { updateNodeInternals(id) }, [id, horizontalHandles, updateNodeInternals]) @@ -220,6 +156,7 @@ export function WorkflowBlock({ id, data }: NodeProps) { } } + // Add effect to observe size changes with debounced updates useEffect(() => { if (!contentRef.current) return @@ -232,10 +169,12 @@ export function WorkflowBlock({ id, data }: NodeProps) { }, 100) const resizeObserver = new ResizeObserver((entries) => { + // Cancel any pending animation frame if (rafId) { cancelAnimationFrame(rafId) } + // Schedule the update on the next animation frame rafId = requestAnimationFrame(() => { for (const entry of entries) { const height = @@ -261,7 +200,7 @@ export function WorkflowBlock({ id, data }: NodeProps) { let currentRow: SubBlockConfig[] = [] let currentRowWidth = 0 - // Get merged state for this block - use direct props if in preview mode + // Get merged state for this block const blocks = useWorkflowStore.getState().blocks const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId || undefined const isAdvancedMode = useWorkflowStore((state) => state.blocks[id]?.advancedMode ?? false) @@ -279,9 +218,9 @@ export function WorkflowBlock({ id, data }: NodeProps) { if (!block.condition) return true // Get the values of the fields this block depends on from merged state - const fieldValue = mergedState?.[blockId]?.subBlocks[block.condition.field]?.value + const fieldValue = mergedState?.subBlocks[block.condition.field]?.value const andFieldValue = block.condition.and - ? mergedState?.[blockId]?.subBlocks[block.condition.and.field]?.value + ? mergedState?.subBlocks[block.condition.and.field]?.value : undefined // Check if the condition value is an array @@ -368,8 +307,9 @@ export function WorkflowBlock({ id, data }: NodeProps) { } } - // Check if this is a starter block and if we need to show schedule / webhook indicators + // Check if this is a starter block and has active schedule or webhook const isStarterBlock = type === 'starter' + const showScheduleIndicator = isStarterBlock && hasActiveSchedule const showWebhookIndicator = isStarterBlock && hasActiveWebhook const getProviderName = (providerId: string): string => { @@ -386,8 +326,6 @@ export function WorkflowBlock({ id, data }: NodeProps) { return providers[providerId] || 'Webhook' } - const shouldShowScheduleBadge = isStarterBlock && !isLoadingScheduleInfo && scheduleInfo !== null - return (
) { isPending && 'ring-2 ring-amber-500', 'z-[20]' )} - data-id={id} - data-props={data.isPreview ? JSON.stringify({ - isPreview: data.isPreview, - isReadOnly: data.isReadOnly, - blockType: type, - data: { - subBlockValues: data.subBlockValues - } - }) : undefined} > {/* Show debug indicator for pending blocks */} {isPending && ( @@ -502,7 +431,7 @@ export function WorkflowBlock({ id, data }: NodeProps) { )} {/* Schedule indicator badge - displayed for starter blocks with active schedules */} - {shouldShowScheduleBadge && ( + {showScheduleIndicator && ( ) { )} />
- {scheduleInfo?.isDisabled ? 'Disabled' : 'Scheduled'} + Scheduled @@ -598,7 +527,7 @@ export function WorkflowBlock({ id, data }: NodeProps) { )} - {config.subBlocks.some((block) => block.mode) && ( + {config.longDescription && (
))} @@ -806,4 +734,4 @@ export function WorkflowBlock({ id, data }: NodeProps) {
) -} +} \ No newline at end of file diff --git a/apps/sim/app/w/components/workflow-preview/workflow-preview.tsx b/apps/sim/app/w/components/workflow-preview/workflow-preview.tsx index ee9209ca43..89685d113f 100644 --- a/apps/sim/app/w/components/workflow-preview/workflow-preview.tsx +++ b/apps/sim/app/w/components/workflow-preview/workflow-preview.tsx @@ -27,23 +27,13 @@ import { WorkflowEdge } from '@/app/w/[id]/components/workflow-edge/workflow-edg // import { LoopLabel } from '@/app/w/[id]/components/workflow-loop/components/loop-label/loop-label' // import { createLoopNode } from '@/app/w/[id]/components/workflow-loop/workflow-loop' import { getBlock } from '@/blocks' -import type { SubBlockConfig } from '@/blocks/types' +import { WorkflowState } from '@/stores/workflows/workflow/types' const logger = createLogger('WorkflowPreview') interface WorkflowPreviewProps { // The workflow state to render - workflowState: { - blocks: Record - edges: Array<{ - id: string - source: string - target: string - sourceHandle?: string - targetHandle?: string - }> - loops: Record - } + workflowState: WorkflowState // Whether to show subblocks showSubBlocks?: boolean // Optional className for container styling @@ -71,7 +61,6 @@ const edgeTypes: EdgeTypes = { export function WorkflowPreview({ workflowState, showSubBlocks = true, - className, height = '100%', width = '100%', isPannable = false, @@ -203,7 +192,7 @@ export function WorkflowPreview({
| string // Items or expression } -export interface Parallel { - id: string - nodes: string[] - distribution?: any[] | Record | string // Items or expression -} - -export interface DeploymentStatus { - isDeployed: boolean - deployedAt?: Date - apiKey?: string - needsRedeployment?: boolean -} - export interface WorkflowState { blocks: Record edges: Edge[]