mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 23:17:59 -05:00
cleaned up logic
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void>
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, any>
|
||||
edges: Array<any>
|
||||
loops: Record<string, any>
|
||||
_metadata?: {
|
||||
workflowId?: string
|
||||
fetchTimestamp?: number
|
||||
requestId?: number
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
deployedWorkflowState: {
|
||||
blocks: Record<string, any>
|
||||
edges: Array<any>
|
||||
loops: Record<string, any>
|
||||
_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({
|
||||
<Card className={cn('relative overflow-hidden', className)}>
|
||||
<CardHeader
|
||||
className={cn(
|
||||
'sticky top-0 z-10 space-y-4 p-4',
|
||||
'backdrop-blur-xl',
|
||||
'space-y-4 p-4 sticky top-0 z-10',
|
||||
'bg-background/70 dark:bg-background/50',
|
||||
'border-border/30 border-b dark:border-border/20',
|
||||
'shadow-sm'
|
||||
@@ -129,13 +108,13 @@ export function DeployedWorkflowCard({
|
||||
|
||||
<div className="h-px w-full bg-border shadow-sm"></div>
|
||||
|
||||
<CardContent className='p-0'>
|
||||
<CardContent className="p-0">
|
||||
{/* Workflow preview with fixed height */}
|
||||
<div className="h-[500px] w-full">
|
||||
{sanitizedWorkflowState ? (
|
||||
{/* {sanitizedWorkflowState ? ( */}
|
||||
<WorkflowPreview
|
||||
key={previewKey}
|
||||
workflowState={sanitizedWorkflowState}
|
||||
workflowState={workflowToShow as WorkflowState}
|
||||
showSubBlocks={true}
|
||||
height='100%'
|
||||
width='100%'
|
||||
@@ -143,11 +122,6 @@ export function DeployedWorkflowCard({
|
||||
defaultPosition={{ x: 0, y: 0 }}
|
||||
defaultZoom={1}
|
||||
/>
|
||||
) : (
|
||||
<div className='flex h-full items-center justify-center text-muted-foreground'>
|
||||
No workflow data available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -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<string, any>
|
||||
edges: Array<any>
|
||||
loops: Record<string, any>
|
||||
parallels: Record<string, any>
|
||||
_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({
|
||||
</DialogHeader>
|
||||
</div>
|
||||
<DeployedWorkflowCard
|
||||
currentWorkflowState={sanitizedCurrentState}
|
||||
deployedWorkflowState={sanitizedDeployedState}
|
||||
currentWorkflowState={currentWorkflowState}
|
||||
deployedWorkflowState={deployedWorkflowState}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
|
||||
@@ -125,7 +125,7 @@ export function DeploymentControls({
|
||||
workflowId={activeWorkflowId}
|
||||
needsRedeployment={workflowNeedsRedeployment}
|
||||
setNeedsRedeployment={setNeedsRedeployment}
|
||||
deployedState={deployedState}
|
||||
deployedState={deployedState as WorkflowState}
|
||||
isLoadingDeployedState={isLoadingDeployedState}
|
||||
refetchDeployedState={refetchWithErrorHandling}
|
||||
/>
|
||||
|
||||
@@ -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<any>(null)
|
||||
// Deployed state management
|
||||
const [deployedState, setDeployedState] = useState<WorkflowState | null>(null)
|
||||
const [isLoadingDeployedState, setIsLoadingDeployedState] = useState<boolean>(false)
|
||||
|
||||
// Add refs to manage fetch state and prevent race conditions
|
||||
const abortControllerRef = useRef<AbortController | null>(null)
|
||||
const lastFetchedWorkflowIdRef = useRef<string | null>(null)
|
||||
const lastDeployedStateRef = useRef<boolean>(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}
|
||||
/>
|
||||
)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ interface CheckboxListProps {
|
||||
options: { label: string; id: string }[]
|
||||
layout?: 'full' | 'half'
|
||||
isPreview?: boolean
|
||||
value?: Record<string, boolean>
|
||||
subBlockValues?: Record<string, any>
|
||||
}
|
||||
|
||||
export function CheckboxList({
|
||||
@@ -21,25 +21,36 @@ export function CheckboxList({
|
||||
options,
|
||||
layout,
|
||||
isPreview = false,
|
||||
value: propValues
|
||||
subBlockValues
|
||||
}: CheckboxListProps) {
|
||||
return (
|
||||
<div className={cn('grid gap-4', layout === 'half' ? 'grid-cols-2' : 'grid-cols-1', 'pt-1')}>
|
||||
{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 (
|
||||
<div key={option.id} className='flex items-center space-x-2'>
|
||||
<Checkbox
|
||||
id={`${blockId}-${option.id}`}
|
||||
checked={Boolean(value)}
|
||||
onCheckedChange={(checked) => setValue(checked as boolean)}
|
||||
onCheckedChange={handleChange}
|
||||
disabled={isPreview}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`${blockId}-${option.id}`}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useState, useMemo } from 'react'
|
||||
import type { ReactElement } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Wand2 } from 'lucide-react'
|
||||
import { highlight, languages } from 'prismjs'
|
||||
import 'prismjs/components/prism-javascript'
|
||||
@@ -25,8 +25,9 @@ interface CodeProps {
|
||||
placeholder?: string
|
||||
language?: 'javascript' | 'json'
|
||||
generationType?: 'javascript-function-body' | 'json-schema'
|
||||
isPreview?: boolean
|
||||
value?: string
|
||||
isPreview?: boolean
|
||||
previewValue?: string | null
|
||||
}
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
@@ -54,17 +55,17 @@ export function Code({
|
||||
placeholder = 'Write JavaScript...',
|
||||
language = 'javascript',
|
||||
generationType = 'javascript-function-body',
|
||||
value: propValue,
|
||||
isPreview = false,
|
||||
value: propValue
|
||||
previewValue
|
||||
}: CodeProps) {
|
||||
// Determine the AI prompt placeholder based on language
|
||||
const aiPromptPlaceholder =
|
||||
language === 'json'
|
||||
? 'Describe the JSON schema to generate...'
|
||||
: 'Describe the JavaScript code to generate...'
|
||||
const aiPromptPlaceholder = useMemo(() => {
|
||||
return language === 'json' ? 'Describe the JSON schema you need...' : 'Describe the function you need...'
|
||||
}, [language])
|
||||
|
||||
// State management
|
||||
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId, false, isPreview, propValue)
|
||||
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
|
||||
const [code, setCode] = useState<string>('')
|
||||
const [_lineCount, setLineCount] = useState(1)
|
||||
const [showTags, setShowTags] = useState(false)
|
||||
@@ -87,6 +88,8 @@ export function Code({
|
||||
const toggleCollapsed = () => {
|
||||
setCollapsedValue(blockId, collapsedStateKey, !isCollapsed)
|
||||
}
|
||||
// Use preview value when in preview mode, otherwise use store value or prop value
|
||||
const value = isPreview ? previewValue : (propValue !== undefined ? propValue : storeValue)
|
||||
|
||||
// AI Code Generation Hook
|
||||
const handleStreamStart = () => {
|
||||
@@ -97,14 +100,18 @@ export function Code({
|
||||
|
||||
const handleGeneratedContent = (generatedCode: string) => {
|
||||
setCode(generatedCode)
|
||||
setStoreValue(generatedCode)
|
||||
if (!isPreview) {
|
||||
setStoreValue(generatedCode)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle streaming chunks directly into the editor
|
||||
const handleStreamChunk = (chunk: string) => {
|
||||
setCode((currentCode) => {
|
||||
const newCode = currentCode + chunk
|
||||
setStoreValue(newCode)
|
||||
if (!isPreview) {
|
||||
setStoreValue(newCode)
|
||||
}
|
||||
return newCode
|
||||
})
|
||||
}
|
||||
@@ -130,11 +137,11 @@ export function Code({
|
||||
|
||||
// Effects
|
||||
useEffect(() => {
|
||||
const valueString = storeValue?.toString() ?? ''
|
||||
const valueString = value?.toString() ?? ''
|
||||
if (valueString !== code) {
|
||||
setCode(valueString)
|
||||
}
|
||||
}, [storeValue])
|
||||
}, [value])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorRef.current) return
|
||||
@@ -205,6 +212,7 @@ export function Code({
|
||||
|
||||
// Handlers
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
if (isPreview) return
|
||||
e.preventDefault()
|
||||
try {
|
||||
const data = JSON.parse(e.dataTransfer.getData('application/json'))
|
||||
@@ -237,8 +245,10 @@ export function Code({
|
||||
}
|
||||
|
||||
const handleTagSelect = (newValue: string) => {
|
||||
setCode(newValue)
|
||||
setStoreValue(newValue)
|
||||
if (!isPreview) {
|
||||
setCode(newValue)
|
||||
setStoreValue(newValue)
|
||||
}
|
||||
setShowTags(false)
|
||||
setActiveSourceBlockId(null)
|
||||
|
||||
@@ -248,8 +258,10 @@ export function Code({
|
||||
}
|
||||
|
||||
const handleEnvVarSelect = (newValue: string) => {
|
||||
setCode(newValue)
|
||||
setStoreValue(newValue)
|
||||
if (!isPreview) {
|
||||
setCode(newValue)
|
||||
setStoreValue(newValue)
|
||||
}
|
||||
setShowEnvVars(false)
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -313,8 +325,8 @@ export function Code({
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<div className='absolute top-2 right-3 z-10 flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100'>
|
||||
{!isCollapsed && !isAiStreaming && (
|
||||
<div className="absolute right-3 top-2 z-10 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{!isCollapsed && !isAiStreaming && !isPreview && (
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
@@ -327,7 +339,7 @@ export function Code({
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{showCollapseButton && !isAiStreaming && (
|
||||
{showCollapseButton && !isAiStreaming && !isPreview && (
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
@@ -364,7 +376,7 @@ export function Code({
|
||||
<Editor
|
||||
value={code}
|
||||
onValueChange={(newCode) => {
|
||||
if (!isCollapsed && !isAiStreaming) {
|
||||
if (!isCollapsed && !isAiStreaming && !isPreview) {
|
||||
setCode(newCode)
|
||||
setStoreValue(newCode)
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ interface ConditionInputProps {
|
||||
subBlockId: string
|
||||
isConnecting: boolean
|
||||
isPreview?: boolean
|
||||
value?: string
|
||||
previewValue?: string | null
|
||||
}
|
||||
|
||||
// Generate a stable ID based on the blockId and a suffix
|
||||
@@ -42,8 +42,14 @@ const generateStableId = (blockId: string, suffix: string): string => {
|
||||
return `${blockId}-${suffix}`
|
||||
}
|
||||
|
||||
export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview = false, value: propValue }: ConditionInputProps) {
|
||||
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId, false, isPreview, propValue)
|
||||
export function ConditionInput({
|
||||
blockId,
|
||||
subBlockId,
|
||||
isConnecting,
|
||||
isPreview = false,
|
||||
previewValue
|
||||
}: ConditionInputProps) {
|
||||
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
|
||||
const editorRef = useRef<HTMLDivElement>(null)
|
||||
const [visualLineHeights, setVisualLineHeights] = useState<{
|
||||
[key: string]: number[]
|
||||
@@ -126,15 +132,17 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
|
||||
// Skip if syncing is already in progress
|
||||
if (isSyncingFromStoreRef.current) return
|
||||
|
||||
// Convert storeValue to string if it's not null
|
||||
const storeValueStr = storeValue !== null ? storeValue.toString() : null
|
||||
// Use preview value when in preview mode, otherwise use store value
|
||||
const effectiveValue = isPreview ? previewValue : storeValue
|
||||
// Convert effectiveValue to string if it's not null
|
||||
const effectiveValueStr = effectiveValue !== null ? effectiveValue?.toString() : null
|
||||
|
||||
// Set that we're syncing from store to prevent loops
|
||||
isSyncingFromStoreRef.current = true
|
||||
|
||||
try {
|
||||
// If store value is null, and we've already initialized, keep current state
|
||||
if (storeValueStr === null) {
|
||||
// If effective value is null, and we've already initialized, keep current state
|
||||
if (effectiveValueStr === null) {
|
||||
if (hasInitializedRef.current) {
|
||||
// We already have blocks, just mark as ready if not already
|
||||
if (!isReady) setIsReady(true)
|
||||
@@ -150,18 +158,18 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
|
||||
return
|
||||
}
|
||||
|
||||
// Skip if the store value hasn't changed and we're already initialized
|
||||
if (storeValueStr === prevStoreValueRef.current && hasInitializedRef.current) {
|
||||
// Skip if the effective value hasn't changed and we're already initialized
|
||||
if (effectiveValueStr === prevStoreValueRef.current && hasInitializedRef.current) {
|
||||
if (!isReady) setIsReady(true)
|
||||
isSyncingFromStoreRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
// Update the previous store value ref
|
||||
prevStoreValueRef.current = storeValueStr
|
||||
prevStoreValueRef.current = effectiveValueStr
|
||||
|
||||
// Parse the store value
|
||||
const parsedBlocks = safeParseJSON(storeValueStr)
|
||||
// Parse the effective value
|
||||
const parsedBlocks = safeParseJSON(effectiveValueStr)
|
||||
|
||||
if (parsedBlocks) {
|
||||
// Use the parsed blocks, but ensure titles are correct based on position
|
||||
@@ -185,13 +193,13 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
|
||||
isSyncingFromStoreRef.current = false
|
||||
}, 0)
|
||||
}
|
||||
}, [storeValue, blockId, isReady])
|
||||
}, [storeValue, previewValue, isPreview, blockId, isReady])
|
||||
|
||||
// Update store whenever conditional blocks change
|
||||
useEffect(() => {
|
||||
// Skip if we're currently syncing from store to prevent loops
|
||||
// or if we're not ready yet (still initializing)
|
||||
if (isSyncingFromStoreRef.current || !isReady || conditionalBlocks.length === 0) return
|
||||
// or if we're not ready yet (still initializing) or in preview mode
|
||||
if (isSyncingFromStoreRef.current || !isReady || conditionalBlocks.length === 0 || isPreview) return
|
||||
|
||||
const newValue = JSON.stringify(conditionalBlocks)
|
||||
|
||||
@@ -201,7 +209,7 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
|
||||
setStoreValue(newValue)
|
||||
updateNodeInternals(`${blockId}-${subBlockId}`)
|
||||
}
|
||||
}, [conditionalBlocks, blockId, subBlockId, setStoreValue, updateNodeInternals, isReady])
|
||||
}, [conditionalBlocks, blockId, subBlockId, setStoreValue, updateNodeInternals, isReady, isPreview])
|
||||
|
||||
// Cleanup when component unmounts
|
||||
useEffect(() => {
|
||||
@@ -218,6 +226,8 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
|
||||
newValue: string,
|
||||
textarea: HTMLTextAreaElement | null
|
||||
) => {
|
||||
if (isPreview) return
|
||||
|
||||
try {
|
||||
setConditionalBlocks((blocks) =>
|
||||
blocks.map((block) => {
|
||||
@@ -345,6 +355,7 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
|
||||
|
||||
// Handle drops from connection blocks - updated for individual blocks
|
||||
const handleDrop = (blockId: string, e: React.DragEvent) => {
|
||||
if (isPreview) return
|
||||
e.preventDefault()
|
||||
try {
|
||||
const data = JSON.parse(e.dataTransfer.getData('application/json'))
|
||||
@@ -386,6 +397,7 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
|
||||
|
||||
// Handle tag selection - updated for individual blocks
|
||||
const handleTagSelect = (blockId: string, newValue: string) => {
|
||||
if (isPreview) return
|
||||
setConditionalBlocks((blocks) =>
|
||||
blocks.map((block) =>
|
||||
block.id === blockId
|
||||
@@ -402,6 +414,7 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
|
||||
|
||||
// Handle environment variable selection - updated for individual blocks
|
||||
const handleEnvVarSelect = (blockId: string, newValue: string) => {
|
||||
if (isPreview) return
|
||||
setConditionalBlocks((blocks) =>
|
||||
blocks.map((block) =>
|
||||
block.id === blockId
|
||||
@@ -426,6 +439,8 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
|
||||
|
||||
// Update these functions to use updateBlockTitles and stable IDs
|
||||
const addBlock = (afterId: string) => {
|
||||
if (isPreview) return
|
||||
|
||||
const blockIndex = conditionalBlocks.findIndex((block) => block.id === afterId)
|
||||
|
||||
// Generate a stable ID using the blockId and a timestamp
|
||||
@@ -458,6 +473,8 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
|
||||
}
|
||||
|
||||
const removeBlock = (id: string) => {
|
||||
if (isPreview) return
|
||||
|
||||
// Remove any associated edges before removing the block
|
||||
edges.forEach((edge) => {
|
||||
if (edge.sourceHandle?.startsWith(`condition-${id}`)) {
|
||||
@@ -470,6 +487,8 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
|
||||
}
|
||||
|
||||
const moveBlock = (id: string, direction: 'up' | 'down') => {
|
||||
if (isPreview) return
|
||||
|
||||
const blockIndex = conditionalBlocks.findIndex((block) => block.id === id)
|
||||
if (
|
||||
(direction === 'up' && blockIndex === 0) ||
|
||||
@@ -511,6 +530,9 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
|
||||
})
|
||||
}, [conditionalBlocks.length])
|
||||
|
||||
// Use preview value when in preview mode, otherwise use store value
|
||||
const value = isPreview ? previewValue : storeValue
|
||||
|
||||
// Show loading or empty state if not ready or no blocks
|
||||
if (!isReady || conditionalBlocks.length === 0) {
|
||||
return (
|
||||
@@ -574,7 +596,8 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => addBlock(block.id)}
|
||||
className='h-8 w-8'
|
||||
disabled={isPreview}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<Plus className='h-4 w-4' />
|
||||
<span className='sr-only'>Add Block</span>
|
||||
@@ -590,8 +613,8 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => moveBlock(block.id, 'up')}
|
||||
disabled={index === 0}
|
||||
className='h-8 w-8'
|
||||
disabled={isPreview || index === 0}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<ChevronUp className='h-4 w-4' />
|
||||
<span className='sr-only'>Move Up</span>
|
||||
@@ -606,8 +629,8 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => moveBlock(block.id, 'down')}
|
||||
disabled={index === conditionalBlocks.length - 1}
|
||||
className='h-8 w-8'
|
||||
disabled={isPreview || index === conditionalBlocks.length - 1}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<ChevronDown className='h-4 w-4' />
|
||||
<span className='sr-only'>Move Down</span>
|
||||
@@ -623,8 +646,8 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => removeBlock(block.id)}
|
||||
disabled={conditionalBlocks.length === 1}
|
||||
className='h-8 w-8 text-destructive hover:text-destructive'
|
||||
disabled={isPreview || conditionalBlocks.length === 1}
|
||||
className="h-8 w-8 text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash className='h-4 w-4' />
|
||||
<span className='sr-only'>Delete Block</span>
|
||||
@@ -664,10 +687,12 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
|
||||
<Editor
|
||||
value={block.value}
|
||||
onValueChange={(newCode) => {
|
||||
const textarea = editorRef.current?.querySelector(
|
||||
`[data-block-id="${block.id}"] textarea`
|
||||
)
|
||||
updateBlockValue(block.id, newCode, textarea as HTMLTextAreaElement | null)
|
||||
if (!isPreview) {
|
||||
const textarea = editorRef.current?.querySelector(
|
||||
`[data-block-id="${block.id}"] textarea`
|
||||
)
|
||||
updateBlockValue(block.id, newCode, textarea as HTMLTextAreaElement | null)
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
@@ -685,8 +710,8 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
|
||||
minHeight: '46px',
|
||||
lineHeight: '21px',
|
||||
}}
|
||||
className='focus:outline-none'
|
||||
textareaClassName='focus:outline-none focus:ring-0 bg-transparent'
|
||||
className={cn('focus:outline-none', isPreview && 'opacity-50 cursor-not-allowed')}
|
||||
textareaClassName={cn('focus:outline-none focus:ring-0 bg-transparent', isPreview && 'pointer-events-none')}
|
||||
/>
|
||||
|
||||
{block.showEnvVars && (
|
||||
|
||||
@@ -16,11 +16,20 @@ interface DateInputProps {
|
||||
subBlockId: string
|
||||
placeholder?: string
|
||||
isPreview?: boolean
|
||||
value?: string
|
||||
previewValue?: string | null
|
||||
}
|
||||
|
||||
export function DateInput({ blockId, subBlockId, placeholder, isPreview = false, value: propValue }: DateInputProps) {
|
||||
const [value, setValue] = useSubBlockValue<string>(blockId, subBlockId, true, isPreview, propValue)
|
||||
export function DateInput({
|
||||
blockId,
|
||||
subBlockId,
|
||||
placeholder,
|
||||
isPreview = false,
|
||||
previewValue
|
||||
}: DateInputProps) {
|
||||
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
|
||||
|
||||
// Use preview value when in preview mode, otherwise use store value
|
||||
const value = isPreview ? previewValue : storeValue
|
||||
const addNotification = useNotificationStore((state) => state.addNotification)
|
||||
const date = value ? new Date(value) : undefined
|
||||
|
||||
@@ -32,6 +41,8 @@ export function DateInput({ blockId, subBlockId, placeholder, isPreview = false,
|
||||
}, [date])
|
||||
|
||||
const handleDateSelect = (selectedDate: Date | undefined) => {
|
||||
if (isPreview) return
|
||||
|
||||
if (selectedDate) {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
@@ -40,14 +51,15 @@ export function DateInput({ blockId, subBlockId, placeholder, isPreview = false,
|
||||
addNotification('error', 'Cannot start at a date in the past', blockId)
|
||||
}
|
||||
}
|
||||
setValue(selectedDate?.toISOString() || '')
|
||||
setStoreValue(selectedDate?.toISOString() || '')
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
variant="outline"
|
||||
disabled={isPreview}
|
||||
className={cn(
|
||||
'w-full justify-start text-left font-normal',
|
||||
!date && 'text-muted-foreground',
|
||||
|
||||
@@ -9,8 +9,6 @@ import {
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { useSubBlockValue } from '../hooks/use-sub-block-value'
|
||||
|
||||
const logger = createLogger('Dropdown')
|
||||
|
||||
interface DropdownProps {
|
||||
options:
|
||||
| Array<string | { label: string; id: string }>
|
||||
@@ -18,8 +16,9 @@ interface DropdownProps {
|
||||
defaultValue?: string
|
||||
blockId: string
|
||||
subBlockId: string
|
||||
isPreview?: boolean
|
||||
value?: string
|
||||
isPreview?: boolean
|
||||
previewValue?: string | null
|
||||
}
|
||||
|
||||
export function Dropdown({
|
||||
@@ -27,12 +26,16 @@ export function Dropdown({
|
||||
defaultValue,
|
||||
blockId,
|
||||
subBlockId,
|
||||
value: propValue,
|
||||
isPreview = false,
|
||||
value: propValue
|
||||
previewValue
|
||||
}: DropdownProps) {
|
||||
const [value, setValue] = useSubBlockValue<string>(blockId, subBlockId, true, isPreview, propValue)
|
||||
const [storeValue, setStoreValue] = useSubBlockValue<string>(blockId, subBlockId)
|
||||
const [storeInitialized, setStoreInitialized] = useState(false)
|
||||
|
||||
// Use preview value when in preview mode, otherwise use store value or prop value
|
||||
const value = isPreview ? previewValue : (propValue !== undefined ? propValue : storeValue)
|
||||
|
||||
// Evaluate options if it's a function
|
||||
const evaluatedOptions = useMemo(() => {
|
||||
return typeof options === 'function' ? options() : options
|
||||
@@ -72,9 +75,9 @@ export function Dropdown({
|
||||
(value === null || value === undefined) &&
|
||||
defaultOptionValue !== undefined
|
||||
) {
|
||||
setValue(defaultOptionValue)
|
||||
setStoreValue(defaultOptionValue)
|
||||
}
|
||||
}, [storeInitialized, value, defaultOptionValue, setValue])
|
||||
}, [storeInitialized, value, defaultOptionValue, setStoreValue])
|
||||
|
||||
// Calculate the effective value to use in the dropdown
|
||||
const effectiveValue = useMemo(() => {
|
||||
@@ -101,7 +104,13 @@ export function Dropdown({
|
||||
return (
|
||||
<Select
|
||||
value={isValueInOptions ? effectiveValue : undefined}
|
||||
onValueChange={(newValue) => setValue(newValue)}
|
||||
onValueChange={(newValue) => {
|
||||
// Only update store when not in preview mode
|
||||
if (!isPreview) {
|
||||
setStoreValue(newValue)
|
||||
}
|
||||
}}
|
||||
disabled={isPreview}
|
||||
>
|
||||
<SelectTrigger className='text-left'>
|
||||
<SelectValue placeholder='Select an option' />
|
||||
|
||||
@@ -20,7 +20,7 @@ interface EvalInputProps {
|
||||
blockId: string
|
||||
subBlockId: string
|
||||
isPreview?: boolean
|
||||
value?: EvalMetric[]
|
||||
previewValue?: EvalMetric[] | null
|
||||
}
|
||||
|
||||
// Default values
|
||||
@@ -31,56 +31,52 @@ const DEFAULT_METRIC: EvalMetric = {
|
||||
range: { min: 0, max: 1 },
|
||||
}
|
||||
|
||||
export function EvalInput({ blockId, subBlockId, isPreview = false, value: propValue }: EvalInputProps) {
|
||||
export function EvalInput({
|
||||
blockId,
|
||||
subBlockId,
|
||||
isPreview = false,
|
||||
previewValue
|
||||
}: EvalInputProps) {
|
||||
const [storeValue, setStoreValue] = useSubBlockValue<EvalMetric[]>(blockId, subBlockId)
|
||||
|
||||
// Use preview value when in preview mode, otherwise use store value
|
||||
const value = isPreview ? previewValue : storeValue
|
||||
|
||||
// State hooks
|
||||
const [value, setValue] = useSubBlockValue<EvalMetric[]>(blockId, subBlockId, false, isPreview, propValue)
|
||||
const metrics = value || [DEFAULT_METRIC]
|
||||
const metrics: EvalMetric[] = value || [DEFAULT_METRIC]
|
||||
|
||||
// Metric operations
|
||||
const addMetric = () => {
|
||||
if (isPreview) return
|
||||
|
||||
const newMetric: EvalMetric = {
|
||||
...DEFAULT_METRIC,
|
||||
id: crypto.randomUUID(),
|
||||
}
|
||||
setValue([...metrics, newMetric])
|
||||
setStoreValue([...metrics, newMetric])
|
||||
}
|
||||
|
||||
const removeMetric = (id: string) => {
|
||||
if (metrics.length === 1) return
|
||||
setValue(metrics.filter((metric) => metric.id !== id))
|
||||
if (isPreview || metrics.length === 1) return
|
||||
setStoreValue(metrics.filter((metric) => metric.id !== id))
|
||||
}
|
||||
|
||||
// Update handlers
|
||||
const updateMetric = (id: string, field: keyof EvalMetric, value: any) => {
|
||||
setValue(metrics.map((metric) => (metric.id === id ? { ...metric, [field]: value } : metric)))
|
||||
if (isPreview) return
|
||||
setStoreValue(metrics.map((metric) => (metric.id === id ? { ...metric, [field]: value } : metric)))
|
||||
}
|
||||
|
||||
const updateRange = (id: string, field: 'min' | 'max', value: string) => {
|
||||
setValue(
|
||||
metrics.map((metric) =>
|
||||
metric.id === id
|
||||
? {
|
||||
...metric,
|
||||
range: { ...metric.range, [field]: value },
|
||||
}
|
||||
: metric
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Validation handlers
|
||||
const handleRangeBlur = (id: string, field: 'min' | 'max', value: string) => {
|
||||
const sanitizedValue = value.replace(/[^\d.-]/g, '')
|
||||
const numValue = Number.parseFloat(sanitizedValue)
|
||||
|
||||
setValue(
|
||||
if (isPreview) return
|
||||
setStoreValue(
|
||||
metrics.map((metric) =>
|
||||
metric.id === id
|
||||
? {
|
||||
...metric,
|
||||
range: {
|
||||
...metric.range,
|
||||
[field]: !Number.isNaN(numValue) ? numValue : 0,
|
||||
[field]: value === '' ? undefined : parseInt(value, 10),
|
||||
},
|
||||
}
|
||||
: metric
|
||||
@@ -88,6 +84,29 @@ export function EvalInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
)
|
||||
}
|
||||
|
||||
const updateThreshold = (id: string, value: string) => {
|
||||
if (isPreview) return
|
||||
|
||||
// Allow empty values for clearing
|
||||
const sanitizedValue = value.replace(/[^0-9.-]/g, '')
|
||||
if (sanitizedValue === '') {
|
||||
setStoreValue(
|
||||
metrics.map((metric) =>
|
||||
metric.id === id ? { ...metric, threshold: undefined } : metric
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const numValue = parseFloat(sanitizedValue)
|
||||
|
||||
setStoreValue(
|
||||
metrics.map((metric) =>
|
||||
metric.id === id ? { ...metric, threshold: isNaN(numValue) ? undefined : numValue } : metric
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Metric header
|
||||
const renderMetricHeader = (metric: EvalMetric, index: number) => (
|
||||
<div className='flex h-10 items-center justify-between rounded-t-lg border-b bg-card px-3'>
|
||||
@@ -95,9 +114,9 @@ export function EvalInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
<div className='flex items-center gap-1'>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant='ghost' size='sm' onClick={addMetric} className='h-8 w-8'>
|
||||
<Plus className='h-4 w-4' />
|
||||
<span className='sr-only'>Add Metric</span>
|
||||
<Button variant="ghost" size="sm" onClick={addMetric} disabled={isPreview} className="h-8 w-8">
|
||||
<Plus className="h-4 w-4" />
|
||||
<span className="sr-only">Add Metric</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Add Metric</TooltipContent>
|
||||
@@ -109,8 +128,8 @@ export function EvalInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => removeMetric(metric.id)}
|
||||
disabled={metrics.length === 1}
|
||||
className='h-8 w-8 text-destructive hover:text-destructive'
|
||||
disabled={isPreview || metrics.length === 1}
|
||||
className="h-8 w-8 text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash className='h-4 w-4' />
|
||||
<span className='sr-only'>Delete Metric</span>
|
||||
@@ -140,8 +159,9 @@ export function EvalInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
name='name'
|
||||
value={metric.name}
|
||||
onChange={(e) => updateMetric(metric.id, 'name', e.target.value)}
|
||||
placeholder='Accuracy'
|
||||
className='placeholder:text-muted-foreground/50'
|
||||
placeholder="Accuracy"
|
||||
disabled={isPreview}
|
||||
className="placeholder:text-muted-foreground/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -150,8 +170,9 @@ export function EvalInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
<Input
|
||||
value={metric.description}
|
||||
onChange={(e) => updateMetric(metric.id, 'description', e.target.value)}
|
||||
placeholder='How accurate is the response?'
|
||||
className='placeholder:text-muted-foreground/50'
|
||||
placeholder="How accurate is the response?"
|
||||
disabled={isPreview}
|
||||
className="placeholder:text-muted-foreground/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -162,8 +183,9 @@ export function EvalInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
type='text'
|
||||
value={metric.range.min}
|
||||
onChange={(e) => updateRange(metric.id, 'min', e.target.value)}
|
||||
onBlur={(e) => handleRangeBlur(metric.id, 'min', e.target.value)}
|
||||
className='placeholder:text-muted-foreground/50'
|
||||
onBlur={(e) => updateThreshold(metric.id, e.target.value)}
|
||||
disabled={isPreview}
|
||||
className="placeholder:text-muted-foreground/50"
|
||||
/>
|
||||
</div>
|
||||
<div className='space-y-1'>
|
||||
@@ -172,8 +194,9 @@ export function EvalInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
type='text'
|
||||
value={metric.range.max}
|
||||
onChange={(e) => updateRange(metric.id, 'max', e.target.value)}
|
||||
onBlur={(e) => handleRangeBlur(metric.id, 'max', e.target.value)}
|
||||
className='placeholder:text-muted-foreground/50'
|
||||
onBlur={(e) => updateThreshold(metric.id, e.target.value)}
|
||||
disabled={isPreview}
|
||||
className="placeholder:text-muted-foreground/50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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<UploadedFile | UploadedFile[] | null>(
|
||||
blockId,
|
||||
subBlockId,
|
||||
true,
|
||||
isPreview,
|
||||
propValue
|
||||
)
|
||||
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
|
||||
const [uploadingFiles, setUploadingFiles] = useState<UploadingFile[]>([])
|
||||
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<HTMLInputElement>) => {
|
||||
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()
|
||||
|
||||
|
||||
@@ -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<string>('')
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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<string | null>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(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<HTMLTextAreaElement>) => {
|
||||
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({
|
||||
<ChevronsUpDown className='h-3 w-3 text-muted-foreground/70' />
|
||||
</div>
|
||||
|
||||
<EnvVarDropdown
|
||||
visible={showEnvVars}
|
||||
onSelect={setValue}
|
||||
searchTerm={searchTerm}
|
||||
inputValue={value?.toString() ?? ''}
|
||||
cursorPosition={cursorPosition}
|
||||
onClose={() => {
|
||||
setShowEnvVars(false)
|
||||
setSearchTerm('')
|
||||
}}
|
||||
/>
|
||||
<TagDropdown
|
||||
visible={showTags}
|
||||
onSelect={setValue}
|
||||
blockId={blockId}
|
||||
activeSourceBlockId={activeSourceBlockId}
|
||||
inputValue={value?.toString() ?? ''}
|
||||
cursorPosition={cursorPosition}
|
||||
onClose={() => {
|
||||
setShowTags(false)
|
||||
setActiveSourceBlockId(null)
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<EnvVarDropdown
|
||||
visible={showEnvVars}
|
||||
onSelect={(newValue) => {
|
||||
if (onChange) {
|
||||
onChange(newValue)
|
||||
} else if (!isPreview) {
|
||||
setStoreValue(newValue)
|
||||
}
|
||||
}}
|
||||
searchTerm={searchTerm}
|
||||
inputValue={value?.toString() ?? ''}
|
||||
cursorPosition={cursorPosition}
|
||||
onClose={() => {
|
||||
setShowEnvVars(false)
|
||||
setSearchTerm('')
|
||||
}}
|
||||
/>
|
||||
<TagDropdown
|
||||
visible={showTags}
|
||||
onSelect={(newValue) => {
|
||||
if (onChange) {
|
||||
onChange(newValue)
|
||||
} else if (!isPreview) {
|
||||
setStoreValue(newValue)
|
||||
}
|
||||
}}
|
||||
blockId={blockId}
|
||||
activeSourceBlockId={activeSourceBlockId}
|
||||
inputValue={value?.toString() ?? ''}
|
||||
cursorPosition={cursorPosition}
|
||||
onClose={() => {
|
||||
setShowTags(false)
|
||||
setActiveSourceBlockId(null)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<string>('')
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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<string | null>(null)
|
||||
const [scheduleId, setScheduleId] = useState<string | null>(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<boolean> => {
|
||||
if (isPreview) return false
|
||||
|
||||
setIsSaving(true)
|
||||
setError(null)
|
||||
|
||||
@@ -247,7 +255,7 @@ export function ScheduleConfig({
|
||||
}
|
||||
|
||||
const handleDeleteSchedule = async (): Promise<boolean> => {
|
||||
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 ? (
|
||||
<div className='h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
|
||||
@@ -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 ? (
|
||||
<div className='mr-2 h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
<div
|
||||
ref={overlayRef}
|
||||
|
||||
@@ -4,76 +4,69 @@ import { useSubBlockValue } from '../hooks/use-sub-block-value'
|
||||
|
||||
|
||||
interface SliderInputProps {
|
||||
min?: number
|
||||
max?: number
|
||||
defaultValue: number
|
||||
blockId: string
|
||||
subBlockId: string
|
||||
min?: number
|
||||
max?: number
|
||||
defaultValue?: number
|
||||
step?: number
|
||||
integer?: boolean
|
||||
isPreview?: boolean
|
||||
value?: number
|
||||
previewValue?: number | null
|
||||
}
|
||||
|
||||
export function SliderInput({
|
||||
min = 0,
|
||||
max = 100,
|
||||
defaultValue,
|
||||
blockId,
|
||||
subBlockId,
|
||||
step = 0.1,
|
||||
min = 0,
|
||||
max = 100,
|
||||
defaultValue = 50,
|
||||
step = 1,
|
||||
integer = false,
|
||||
isPreview = false,
|
||||
value: propValue
|
||||
previewValue
|
||||
}: SliderInputProps) {
|
||||
const [value, setValue] = useSubBlockValue<number>(blockId, subBlockId, false, isPreview, propValue)
|
||||
const [storeValue, setStoreValue] = useSubBlockValue<number>(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 (
|
||||
<div className='relative pt-2 pb-6'>
|
||||
<Slider
|
||||
value={[normalizedValue]}
|
||||
min={min}
|
||||
max={max}
|
||||
step={integer ? 1 : step}
|
||||
onValueChange={(value) => setValue(integer ? Math.round(value[0]) : value[0])}
|
||||
className='[&_[class*=SliderTrack]]:h-1 [&_[role=slider]]:h-4 [&_[role=slider]]:w-4'
|
||||
/>
|
||||
<div
|
||||
className='absolute text-muted-foreground text-sm'
|
||||
style={{
|
||||
left: `clamp(0%, ${((normalizedValue - min) / (max - min)) * 100}%, 100%)`,
|
||||
transform: `translateX(-${(() => {
|
||||
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)}
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex-1">
|
||||
<Slider
|
||||
value={[displayValue]}
|
||||
min={min}
|
||||
max={max}
|
||||
step={integer ? 1 : step}
|
||||
onValueChange={handleValueChange}
|
||||
disabled={isPreview}
|
||||
className="[&_[role=slider]]:h-4 [&_[role=slider]]:w-4 [&_[class*=SliderTrack]]:h-1"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm font-medium min-w-[3rem] text-right">
|
||||
{displayValue}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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<InputField[]>(blockId, subBlockId, false, isPreview, propValue)
|
||||
const fields = value || [DEFAULT_FIELD]
|
||||
export function InputFormat({
|
||||
blockId,
|
||||
subBlockId,
|
||||
isPreview = false,
|
||||
previewValue
|
||||
}: InputFormatProps) {
|
||||
const [storeValue, setStoreValue] = useSubBlockValue<InputField[]>(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
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex items-center gap-1' onClick={(e) => e.stopPropagation()}>
|
||||
<Button variant='ghost' size='icon' onClick={addField} className='h-6 w-6 rounded-full'>
|
||||
<Plus className='h-3.5 w-3.5' />
|
||||
<span className='sr-only'>Add Field</span>
|
||||
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
||||
<Button variant="ghost" size="icon" onClick={addField} disabled={isPreview} className="h-6 w-6 rounded-full">
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
<span className="sr-only">Add Field</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={() => removeField(field.id)}
|
||||
disabled={fields.length === 1}
|
||||
className='h-6 w-6 rounded-full text-destructive hover:text-destructive'
|
||||
disabled={isPreview || fields.length === 1}
|
||||
className="h-6 w-6 rounded-full text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash className='h-3.5 w-3.5' />
|
||||
<span className='sr-only'>Delete Field</span>
|
||||
@@ -137,8 +148,9 @@ export function InputFormat({ blockId, subBlockId, isPreview = false, value: pro
|
||||
name='name'
|
||||
value={field.name}
|
||||
onChange={(e) => updateField(field.id, 'name', e.target.value)}
|
||||
placeholder='firstName'
|
||||
className='h-9 placeholder:text-muted-foreground/50'
|
||||
placeholder="firstName"
|
||||
disabled={isPreview}
|
||||
className="h-9 placeholder:text-muted-foreground/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -146,8 +158,8 @@ export function InputFormat({ blockId, subBlockId, isPreview = false, value: pro
|
||||
<Label className='text-xs'>Type</Label>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant='outline' className='h-9 w-full justify-between font-normal'>
|
||||
<div className='flex items-center'>
|
||||
<Button variant="outline" disabled={isPreview} className="w-full justify-between h-9 font-normal">
|
||||
<div className="flex items-center">
|
||||
<span>{field.type}</span>
|
||||
</div>
|
||||
<ChevronDown className='h-4 w-4 opacity-50' />
|
||||
|
||||
@@ -3,30 +3,49 @@ import { Label } from '@/components/ui/label'
|
||||
import { Switch as UISwitch } from '@/components/ui/switch'
|
||||
import { useSubBlockValue } from '../hooks/use-sub-block-value'
|
||||
|
||||
|
||||
interface SwitchProps {
|
||||
blockId: string
|
||||
subBlockId: string
|
||||
title: string
|
||||
isPreview?: boolean
|
||||
value?: boolean
|
||||
isPreview?: boolean
|
||||
previewValue?: boolean | null
|
||||
}
|
||||
|
||||
export function Switch({
|
||||
blockId,
|
||||
subBlockId,
|
||||
title,
|
||||
value: propValue,
|
||||
isPreview = false,
|
||||
value: propValue
|
||||
previewValue
|
||||
}: SwitchProps) {
|
||||
const [value, setValue] = useSubBlockValue(blockId, subBlockId, false, isPreview, propValue)
|
||||
const [storeValue, setStoreValue] = useSubBlockValue<boolean>(blockId, subBlockId)
|
||||
|
||||
// Use preview value when in preview mode, otherwise use store value or prop value
|
||||
const value = isPreview ? previewValue : (propValue !== undefined ? propValue : storeValue)
|
||||
|
||||
const handleChange = (checked: boolean) => {
|
||||
// Only update store when not in preview mode
|
||||
if (!isPreview) {
|
||||
setStoreValue(checked)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-2'>
|
||||
<Label className='font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'>
|
||||
<div className="flex items-center space-x-3">
|
||||
<UISwitch
|
||||
id={`${blockId}-${subBlockId}`}
|
||||
checked={Boolean(value)}
|
||||
onCheckedChange={handleChange}
|
||||
disabled={isPreview}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`${blockId}-${subBlockId}`}
|
||||
className="text-sm font-normal leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
|
||||
>
|
||||
{title}
|
||||
</Label>
|
||||
<UISwitch checked={Boolean(value)} onCheckedChange={(checked) => setValue(checked)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ import { useSubBlockValue } from '../hooks/use-sub-block-value'
|
||||
|
||||
|
||||
interface TableProps {
|
||||
columns: string[]
|
||||
blockId: string
|
||||
subBlockId: string
|
||||
columns: string[]
|
||||
isPreview?: boolean
|
||||
value?: TableRow[]
|
||||
previewValue?: TableRow[] | null
|
||||
}
|
||||
|
||||
interface TableRow {
|
||||
@@ -22,8 +22,17 @@ interface TableRow {
|
||||
cells: Record<string, string>
|
||||
}
|
||||
|
||||
export function Table({ columns, blockId, subBlockId, isPreview = false, value: propValue }: TableProps) {
|
||||
const [value, setValue] = useSubBlockValue(blockId, subBlockId, false, isPreview, propValue)
|
||||
export function Table({
|
||||
blockId,
|
||||
subBlockId,
|
||||
columns,
|
||||
isPreview = false,
|
||||
previewValue
|
||||
}: TableProps) {
|
||||
const [storeValue, setStoreValue] = useSubBlockValue<TableRow[]>(blockId, subBlockId)
|
||||
|
||||
// Use preview value when in preview mode, otherwise use store value
|
||||
const value = isPreview ? previewValue : storeValue
|
||||
|
||||
// Create refs for input elements
|
||||
const inputRefs = useRef<Map<string, HTMLInputElement>>(new Map())
|
||||
@@ -74,6 +83,8 @@ export function Table({ columns, blockId, subBlockId, isPreview = false, value:
|
||||
}, [activeCell])
|
||||
|
||||
const handleCellChange = (rowIndex: number, column: string, value: string) => {
|
||||
if (isPreview) return
|
||||
|
||||
const updatedRows = [...rows].map((row, idx) =>
|
||||
idx === rowIndex
|
||||
? {
|
||||
@@ -90,12 +101,12 @@ export function Table({ columns, blockId, subBlockId, isPreview = false, value:
|
||||
})
|
||||
}
|
||||
|
||||
setValue(updatedRows)
|
||||
setStoreValue(updatedRows)
|
||||
}
|
||||
|
||||
const handleDeleteRow = (rowIndex: number) => {
|
||||
if (rows.length === 1) return
|
||||
setValue(rows.filter((_, index) => index !== rowIndex))
|
||||
if (isPreview || rows.length === 1) return
|
||||
setStoreValue(rows.filter((_, index) => index !== rowIndex))
|
||||
}
|
||||
|
||||
const renderHeader = () => (
|
||||
@@ -175,7 +186,8 @@ export function Table({ columns, blockId, subBlockId, isPreview = false, value:
|
||||
setActiveCell(null)
|
||||
}
|
||||
}}
|
||||
className='w-full border-0 text-transparent caret-foreground placeholder:text-muted-foreground/50 focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
disabled={isPreview}
|
||||
className="border-0 focus-visible:ring-0 focus-visible:ring-offset-0 text-transparent caret-foreground placeholder:text-muted-foreground/50 w-full"
|
||||
/>
|
||||
<div
|
||||
data-overlay={cellKey}
|
||||
@@ -189,8 +201,8 @@ export function Table({ columns, blockId, subBlockId, isPreview = false, value:
|
||||
}
|
||||
|
||||
const renderDeleteButton = (rowIndex: number) =>
|
||||
rows.length > 1 && (
|
||||
<td className='w-0 p-0'>
|
||||
rows.length > 1 && !isPreview && (
|
||||
<td className="w-0 p-0">
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
|
||||
@@ -13,20 +13,23 @@ interface TimeInputProps {
|
||||
blockId: string
|
||||
subBlockId: string
|
||||
placeholder?: string
|
||||
className?: string
|
||||
isPreview?: boolean
|
||||
value?: string
|
||||
previewValue?: string | null
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function TimeInput({
|
||||
blockId,
|
||||
subBlockId,
|
||||
placeholder,
|
||||
className,
|
||||
placeholder,
|
||||
isPreview = false,
|
||||
value: propValue
|
||||
previewValue,
|
||||
className,
|
||||
}: TimeInputProps) {
|
||||
const [value, setValue] = useSubBlockValue<string>(blockId, subBlockId, true, isPreview, propValue)
|
||||
const [storeValue, setStoreValue] = useSubBlockValue<string>(blockId, subBlockId)
|
||||
|
||||
// Use preview value when in preview mode, otherwise use store value
|
||||
const value = isPreview ? previewValue : storeValue
|
||||
const [isOpen, setIsOpen] = React.useState(false)
|
||||
|
||||
// Convert 24h time string to display format (12h with AM/PM)
|
||||
@@ -51,10 +54,11 @@ export function TimeInput({
|
||||
|
||||
// Update the time when any component changes
|
||||
const updateTime = (newHour?: string, newMinute?: string, newAmpm?: 'AM' | 'PM') => {
|
||||
const h = Number.parseInt(newHour ?? hour) || 12
|
||||
const m = Number.parseInt(newMinute ?? minute) || 0
|
||||
if (isPreview) return
|
||||
const h = parseInt(newHour ?? hour) || 12
|
||||
const m = parseInt(newMinute ?? minute) || 0
|
||||
const p = newAmpm ?? ampm
|
||||
setValue(formatStorageTime(h, m, p))
|
||||
setStoreValue(formatStorageTime(h, m, p))
|
||||
}
|
||||
|
||||
// Initialize from existing value
|
||||
@@ -87,7 +91,8 @@ export function TimeInput({
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
variant="outline"
|
||||
disabled={isPreview}
|
||||
className={cn(
|
||||
'w-full justify-start text-left font-normal',
|
||||
!value && 'text-muted-foreground',
|
||||
|
||||
@@ -27,12 +27,14 @@ import { CredentialSelector } from '../credential-selector/credential-selector'
|
||||
import { ShortInput } from '../short-input'
|
||||
import { type CustomTool, CustomToolModal } from './components/custom-tool-modal/custom-tool-modal'
|
||||
import { ToolCommand } from './components/tool-command/tool-command'
|
||||
import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandSeparator } from '@/components/ui/command'
|
||||
import { Plus } from 'lucide-react'
|
||||
|
||||
interface ToolInputProps {
|
||||
blockId: string
|
||||
subBlockId: string
|
||||
isPreview?: boolean
|
||||
value?: StoredTool[]
|
||||
previewValue?: any
|
||||
}
|
||||
|
||||
interface StoredTool {
|
||||
@@ -272,8 +274,13 @@ const shouldBePasswordField = (blockType: string, paramId: string): boolean => {
|
||||
return false
|
||||
}
|
||||
|
||||
export function ToolInput({ blockId, subBlockId, isPreview = false, value: propValue }: ToolInputProps) {
|
||||
const [value, setValue] = useSubBlockValue(blockId, subBlockId, false, isPreview, propValue)
|
||||
export function ToolInput({
|
||||
blockId,
|
||||
subBlockId,
|
||||
isPreview = false,
|
||||
previewValue
|
||||
}: ToolInputProps) {
|
||||
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [customToolModalOpen, setCustomToolModalOpen] = useState(false)
|
||||
const [editingToolIndex, setEditingToolIndex] = useState<number | null>(null)
|
||||
@@ -291,6 +298,9 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
|
||||
const toolBlocks = getAllBlocks().filter((block) => block.category === 'tools')
|
||||
|
||||
// Use preview value when in preview mode, otherwise use store value
|
||||
const value = isPreview ? previewValue : storeValue
|
||||
|
||||
// Custom filter function for the Command component
|
||||
const customFilter = useCallback((value: string, search: string) => {
|
||||
if (!search.trim()) return 1
|
||||
@@ -316,6 +326,11 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
? (value as unknown as StoredTool[])
|
||||
: []
|
||||
|
||||
// Check if a tool is already selected
|
||||
const isToolAlreadySelected = (toolType: string) => {
|
||||
return selectedTools.some((tool) => tool.type === toolType)
|
||||
}
|
||||
|
||||
const handleSelectTool = (toolBlock: (typeof toolBlocks)[0]) => {
|
||||
const hasOperations = hasMultipleOperations(toolBlock.type)
|
||||
const operationOptions = hasOperations ? getOperationOptions(toolBlock.type) : []
|
||||
@@ -344,7 +359,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
|
||||
// If isWide, keep tools in the same row expanded
|
||||
if (isWide) {
|
||||
setValue([
|
||||
setStoreValue([
|
||||
...selectedTools.map((tool, index) => ({
|
||||
...tool,
|
||||
// Keep expanded if it's in the same row as the new tool
|
||||
@@ -354,7 +369,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
])
|
||||
} else {
|
||||
// Original behavior for non-wide mode
|
||||
setValue([...selectedTools.map((tool) => ({ ...tool, isExpanded: false })), newTool])
|
||||
setStoreValue([...selectedTools.map((tool) => ({ ...tool, isExpanded: false })), newTool])
|
||||
}
|
||||
|
||||
setOpen(false)
|
||||
@@ -399,7 +414,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
|
||||
// If isWide, keep tools in the same row expanded
|
||||
if (isWide) {
|
||||
setValue([
|
||||
setStoreValue([
|
||||
...selectedTools.map((tool, index) => ({
|
||||
...tool,
|
||||
// Keep expanded if it's in the same row as the new tool
|
||||
@@ -409,7 +424,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
])
|
||||
} else {
|
||||
// Original behavior for non-wide mode
|
||||
setValue([...selectedTools.map((tool) => ({ ...tool, isExpanded: false })), newTool])
|
||||
setStoreValue([...selectedTools.map((tool) => ({ ...tool, isExpanded: false })), newTool])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,7 +445,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
const handleSaveCustomTool = (customTool: CustomTool) => {
|
||||
if (editingToolIndex !== null) {
|
||||
// Update existing tool
|
||||
setValue(
|
||||
setStoreValue(
|
||||
selectedTools.map((tool, index) =>
|
||||
index === editingToolIndex
|
||||
? {
|
||||
@@ -450,7 +465,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
}
|
||||
|
||||
const handleRemoveTool = (toolType: string, toolIndex: number) => {
|
||||
setValue(selectedTools.filter((_, index) => index !== toolIndex))
|
||||
setStoreValue(selectedTools.filter((_, index) => index !== toolIndex))
|
||||
}
|
||||
|
||||
// New handler for when a custom tool is completely deleted from the store
|
||||
@@ -474,7 +489,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
|
||||
// Update the workflow value if any tools were removed
|
||||
if (updatedTools.length !== selectedTools.length) {
|
||||
setValue(updatedTools)
|
||||
setStoreValue(updatedTools)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,7 +507,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
}
|
||||
|
||||
// Update the value in the workflow
|
||||
setValue(
|
||||
setStoreValue(
|
||||
selectedTools.map((tool, index) =>
|
||||
index === toolIndex
|
||||
? {
|
||||
@@ -521,7 +536,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
subBlockStore.setValue(blockId, 'parentIssue', '')
|
||||
}
|
||||
|
||||
setValue(
|
||||
setStoreValue(
|
||||
selectedTools.map((tool, index) =>
|
||||
index === toolIndex
|
||||
? {
|
||||
@@ -536,7 +551,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
}
|
||||
|
||||
const handleCredentialChange = (toolIndex: number, credentialId: string) => {
|
||||
setValue(
|
||||
setStoreValue(
|
||||
selectedTools.map((tool, index) =>
|
||||
index === toolIndex
|
||||
? {
|
||||
@@ -552,7 +567,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
}
|
||||
|
||||
const handleUsageControlChange = (toolIndex: number, usageControl: string) => {
|
||||
setValue(
|
||||
setStoreValue(
|
||||
selectedTools.map((tool, index) =>
|
||||
index === toolIndex
|
||||
? {
|
||||
@@ -565,7 +580,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
}
|
||||
|
||||
const toggleToolExpansion = (toolIndex: number) => {
|
||||
setValue(
|
||||
setStoreValue(
|
||||
selectedTools.map((tool, index) =>
|
||||
index === toolIndex ? { ...tool, isExpanded: !tool.isExpanded } : tool
|
||||
)
|
||||
@@ -589,112 +604,50 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
</div>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-[200px] p-0' align='start'>
|
||||
<ToolCommand.Root filter={customFilter}>
|
||||
<ToolCommand.Input placeholder='Search tools...' onValueChange={setSearchQuery} />
|
||||
<ToolCommand.List>
|
||||
<ToolCommand.Empty>No tools found</ToolCommand.Empty>
|
||||
<ToolCommand.Group>
|
||||
<ToolCommand.Item
|
||||
value='Create Tool'
|
||||
<PopoverContent className="p-0 w-[200px]" align="start">
|
||||
<Command filter={customFilter} className="max-h-48" shouldFilter={false}>
|
||||
<CommandInput placeholder="Search for tools..." disabled={isPreview} />
|
||||
<CommandList>
|
||||
<CommandEmpty>No tools found.</CommandEmpty>
|
||||
{toolBlocks.map((block) => (
|
||||
<CommandGroup key={block.type} heading={block.name}>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
if (!isPreview) {
|
||||
handleSelectTool(block)
|
||||
setOpen(false)
|
||||
}
|
||||
}}
|
||||
disabled={isPreview || isToolAlreadySelected(block.type)}
|
||||
>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<IconComponent icon={block.icon} className="h-4 w-4" />
|
||||
<span>{block.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
))}
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading="Custom Tools">
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
setOpen(false)
|
||||
setCustomToolModalOpen(true)
|
||||
if (!isPreview) {
|
||||
setCustomToolModalOpen(true)
|
||||
setOpen(false)
|
||||
}
|
||||
}}
|
||||
className='mb-1 flex cursor-pointer items-center gap-2'
|
||||
disabled={isPreview}
|
||||
>
|
||||
<div className='flex h-6 w-6 items-center justify-center rounded border border-muted-foreground/50 border-dashed bg-transparent'>
|
||||
<WrenchIcon className='h-4 w-4 text-muted-foreground' />
|
||||
<div className="flex items-center gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Create Custom Tool</span>
|
||||
</div>
|
||||
<span>Create Tool</span>
|
||||
</ToolCommand.Item>
|
||||
|
||||
{/* Display saved custom tools at the top */}
|
||||
{customTools.length > 0 && (
|
||||
<>
|
||||
<ToolCommand.Separator />
|
||||
<div className='px-2 pt-2.5 pb-0.5 font-medium text-muted-foreground text-xs'>
|
||||
Custom Tools
|
||||
</div>
|
||||
<ToolCommand.Group className='-mx-1 -px-1'>
|
||||
{customTools.map((customTool) => (
|
||||
<ToolCommand.Item
|
||||
key={customTool.id}
|
||||
value={customTool.title}
|
||||
onSelect={() => {
|
||||
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'
|
||||
>
|
||||
<div className='flex h-6 w-6 items-center justify-center rounded bg-blue-500'>
|
||||
<WrenchIcon className='h-4 w-4 text-white' />
|
||||
</div>
|
||||
<span className='max-w-[140px] truncate'>{customTool.title}</span>
|
||||
</ToolCommand.Item>
|
||||
))}
|
||||
</ToolCommand.Group>
|
||||
<ToolCommand.Separator />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Display built-in tools */}
|
||||
{toolBlocks.some((block) => customFilter(block.name, searchQuery || '') > 0) && (
|
||||
<>
|
||||
<div className='px-2 pt-2.5 pb-0.5 font-medium text-muted-foreground text-xs'>
|
||||
Built-in Tools
|
||||
</div>
|
||||
<ToolCommand.Group className='-mx-1 -px-1'>
|
||||
{toolBlocks.map((block) => (
|
||||
<ToolCommand.Item
|
||||
key={block.type}
|
||||
value={block.name}
|
||||
onSelect={() => handleSelectTool(block)}
|
||||
className='flex cursor-pointer items-center gap-2'
|
||||
>
|
||||
<div
|
||||
className='flex h-6 w-6 items-center justify-center rounded'
|
||||
style={{ backgroundColor: block.bgColor }}
|
||||
>
|
||||
<IconComponent icon={block.icon} className='h-4 w-4 text-white' />
|
||||
</div>
|
||||
<span className='max-w-[140px] truncate'>{block.name}</span>
|
||||
</ToolCommand.Item>
|
||||
))}
|
||||
</ToolCommand.Group>
|
||||
</>
|
||||
)}
|
||||
</ToolCommand.Group>
|
||||
</ToolCommand.List>
|
||||
</ToolCommand.Root>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
@@ -935,50 +888,52 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={param.id} className='relative space-y-1.5'>
|
||||
<div className='flex items-center font-medium text-muted-foreground text-xs'>
|
||||
{formatParamId(param.id)}
|
||||
{param.optionalToolInput && !param.requiredForToolCall && (
|
||||
<span className='ml-1 text-muted-foreground/60 text-xs'>
|
||||
(Optional)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className='relative'>
|
||||
{useChannelSelector && channelSelectorConfig ? (
|
||||
<ChannelSelectorInput
|
||||
blockId={blockId}
|
||||
subBlock={{
|
||||
id: param.id,
|
||||
type: 'channel-selector',
|
||||
title: channelSelectorConfig.title || formatParamId(param.id),
|
||||
provider: channelSelectorConfig.provider,
|
||||
placeholder:
|
||||
channelSelectorConfig.placeholder || param.description,
|
||||
}}
|
||||
credential={credentialForChannelSelector}
|
||||
onChannelSelect={(channelId) => {
|
||||
handleParamChange(toolIndex, param.id, channelId)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ShortInput
|
||||
blockId={blockId}
|
||||
subBlockId={`${subBlockId}-param`}
|
||||
placeholder={param.description}
|
||||
password={shouldBePasswordField(tool.type, param.id)}
|
||||
isConnecting={false}
|
||||
config={{
|
||||
id: `${subBlockId}-param`,
|
||||
type: 'short-input',
|
||||
title: param.id,
|
||||
}}
|
||||
value={tool.params[param.id] || ''}
|
||||
onChange={(value) =>
|
||||
handleParamChange(toolIndex, param.id, value)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<div key={param.id}>
|
||||
<div className='relative space-y-1.5'>
|
||||
<div className='flex items-center font-medium text-muted-foreground text-xs'>
|
||||
{formatParamId(param.id)}
|
||||
{param.optionalToolInput && !param.requiredForToolCall && (
|
||||
<span className='ml-1 text-muted-foreground/60 text-xs'>
|
||||
(Optional)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className='relative'>
|
||||
{useChannelSelector && channelSelectorConfig ? (
|
||||
<ChannelSelectorInput
|
||||
blockId={blockId}
|
||||
subBlock={{
|
||||
id: param.id,
|
||||
type: 'channel-selector',
|
||||
title: channelSelectorConfig.title || formatParamId(param.id),
|
||||
provider: channelSelectorConfig.provider,
|
||||
placeholder:
|
||||
channelSelectorConfig.placeholder || param.description,
|
||||
}}
|
||||
credential={credentialForChannelSelector}
|
||||
onChannelSelect={(channelId) => {
|
||||
handleParamChange(toolIndex, param.id, channelId)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ShortInput
|
||||
blockId={blockId}
|
||||
subBlockId={`${subBlockId}-param`}
|
||||
placeholder={param.description}
|
||||
password={shouldBePasswordField(tool.type, param.id)}
|
||||
isConnecting={false}
|
||||
config={{
|
||||
id: `${subBlockId}-param`,
|
||||
type: 'short-input',
|
||||
title: param.id,
|
||||
}}
|
||||
value={tool.params[param.id] || ''}
|
||||
onChange={(value) =>
|
||||
handleParamChange(toolIndex, param.id, value)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -989,6 +944,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
@@ -1044,7 +1000,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
}
|
||||
|
||||
if (isWide) {
|
||||
setValue([
|
||||
setStoreValue([
|
||||
...selectedTools.map((tool, index) => ({
|
||||
...tool,
|
||||
isExpanded:
|
||||
@@ -1054,7 +1010,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
|
||||
newTool,
|
||||
])
|
||||
} else {
|
||||
setValue([
|
||||
setStoreValue([
|
||||
...selectedTools.map((tool) => ({
|
||||
...tool,
|
||||
isExpanded: false,
|
||||
|
||||
@@ -299,7 +299,7 @@ export function WebhookConfig({
|
||||
blockId,
|
||||
subBlockId,
|
||||
isConnecting,
|
||||
isPreview = false,
|
||||
isPreview = false,
|
||||
value: propValue
|
||||
}: WebhookConfigProps) {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
@@ -316,27 +316,38 @@ export function WebhookConfig({
|
||||
const setWebhookStatus = useWorkflowStore((state) => state.setWebhookStatus)
|
||||
|
||||
// Get the webhook provider from the block state
|
||||
const [webhookProvider, setWebhookProvider] = useSubBlockValue(blockId, 'webhookProvider', false, isPreview, propValue?.webhookProvider)
|
||||
const [storeWebhookProvider, setWebhookProvider] = useSubBlockValue(blockId, 'webhookProvider')
|
||||
|
||||
// Store the webhook path
|
||||
const [webhookPath, setWebhookPath] = useSubBlockValue(blockId, 'webhookPath', false, isPreview, propValue?.webhookPath)
|
||||
const [storeWebhookPath, setWebhookPath] = useSubBlockValue(blockId, 'webhookPath')
|
||||
|
||||
// Store provider-specific configuration
|
||||
const [providerConfig, setProviderConfig] = useSubBlockValue(blockId, 'providerConfig', false, isPreview, propValue?.providerConfig)
|
||||
const [storeProviderConfig, setProviderConfig] = useSubBlockValue(blockId, 'providerConfig')
|
||||
|
||||
// Use prop values when available (preview mode), otherwise use store values
|
||||
const webhookProvider = propValue?.webhookProvider ?? storeWebhookProvider
|
||||
const webhookPath = propValue?.webhookPath ?? storeWebhookPath
|
||||
const providerConfig = propValue?.providerConfig ?? storeProviderConfig
|
||||
|
||||
// Reset provider config when provider changes
|
||||
useEffect(() => {
|
||||
if (webhookProvider && !isPreview) {
|
||||
if (webhookProvider) {
|
||||
// Reset the provider config when the provider changes
|
||||
setProviderConfig({})
|
||||
}
|
||||
}, [webhookProvider, setProviderConfig, isPreview])
|
||||
}, [webhookProvider, setProviderConfig])
|
||||
|
||||
// Store the actual provider from the database
|
||||
const [actualProvider, setActualProvider] = useState<string | null>(null)
|
||||
|
||||
// Check if webhook exists in the database
|
||||
useEffect(() => {
|
||||
// Skip API calls in preview mode
|
||||
if (isPreview) {
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const checkWebhook = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
@@ -386,6 +397,7 @@ export function WebhookConfig({
|
||||
setWebhookPath,
|
||||
setWebhookProvider,
|
||||
setWebhookStatus,
|
||||
isPreview,
|
||||
])
|
||||
|
||||
const handleOpenModal = () => {
|
||||
@@ -398,6 +410,9 @@ export function WebhookConfig({
|
||||
}
|
||||
|
||||
const handleSaveWebhook = async (path: string, config: ProviderConfig) => {
|
||||
// Prevent saving in preview mode
|
||||
if (isPreview) return false
|
||||
|
||||
try {
|
||||
setIsSaving(true)
|
||||
setError(null)
|
||||
@@ -461,7 +476,8 @@ export function WebhookConfig({
|
||||
}
|
||||
|
||||
const handleDeleteWebhook = async () => {
|
||||
if (!webhookId) return false
|
||||
// Prevent deletion in preview mode
|
||||
if (isPreview || !webhookId) return false
|
||||
|
||||
try {
|
||||
setIsDeleting(true)
|
||||
@@ -554,8 +570,8 @@ export function WebhookConfig({
|
||||
'https://www.googleapis.com/auth/gmail.modify',
|
||||
'https://www.googleapis.com/auth/gmail.labels',
|
||||
]}
|
||||
label='Select Gmail account'
|
||||
disabled={isConnecting || isSaving || isDeleting}
|
||||
label="Select Gmail account"
|
||||
disabled={isConnecting || isSaving || isDeleting || isPreview}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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 ? (
|
||||
<div className='mr-2 h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
|
||||
@@ -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 ? (
|
||||
<div className='mr-2 h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
|
||||
|
||||
@@ -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<T = any>(
|
||||
@@ -179,36 +173,12 @@ export function useSubBlockValue<T = any>(
|
||||
|
||||
// Previous model reference for detecting model changes
|
||||
const prevModelRef = useRef<string | null>(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<T = any>(
|
||||
// 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<T = any>(
|
||||
[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<T = any>(
|
||||
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
|
||||
}
|
||||
@@ -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<string, any>
|
||||
}
|
||||
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -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 <ToolInput
|
||||
blockId={blockId}
|
||||
subBlockId={config.id}
|
||||
isPreview={isPreview}
|
||||
value={directValue}
|
||||
previewValue={previewValue}
|
||||
/>
|
||||
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({
|
||||
<WebhookConfig
|
||||
blockId={blockId}
|
||||
subBlockId={config.id}
|
||||
isConnecting={isConnecting}
|
||||
isConnecting={isConnecting}
|
||||
isPreview={isPreview}
|
||||
value={directValue}
|
||||
value={previewValue}
|
||||
/>
|
||||
)
|
||||
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 (
|
||||
<CredentialSelector
|
||||
value={isPreview && directValue ? directValue : (typeof config.value === 'string' ? config.value : '')}
|
||||
value={isPreview ? (previewValue || '') : (typeof config.value === 'string' ? config.value : '')}
|
||||
onChange={(value) => {
|
||||
// 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 <FileSelectorInput
|
||||
blockId={blockId}
|
||||
subBlock={config}
|
||||
disabled={isConnecting}
|
||||
disabled={isConnecting || isPreview}
|
||||
isPreview={isPreview}
|
||||
value={directValue}
|
||||
previewValue={previewValue}
|
||||
/>
|
||||
case 'project-selector':
|
||||
return <ProjectSelectorInput
|
||||
blockId={blockId}
|
||||
subBlock={config}
|
||||
disabled={isConnecting}
|
||||
disabled={isConnecting || isPreview}
|
||||
isPreview={isPreview}
|
||||
value={directValue}
|
||||
previewValue={previewValue}
|
||||
/>
|
||||
case 'folder-selector':
|
||||
return <FolderSelectorInput
|
||||
blockId={blockId}
|
||||
subBlock={config}
|
||||
disabled={isConnecting}
|
||||
disabled={isConnecting || isPreview}
|
||||
isPreview={isPreview}
|
||||
value={directValue}
|
||||
previewValue={previewValue}
|
||||
/>
|
||||
case 'input-format':
|
||||
return <InputFormat
|
||||
blockId={blockId}
|
||||
subBlockId={config.id}
|
||||
isPreview={isPreview}
|
||||
value={directValue}
|
||||
previewValue={previewValue}
|
||||
/>
|
||||
default:
|
||||
return <div>Unknown input type: {config.type}</div>
|
||||
@@ -327,4 +336,4 @@ export function SubBlock({
|
||||
{renderInput()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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<string, any>
|
||||
blockState?: any
|
||||
}
|
||||
|
||||
// Combine both interfaces into a single component
|
||||
@@ -42,11 +39,7 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
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<WorkflowBlockProps>) {
|
||||
)
|
||||
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<WorkflowBlockProps>) {
|
||||
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<WorkflowBlockProps>) {
|
||||
}
|
||||
}, [type, hasActiveWebhook])
|
||||
|
||||
// Update node internals when handles change
|
||||
useEffect(() => {
|
||||
updateNodeInternals(id)
|
||||
}, [id, horizontalHandles, updateNodeInternals])
|
||||
@@ -220,6 +156,7 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
}
|
||||
}
|
||||
|
||||
// Add effect to observe size changes with debounced updates
|
||||
useEffect(() => {
|
||||
if (!contentRef.current) return
|
||||
|
||||
@@ -232,10 +169,12 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
}, 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<WorkflowBlockProps>) {
|
||||
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<WorkflowBlockProps>) {
|
||||
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<WorkflowBlockProps>) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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<WorkflowBlockProps>) {
|
||||
return providers[providerId] || 'Webhook'
|
||||
}
|
||||
|
||||
const shouldShowScheduleBadge = isStarterBlock && !isLoadingScheduleInfo && scheduleInfo !== null
|
||||
|
||||
return (
|
||||
<div className='group relative'>
|
||||
<Card
|
||||
@@ -401,15 +339,6 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
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<WorkflowBlockProps>) {
|
||||
</Badge>
|
||||
)}
|
||||
{/* Schedule indicator badge - displayed for starter blocks with active schedules */}
|
||||
{shouldShowScheduleBadge && (
|
||||
{showScheduleIndicator && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge
|
||||
@@ -533,7 +462,7 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{scheduleInfo?.isDisabled ? 'Disabled' : 'Scheduled'}
|
||||
Scheduled
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side='top' className='max-w-[300px] p-4'>
|
||||
@@ -598,7 +527,7 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{config.subBlocks.some((block) => block.mode) && (
|
||||
{config.longDescription && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
@@ -718,10 +647,9 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
<SubBlock
|
||||
blockId={id}
|
||||
config={subBlock}
|
||||
isConnecting={isConnecting}
|
||||
isPreview={data.isPreview}
|
||||
previewValue={data.isPreview && data.subBlockValues ?
|
||||
(data.subBlockValues[subBlock.id]?.value || null) : null}
|
||||
isConnecting={isConnecting}
|
||||
isPreview={data.isPreview}
|
||||
subBlockValues={data.subBlockValues}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@@ -806,4 +734,4 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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<string, any>
|
||||
edges: Array<{
|
||||
id: string
|
||||
source: string
|
||||
target: string
|
||||
sourceHandle?: string
|
||||
targetHandle?: string
|
||||
}>
|
||||
loops: Record<string, any>
|
||||
}
|
||||
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({
|
||||
<ReactFlowProvider>
|
||||
<div
|
||||
style={{ height, width }}
|
||||
className={cn(className, 'preview-mode')}
|
||||
className={cn('preview-mode')}
|
||||
>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Edge } from 'reactflow'
|
||||
import type { BlockOutput, SubBlockType } from '@/blocks/types'
|
||||
import { Edge } from 'reactflow'
|
||||
import { BlockOutput, SubBlockType } from '@/blocks/types'
|
||||
import { DeploymentStatus } from '../registry/types'
|
||||
|
||||
export interface Position {
|
||||
x: number
|
||||
@@ -63,19 +64,6 @@ export interface Loop {
|
||||
forEachItems?: any[] | Record<string, any> | string // Items or expression
|
||||
}
|
||||
|
||||
export interface Parallel {
|
||||
id: string
|
||||
nodes: string[]
|
||||
distribution?: 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[]
|
||||
|
||||
Reference in New Issue
Block a user