cleaning up

This commit is contained in:
Adam Gough
2025-06-02 10:10:35 -07:00
parent 43d60e9670
commit 798cdd1a56
36 changed files with 701 additions and 521 deletions
@@ -69,4 +69,4 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
const response = createErrorResponse(error.message || 'Failed to fetch deployed state', 500)
return addNoCacheHeaders(response)
}
}
}
@@ -1,7 +1,7 @@
'use client'
import { useState, useEffect } from 'react'
import { Info, Loader2 } from 'lucide-react'
import { useState } from 'react'
import { Loader2 } from 'lucide-react'
import {
AlertDialog,
AlertDialogAction,
@@ -20,9 +20,8 @@ import { ApiKey } from '@/app/w/[id]/components/control-bar/components/deploy-mo
import { DeployStatus } from '@/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/components/deploy-status/deploy-status'
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 type { WorkflowState } from '@/stores/workflows/workflow/types'
import { DeployedWorkflowModal } from '../../../deployment-controls/components/deployed-workflow-modal'
import { WorkflowState } from '@/stores/workflows/workflow/types'
interface DeploymentInfoProps {
isLoading?: boolean
@@ -51,7 +50,7 @@ export function DeploymentInfo({
isUndeploying,
workflowId,
deployedState,
isLoadingDeployedState
isLoadingDeployedState,
}: DeploymentInfoProps) {
const [isViewingDeployed, setIsViewingDeployed] = useState(false)
const { addNotification } = useNotificationStore()
@@ -61,12 +60,13 @@ export function DeploymentInfo({
addNotification('error', 'Cannot view deployment: Workflow ID is missing', null)
return
}
// If deployedState is already loaded, use it directly
if (deployedState) {
setIsViewingDeployed(true)
return
} else if (!isLoadingDeployedState) {
}
if (!isLoadingDeployedState) {
addNotification('error', 'Cannot view deployment: No deployed state available', workflowId)
}
}
@@ -27,7 +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'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
const logger = createLogger('DeployModal')
@@ -618,8 +618,8 @@ export function DeployModal({
</div>
</div>
<div className="flex-1 overflow-y-auto">
<div className="p-6">
<div className='flex-1 overflow-y-auto'>
<div className='p-6'>
{activeTab === 'api' && (
<>
{isDeployed ? (
@@ -637,12 +637,12 @@ export function DeployModal({
) : (
<>
{apiDeployError && (
<div className="mb-4 p-3 bg-destructive/10 border border-destructive/30 rounded-md text-sm text-destructive">
<div className="font-semibold">API Deployment Error</div>
<div className='mb-4 rounded-md border border-destructive/30 bg-destructive/10 p-3 text-destructive text-sm'>
<div className='font-semibold'>API Deployment Error</div>
<div>{apiDeployError}</div>
</div>
)}
<div className="px-1 -mx-1">
<div className='-mx-1 px-1'>
<DeployForm
apiKeys={apiKeys}
keysLoaded={keysLoaded}
@@ -1,14 +1,14 @@
'use client'
import { useState, useMemo } from 'react'
import { useMemo, useState } from 'react'
import { Card, CardContent, CardHeader } from '@/components/ui/card'
import { Switch } from '@/components/ui/switch'
import { Label } from '@/components/ui/label'
import { cn } from '@/lib/utils'
import { Switch } from '@/components/ui/switch'
import { createLogger } from '@/lib/logs/console-logger'
import { cn } from '@/lib/utils'
import { WorkflowPreview } from '@/app/w/components/workflow-preview/workflow-preview'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { WorkflowState } from '@/stores/workflows/workflow/types'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
const logger = createLogger('DeployedWorkflowCard')
@@ -29,14 +29,14 @@ export function DeployedWorkflowCard({
// // Generate a unique key for the workflow preview
const previewKey = useMemo(() => {
return `${showingDeployed ? 'deployed' : 'current'}-preview-${activeWorkflowId}}`;
}, [showingDeployed, activeWorkflowId]);
return `${showingDeployed ? 'deployed' : 'current'}-preview-${activeWorkflowId}}`
}, [showingDeployed, activeWorkflowId])
return (
<Card className={cn('relative overflow-hidden', className)}>
<CardHeader
className={cn(
'space-y-4 p-4 sticky top-0 z-10',
'sticky top-0 z-10 space-y-4 p-4',
'bg-background/70 dark:bg-background/50',
'border-border/30 border-b dark:border-border/20',
'shadow-sm'
@@ -47,19 +47,19 @@ export function DeployedWorkflowCard({
{showingDeployed ? 'Deployed Workflow' : 'Current Workflow'}
</h3>
{/* Controls */}
<div className="flex items-center gap-2">
<div className='flex items-center gap-2'>
{/* Version toggle - only show if there's a current version */}
{currentWorkflowState && (
<div className="flex items-center space-x-2">
<Label htmlFor="workflow-version-toggle" className="text-sm text-muted-foreground">
<div className='flex items-center space-x-2'>
<Label htmlFor='workflow-version-toggle' className='text-muted-foreground text-sm'>
Current
</Label>
<Switch
id="workflow-version-toggle"
checked={showingDeployed}
<Switch
id='workflow-version-toggle'
checked={showingDeployed}
onCheckedChange={setShowingDeployed}
/>
<Label htmlFor="workflow-version-toggle" className="text-sm text-muted-foreground">
<Label htmlFor='workflow-version-toggle' className='text-muted-foreground text-sm'>
Deployed
</Label>
</div>
@@ -67,23 +67,22 @@ export function DeployedWorkflowCard({
</div>
</div>
</CardHeader>
<div className="h-px w-full bg-border shadow-sm"></div>
<CardContent className="p-0">
<div className='h-px w-full bg-border shadow-sm' />
<CardContent className='p-0'>
{/* Workflow preview with fixed height */}
<div className="h-[500px] w-full">
{/* {sanitizedWorkflowState ? ( */}
<WorkflowPreview
key={previewKey}
workflowState={workflowToShow as WorkflowState}
showSubBlocks={true}
height='100%'
width='100%'
isPannable={true}
defaultPosition={{ x: 0, y: 0 }}
defaultZoom={1}
/>
<div className='h-[500px] w-full'>
<WorkflowPreview
key={previewKey}
workflowState={workflowToShow as WorkflowState}
showSubBlocks={true}
height='100%'
width='100%'
isPannable={true}
defaultPosition={{ x: 0, y: 0 }}
defaultZoom={1}
/>
</div>
</CardContent>
</Card>
@@ -1,6 +1,6 @@
'use client'
import { useState, useMemo, useEffect, useRef } from 'react'
import { useEffect, useRef, useState } from 'react'
import {
AlertDialog,
AlertDialogAction,
@@ -13,19 +13,13 @@ import {
AlertDialogTrigger,
} from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { createLogger } from '@/lib/logs/console-logger'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { mergeSubblockState } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
import { DeployedWorkflowCard } from './deployed-workflow-card'
import { WorkflowState } from '@/stores/workflows/workflow/types'
const logger = createLogger('DeployedWorkflowModal')
@@ -45,15 +39,15 @@ export function DeployedWorkflowModal({
const [showRevertDialog, setShowRevertDialog] = useState(false)
const { revertToDeployedState } = useWorkflowStore()
const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
// Add instance ID to track component lifecycle
const modalOpenCount = useRef(0);
const modalOpenCount = useRef(0)
useEffect(() => {
if (isOpen) {
modalOpenCount.current += 1;
modalOpenCount.current += 1
}
}, [isOpen]);
}, [isOpen])
// Get current workflow state to compare with deployed state
const currentWorkflowState = useWorkflowStore((state) => ({
@@ -88,13 +82,13 @@ export function DeployedWorkflowModal({
deployedWorkflowState={deployedWorkflowState}
/>
<div className="flex justify-between mt-6">
<div className='mt-6 flex justify-between'>
{needsRedeployment && (
<AlertDialog open={showRevertDialog} onOpenChange={setShowRevertDialog}>
<AlertDialogTrigger asChild>
<Button variant="destructive">Revert to Deployed</Button>
<Button variant='destructive'>Revert to Deployed</Button>
</AlertDialogTrigger>
<AlertDialogContent style={{ zIndex: 1001 }} className="sm:max-w-[425px]">
<AlertDialogContent style={{ zIndex: 1001 }} className='sm:max-w-[425px]'>
<AlertDialogHeader>
<AlertDialogTitle>Revert to Deployed Version?</AlertDialogTitle>
<AlertDialogDescription>
@@ -106,7 +100,7 @@ export function DeployedWorkflowModal({
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleRevert}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
className='bg-destructive text-destructive-foreground hover:bg-destructive/90'
>
Revert
</AlertDialogAction>
@@ -115,7 +109,7 @@ export function DeployedWorkflowModal({
</AlertDialog>
)}
<Button variant="outline" onClick={onClose} className="ml-auto">
<Button variant='outline' onClick={onClose} className='ml-auto'>
Close
</Button>
</div>
@@ -1,12 +1,12 @@
'use client'
import { useState, useEffect, useRef } from 'react'
import { useEffect, useRef, useState } from 'react'
import { Loader2, Rocket } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { WorkflowState } from '@/stores/workflows/workflow/types'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
import { DeployModal } from '../deploy-modal/deploy-modal'
interface DeploymentControlsProps {
@@ -40,27 +40,27 @@ export function DeploymentControls({
const [isDeploying, _setIsDeploying] = useState(false)
const [isModalOpen, setIsModalOpen] = useState(false)
// Track the last seen workflow ID
const lastWorkflowIdRef = useRef<string | null>(null)
// Update last seen workflow ID
useEffect(() => {
if (activeWorkflowId !== lastWorkflowIdRef.current) {
lastWorkflowIdRef.current = activeWorkflowId;
lastWorkflowIdRef.current = activeWorkflowId
}
}, [activeWorkflowId]);
}, [activeWorkflowId])
// Refetch deployed state wrapper
const refetchWithErrorHandling = async () => {
if (!activeWorkflowId) return;
if (!activeWorkflowId) return
try {
await refetchDeployedState();
await refetchDeployedState()
} catch (error) {
// Silent error handling
}
};
}
// Update parent component when workflow-specific status changes
useEffect(() => {
@@ -46,7 +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 type { WorkflowState } from '@/stores/workflows/workflow/types'
import {
getKeyboardShortcutText,
useKeyboardShortcuts,
@@ -87,7 +87,7 @@ export function ControlBar() {
showNotification,
removeNotification,
} = useNotificationStore()
const { history, revertToHistoryState, lastSaved } = useWorkflowStore()
const { history, revertToHistoryState, lastSaved } = useWorkflowStore()
const {
workflows,
updateWorkflow,
@@ -215,15 +215,15 @@ export function ControlBar() {
try {
setIsLoadingDeployedState(true)
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) {
if (response.status === 404) {
// No deployed state found
@@ -232,16 +232,15 @@ export function ControlBar() {
}
throw new Error(`Failed to fetch deployed state: ${response.statusText}`)
}
const data = await response.json()
// Final check to ensure we're still on the same workflow
if (requestWorkflowId === useWorkflowRegistry.getState().activeWorkflowId) {
setDeployedState(data.deployedState || null)
} else {
logger.debug('Workflow changed after deployed state response, ignoring result')
}
} catch (error) {
logger.error('Error fetching deployed state:', { error })
// Only set error state if we're still on the same workflow
@@ -263,7 +262,7 @@ export function ControlBar() {
setDeployedState(null)
setIsLoadingDeployedState(false)
}
// Then fetch the new deployed state
fetchDeployedState()
}, [activeWorkflowId, isDeployed])
@@ -276,7 +275,7 @@ export function ControlBar() {
setIsLoadingDeployedState(false)
return
}
if (isDeployed) {
// When deployment status becomes true, reset the needsRedeployment flag
setNeedsRedeployment(false)
@@ -43,9 +43,10 @@ export function OutputSelect({
if (block.type === 'starter') return
// Add defensive check to ensure block.name exists and is a string
const blockName = block.name && typeof block.name === 'string'
? block.name.replace(/\s+/g, '').toLowerCase()
: `block-${block.id}`;
const blockName =
block.name && typeof block.name === 'string'
? block.name.replace(/\s+/g, '').toLowerCase()
: `block-${block.id}`
// Add response outputs
if (block.outputs && typeof block.outputs === 'object') {
@@ -97,9 +98,10 @@ export function OutputSelect({
const output = workflowOutputs.find((o) => o.id === validOutputs[0])
if (output) {
// Add defensive check for output.blockName
const blockNameText = output.blockName && typeof output.blockName === 'string'
? output.blockName.replace(/\s+/g, '').toLowerCase()
: `block-${output.blockId}`;
const blockNameText =
output.blockName && typeof output.blockName === 'string'
? output.blockName.replace(/\s+/g, '').toLowerCase()
: `block-${output.blockId}`
return `${blockNameText}.${output.path}`
}
return placeholder
@@ -3,7 +3,6 @@ import { Label } from '@/components/ui/label'
import { cn } from '@/lib/utils'
import { useSubBlockValue } from '../hooks/use-sub-block-value'
interface CheckboxListProps {
blockId: string
subBlockId: string
@@ -14,36 +13,34 @@ interface CheckboxListProps {
subBlockValues?: Record<string, any>
}
export function CheckboxList({
blockId,
subBlockId,
title,
options,
export function CheckboxList({
blockId,
subBlockId,
title,
options,
layout,
isPreview = false,
subBlockValues
subBlockValues,
}: CheckboxListProps) {
return (
<div className={cn('grid gap-4', layout === 'half' ? 'grid-cols-2' : 'grid-cols-1', 'pt-1')}>
{options.map((option) => {
const [storeValue, setStoreValue] = useSubBlockValue(
blockId,
option.id
)
const [storeValue, setStoreValue] = useSubBlockValue(blockId, option.id)
// Get preview value for this specific option
const previewValue = isPreview && subBlockValues ? subBlockValues[option.id]?.value : undefined
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
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState, useMemo } from 'react'
import type { ReactElement } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { Wand2 } from 'lucide-react'
import { highlight, languages } from 'prismjs'
import 'prismjs/components/prism-javascript'
@@ -57,11 +57,13 @@ export function Code({
generationType = 'javascript-function-body',
value: propValue,
isPreview = false,
previewValue
previewValue,
}: CodeProps) {
// Determine the AI prompt placeholder based on language
const aiPromptPlaceholder = useMemo(() => {
return language === 'json' ? 'Describe the JSON schema you need...' : 'Describe the function you need...'
return language === 'json'
? 'Describe the JSON schema you need...'
: 'Describe the function you need...'
}, [language])
// State management
@@ -89,7 +91,7 @@ export function Code({
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)
const value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue
// AI Code Generation Hook
const handleStreamStart = () => {
@@ -208,7 +210,7 @@ export function Code({
clearTimeout(timeoutId)
resizeObserver.disconnect()
}
}, [code])
}, [code])
// Handlers
const handleDrop = (e: React.DragEvent) => {
@@ -325,7 +327,7 @@ export function Code({
onDragOver={(e) => e.preventDefault()}
onDrop={handleDrop}
>
<div className="absolute right-3 top-2 z-10 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<div className='absolute top-2 right-3 z-10 flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100'>
{!isCollapsed && !isAiStreaming && !isPreview && (
<Button
variant='ghost'
@@ -42,12 +42,12 @@ const generateStableId = (blockId: string, suffix: string): string => {
return `${blockId}-${suffix}`
}
export function ConditionInput({
blockId,
subBlockId,
export function ConditionInput({
blockId,
subBlockId,
isConnecting,
isPreview = false,
previewValue
previewValue,
}: ConditionInputProps) {
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
const editorRef = useRef<HTMLDivElement>(null)
@@ -199,7 +199,8 @@ export function ConditionInput({
useEffect(() => {
// Skip if we're currently syncing from store to prevent loops
// or if we're not ready yet (still initializing) or in preview mode
if (isSyncingFromStoreRef.current || !isReady || conditionalBlocks.length === 0 || isPreview) return
if (isSyncingFromStoreRef.current || !isReady || conditionalBlocks.length === 0 || isPreview)
return
const newValue = JSON.stringify(conditionalBlocks)
@@ -209,7 +210,15 @@ export function ConditionInput({
setStoreValue(newValue)
updateNodeInternals(`${blockId}-${subBlockId}`)
}
}, [conditionalBlocks, blockId, subBlockId, setStoreValue, updateNodeInternals, isReady, isPreview])
}, [
conditionalBlocks,
blockId,
subBlockId,
setStoreValue,
updateNodeInternals,
isReady,
isPreview,
])
// Cleanup when component unmounts
useEffect(() => {
@@ -227,7 +236,7 @@ export function ConditionInput({
textarea: HTMLTextAreaElement | null
) => {
if (isPreview) return
try {
setConditionalBlocks((blocks) =>
blocks.map((block) => {
@@ -440,7 +449,7 @@ export function ConditionInput({
// 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
@@ -474,7 +483,7 @@ export function ConditionInput({
const removeBlock = (id: string) => {
if (isPreview) return
// Remove any associated edges before removing the block
edges.forEach((edge) => {
if (edge.sourceHandle?.startsWith(`condition-${id}`)) {
@@ -488,7 +497,7 @@ export function ConditionInput({
const moveBlock = (id: string, direction: 'up' | 'down') => {
if (isPreview) return
const blockIndex = conditionalBlocks.findIndex((block) => block.id === id)
if (
(direction === 'up' && blockIndex === 0) ||
@@ -597,7 +606,7 @@ export function ConditionInput({
size='sm'
onClick={() => addBlock(block.id)}
disabled={isPreview}
className="h-8 w-8"
className='h-8 w-8'
>
<Plus className='h-4 w-4' />
<span className='sr-only'>Add Block</span>
@@ -614,7 +623,7 @@ export function ConditionInput({
size='sm'
onClick={() => moveBlock(block.id, 'up')}
disabled={isPreview || index === 0}
className="h-8 w-8"
className='h-8 w-8'
>
<ChevronUp className='h-4 w-4' />
<span className='sr-only'>Move Up</span>
@@ -630,7 +639,7 @@ export function ConditionInput({
size='sm'
onClick={() => moveBlock(block.id, 'down')}
disabled={isPreview || index === conditionalBlocks.length - 1}
className="h-8 w-8"
className='h-8 w-8'
>
<ChevronDown className='h-4 w-4' />
<span className='sr-only'>Move Down</span>
@@ -647,7 +656,7 @@ export function ConditionInput({
size='sm'
onClick={() => removeBlock(block.id)}
disabled={isPreview || conditionalBlocks.length === 1}
className="h-8 w-8 text-destructive hover:text-destructive"
className='h-8 w-8 text-destructive hover:text-destructive'
>
<Trash className='h-4 w-4' />
<span className='sr-only'>Delete Block</span>
@@ -710,8 +719,11 @@ export function ConditionInput({
minHeight: '46px',
lineHeight: '21px',
}}
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')}
className={cn('focus:outline-none', isPreview && 'cursor-not-allowed opacity-50')}
textareaClassName={cn(
'focus:outline-none focus:ring-0 bg-transparent',
isPreview && 'pointer-events-none'
)}
/>
{block.showEnvVars && (
@@ -10,7 +10,6 @@ import { cn } from '@/lib/utils'
import { useNotificationStore } from '@/stores/notifications/store'
import { useSubBlockValue } from '../hooks/use-sub-block-value'
interface DateInputProps {
blockId: string
subBlockId: string
@@ -19,15 +18,15 @@ interface DateInputProps {
previewValue?: string | null
}
export function DateInput({
blockId,
subBlockId,
export function DateInput({
blockId,
subBlockId,
placeholder,
isPreview = false,
previewValue
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)
@@ -42,7 +41,7 @@ export function DateInput({
const handleDateSelect = (selectedDate: Date | undefined) => {
if (isPreview) return
if (selectedDate) {
const today = new Date()
today.setHours(0, 0, 0, 0)
@@ -58,7 +57,7 @@ export function DateInput({
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
variant='outline'
disabled={isPreview}
className={cn(
'w-full justify-start text-left font-normal',
@@ -6,7 +6,6 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { createLogger } from '@/lib/logs/console-logger'
import { useSubBlockValue } from '../hooks/use-sub-block-value'
interface DropdownProps {
@@ -21,20 +20,20 @@ interface DropdownProps {
previewValue?: string | null
}
export function Dropdown({
options,
defaultValue,
blockId,
export function Dropdown({
options,
defaultValue,
blockId,
subBlockId,
value: propValue,
isPreview = false,
previewValue
previewValue,
}: DropdownProps) {
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)
const value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue
// Evaluate options if it's a function
const evaluatedOptions = useMemo(() => {
@@ -5,7 +5,6 @@ import { Label } from '@/components/ui/label'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useSubBlockValue } from '../hooks/use-sub-block-value'
interface EvalMetric {
id: string
name: string
@@ -31,14 +30,14 @@ const DEFAULT_METRIC: EvalMetric = {
range: { min: 0, max: 1 },
}
export function EvalInput({
blockId,
export function EvalInput({
blockId,
subBlockId,
isPreview = false,
previewValue
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
@@ -48,7 +47,7 @@ export function EvalInput({
// Metric operations
const addMetric = () => {
if (isPreview) return
const newMetric: EvalMetric = {
...DEFAULT_METRIC,
id: crypto.randomUUID(),
@@ -64,7 +63,9 @@ export function EvalInput({
// Update handlers
const updateMetric = (id: string, field: keyof EvalMetric, value: any) => {
if (isPreview) return
setStoreValue(metrics.map((metric) => (metric.id === id ? { ...metric, [field]: value } : metric)))
setStoreValue(
metrics.map((metric) => (metric.id === id ? { ...metric, [field]: value } : metric))
)
}
const updateRange = (id: string, field: 'min' | 'max', value: string) => {
@@ -76,7 +77,7 @@ export function EvalInput({
...metric,
range: {
...metric.range,
[field]: value === '' ? undefined : parseInt(value, 10),
[field]: value === '' ? undefined : Number.parseInt(value, 10),
},
}
: metric
@@ -86,23 +87,23 @@ export function EvalInput({
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
)
metrics.map((metric) => (metric.id === id ? { ...metric, threshold: undefined } : metric))
)
return
}
const numValue = parseFloat(sanitizedValue)
const numValue = Number.parseFloat(sanitizedValue)
setStoreValue(
metrics.map((metric) =>
metric.id === id ? { ...metric, threshold: isNaN(numValue) ? undefined : numValue } : metric
metric.id === id
? { ...metric, threshold: Number.isNaN(numValue) ? undefined : numValue }
: metric
)
)
}
@@ -114,9 +115,15 @@ export function EvalInput({
<div className='flex items-center gap-1'>
<Tooltip>
<TooltipTrigger asChild>
<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
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>
@@ -129,7 +136,7 @@ export function EvalInput({
size='sm'
onClick={() => removeMetric(metric.id)}
disabled={isPreview || metrics.length === 1}
className="h-8 w-8 text-destructive hover:text-destructive"
className='h-8 w-8 text-destructive hover:text-destructive'
>
<Trash className='h-4 w-4' />
<span className='sr-only'>Delete Metric</span>
@@ -159,9 +166,9 @@ export function EvalInput({
name='name'
value={metric.name}
onChange={(e) => updateMetric(metric.id, 'name', e.target.value)}
placeholder="Accuracy"
placeholder='Accuracy'
disabled={isPreview}
className="placeholder:text-muted-foreground/50"
className='placeholder:text-muted-foreground/50'
/>
</div>
@@ -170,9 +177,9 @@ export function EvalInput({
<Input
value={metric.description}
onChange={(e) => updateMetric(metric.id, 'description', e.target.value)}
placeholder="How accurate is the response?"
placeholder='How accurate is the response?'
disabled={isPreview}
className="placeholder:text-muted-foreground/50"
className='placeholder:text-muted-foreground/50'
/>
</div>
@@ -185,7 +192,7 @@ export function EvalInput({
onChange={(e) => updateRange(metric.id, 'min', e.target.value)}
onBlur={(e) => updateThreshold(metric.id, e.target.value)}
disabled={isPreview}
className="placeholder:text-muted-foreground/50"
className='placeholder:text-muted-foreground/50'
/>
</div>
<div className='space-y-1'>
@@ -196,7 +203,7 @@ export function EvalInput({
onChange={(e) => updateRange(metric.id, 'max', e.target.value)}
onBlur={(e) => updateThreshold(metric.id, e.target.value)}
disabled={isPreview}
className="placeholder:text-muted-foreground/50"
className='placeholder:text-muted-foreground/50'
/>
</div>
</div>
@@ -5,7 +5,6 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp
import { env } from '@/lib/env'
import type { SubBlockConfig } from '@/blocks/types'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { createLogger } from '@/lib/logs/console-logger'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import type { ConfluenceFileInfo } from './components/confluence-file-selector'
import { ConfluenceFileSelector } from './components/confluence-file-selector'
@@ -20,7 +19,6 @@ import { MicrosoftFileSelector } from './components/microsoft-file-selector'
import type { TeamsMessageInfo } from './components/teams-message-selector'
import { TeamsMessageSelector } from './components/teams-message-selector'
interface FileSelectorInputProps {
blockId: string
subBlock: SubBlockConfig
@@ -29,12 +27,12 @@ interface FileSelectorInputProps {
previewValue?: any | null
}
export function FileSelectorInput({
blockId,
subBlock,
export function FileSelectorInput({
blockId,
subBlock,
disabled,
isPreview = false,
previewValue
previewValue,
}: FileSelectorInputProps) {
const { getValue, setValue } = useSubBlockStore()
const { activeWorkflowId } = useWorkflowRegistry()
@@ -66,31 +64,33 @@ export function FileSelectorInput({
// Get the current value from the store or prop value if in preview mode
useEffect(() => {
if (isPreview && previewValue !== undefined) {
const value = previewValue;
const value = previewValue
if (value && typeof value === 'string') {
if (isJira) {
setSelectedIssueId(value);
setSelectedIssueId(value)
} else if (isDiscord) {
setSelectedChannelId(value);
setSelectedChannelId(value)
} else if (isMicrosoftTeams) {
setSelectedMessageId(value)
} else {
setSelectedFileId(value);
setSelectedFileId(value)
}
}
} else {
const value = getValue(blockId, subBlock.id);
const value = getValue(blockId, subBlock.id)
if (value && typeof value === 'string') {
if (isJira) {
setSelectedIssueId(value);
setSelectedIssueId(value)
} else if (isDiscord) {
setSelectedChannelId(value);
setSelectedChannelId(value)
} else if (isMicrosoftTeams) {
setSelectedMessageId(value);
setSelectedMessageId(value)
} else {
setSelectedFileId(value);
setSelectedFileId(value)
}
}
}
}, [blockId, subBlock.id, getValue, isJira, isDiscord, isPreview, previewValue]);
}, [blockId, subBlock.id, getValue, isJira, isDiscord, isMicrosoftTeams, isPreview, previewValue])
// Handle file selection
const handleFileChange = (fileId: string, info?: any) => {
@@ -1,6 +1,6 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { useRef, useState } from 'react'
import { X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Progress } from '@/components/ui/progress'
@@ -10,7 +10,6 @@ import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import { useSubBlockValue } from '../hooks/use-sub-block-value'
interface FileUploadProps {
blockId: string
subBlockId: string
@@ -41,7 +40,7 @@ export function FileUpload({
acceptedTypes = '*',
multiple = false, // Default to single file for backward compatibility
isPreview = false,
previewValue
previewValue,
}: FileUploadProps) {
// State management - handle both single file and array of files
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
@@ -89,7 +88,7 @@ export function FileUpload({
*/
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
if (isPreview) return
e.stopPropagation()
const files = e.target.files
@@ -1,11 +1,7 @@
'use client'
import { useEffect, useState } from 'react'
<<<<<<< HEAD
import type { SubBlockConfig } from '@/blocks/types'
import { createLogger } from '@/lib/logs/console-logger'
=======
>>>>>>> 86800d81 (fix: removed comments)
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { type FolderInfo, FolderSelector } from '../folder-selector'
@@ -22,7 +18,7 @@ export function FolderSelectorInput({
subBlock,
disabled = false,
isPreview = false,
previewValue
previewValue,
}: FolderSelectorInputProps) {
const { getValue, setValue } = useSubBlockStore()
const [selectedFolderId, setSelectedFolderId] = useState<string>('')
@@ -31,27 +27,27 @@ export function FolderSelectorInput({
// Get the current value from the store or prop value if in preview mode
useEffect(() => {
if (isPreview && previewValue !== undefined) {
setSelectedFolderId(previewValue);
setSelectedFolderId(previewValue)
} else {
const value = getValue(blockId, subBlock.id);
const value = getValue(blockId, subBlock.id)
if (value && typeof value === 'string') {
setSelectedFolderId(value);
setSelectedFolderId(value)
} else {
const defaultValue = 'INBOX';
setSelectedFolderId(defaultValue);
const defaultValue = 'INBOX'
setSelectedFolderId(defaultValue)
if (!isPreview) {
setValue(blockId, subBlock.id, defaultValue);
setValue(blockId, subBlock.id, defaultValue)
}
}
}
}, [blockId, subBlock.id, getValue, setValue, isPreview, previewValue]);
}, [blockId, subBlock.id, getValue, setValue, isPreview, previewValue])
// Handle folder selection
const handleFolderChange = (folderId: string, info?: FolderInfo) => {
setSelectedFolderId(folderId);
setFolderInfo(info || null);
setSelectedFolderId(folderId)
setFolderInfo(info || null)
if (!isPreview) {
setValue(blockId, subBlock.id, folderId);
setValue(blockId, subBlock.id, folderId)
}
}
@@ -53,7 +53,7 @@ export function LongInput({
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)
const value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue
// Calculate initial height based on rows prop with reasonable defaults
const getInitialHeight = () => {
@@ -83,14 +83,14 @@ export function LongInput({
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = e.target.value
const newCursorPosition = e.target.selectionStart ?? 0
if (onChange) {
onChange(newValue)
} else if (!isPreview) {
// Only update store when not in preview mode
setStoreValue(newValue)
}
setCursorPosition(newCursorPosition)
// Check for environment variables trigger
@@ -2,18 +2,13 @@
import { useEffect, useState } from 'react'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
<<<<<<< HEAD
import type { SubBlockConfig } from '@/blocks/types'
import { createLogger } from '@/lib/logs/console-logger'
=======
>>>>>>> 86800d81 (fix: removed comments)
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { type DiscordServerInfo, DiscordServerSelector } from './components/discord-server-selector'
import { type JiraProjectInfo, JiraProjectSelector } from './components/jira-project-selector'
import { type LinearProjectInfo, LinearProjectSelector } from './components/linear-project-selector'
import { type LinearTeamInfo, LinearTeamSelector } from './components/linear-team-selector'
interface ProjectSelectorInputProps {
blockId: string
subBlock: SubBlockConfig
@@ -29,7 +24,7 @@ export function ProjectSelectorInput({
disabled = false,
onProjectSelect,
isPreview = false,
previewValue
previewValue,
}: ProjectSelectorInputProps) {
const { getValue, setValue } = useSubBlockStore()
const [selectedProjectId, setSelectedProjectId] = useState<string>('')
@@ -47,14 +42,14 @@ export function ProjectSelectorInput({
// Get the current value from the store or prop value if in preview mode
useEffect(() => {
if (isPreview && previewValue !== undefined) {
setSelectedProjectId(previewValue);
setSelectedProjectId(previewValue)
} else {
const value = getValue(blockId, subBlock.id);
const value = getValue(blockId, subBlock.id)
if (value && typeof value === 'string') {
setSelectedProjectId(value);
setSelectedProjectId(value)
}
}
}, [blockId, subBlock.id, getValue, isPreview, previewValue]);
}, [blockId, subBlock.id, getValue, isPreview, previewValue])
// Handle project selection
const handleProjectChange = (
@@ -23,12 +23,12 @@ interface ScheduleConfigProps {
previewValue?: any | null
}
export function ScheduleConfig({
blockId,
subBlockId,
export function ScheduleConfig({
blockId,
subBlockId,
isConnecting,
isPreview = false,
previewValue
previewValue,
}: ScheduleConfigProps) {
const [error, setError] = useState<string | null>(null)
const [scheduleId, setScheduleId] = useState<string | null>(null)
@@ -58,7 +58,7 @@ export function ScheduleConfig({
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
@@ -152,7 +152,7 @@ export function ScheduleConfig({
const handleSaveSchedule = async (): Promise<boolean> => {
if (isPreview) return false
setIsSaving(true)
setError(null)
@@ -34,24 +34,21 @@ export function ShortInput({
onChange,
value: propValue,
isPreview = false,
previewValue
previewValue,
}: ShortInputProps) {
const [isFocused, setIsFocused] = useState(false)
const [showEnvVars, setShowEnvVars] = useState(false)
const [showTags, setShowTags] = useState(false)
const validatePropValue = (value: any): string => {
if (value === undefined || value === null) return '';
if (typeof value === 'string') return value;
if (value === undefined || value === null) return ''
if (typeof value === 'string') return value
try {
return String(value);
return String(value)
} catch {
return '';
return ''
}
}
const [storeValue, setStoreValue] = useSubBlockValue(
blockId,
subBlockId
)
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
const [searchTerm, setSearchTerm] = useState('')
const [cursorPosition, setCursorPosition] = useState(0)
const inputRef = useRef<HTMLInputElement>(null)
@@ -62,7 +59,7 @@ export function ShortInput({
const reactFlowInstance = useReactFlow()
// Use preview value when in preview mode, otherwise use store value or prop value
const value = isPreview ? previewValue : (propValue !== undefined ? propValue : storeValue)
const value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue
// Check if this input is API key related
const isApiKeyField = useMemo(() => {
@@ -1,8 +1,7 @@
import { useEffect, useMemo } from 'react'
import { useEffect } from 'react'
import { Slider } from '@/components/ui/slider'
import { useSubBlockValue } from '../hooks/use-sub-block-value'
interface SliderInputProps {
blockId: string
subBlockId: string
@@ -21,20 +20,19 @@ export function SliderInput({
min = 0,
max = 100,
defaultValue = 50,
step = 1,
step = 0.1,
integer = false,
isPreview = false,
previewValue
previewValue,
}: SliderInputProps) {
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 = value !== null && value !== undefined
? Math.max(min, Math.min(max, value))
: defaultValue
const normalizedValue =
value !== null && value !== undefined ? Math.max(min, Math.min(max, value)) : defaultValue
const displayValue = normalizedValue ?? defaultValue
@@ -53,20 +51,29 @@ export function SliderInput({
}
return (
<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 className='relative pt-2 pb-6'>
<Slider
value={[normalizedValue]}
min={min}
max={max}
step={integer ? 1 : step}
onValueChange={(value) => setStoreValue(integer ? Math.round(value[0]) : value[0])}
disabled={isPreview}
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>
</div>
)
@@ -12,7 +12,6 @@ import { Label } from '@/components/ui/label'
import { cn } from '@/lib/utils'
import { useSubBlockValue } from '../../hooks/use-sub-block-value'
interface InputField {
id: string
name: string
@@ -35,14 +34,14 @@ const DEFAULT_FIELD: InputField = {
collapsed: true,
}
export function InputFormat({
blockId,
export function InputFormat({
blockId,
subBlockId,
isPreview = false,
previewValue
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]
@@ -50,7 +49,7 @@ export function InputFormat({
// Field operations
const addField = () => {
if (isPreview) return
const newField: InputField = {
...DEFAULT_FIELD,
id: crypto.randomUUID(),
@@ -71,7 +70,9 @@ export function InputFormat({
const toggleCollapse = (id: string) => {
if (isPreview) return
setStoreValue(fields.map((f: InputField) => (f.id === id ? { ...f, collapsed: !f.collapsed } : f)))
setStoreValue(
fields.map((f: InputField) => (f.id === id ? { ...f, collapsed: !f.collapsed } : f))
)
}
// Field header
@@ -98,10 +99,16 @@ export function InputFormat({
</Badge>
)}
</div>
<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>
<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
@@ -109,7 +116,7 @@ export function InputFormat({
size='icon'
onClick={() => removeField(field.id)}
disabled={isPreview || fields.length === 1}
className="h-6 w-6 rounded-full text-destructive hover:text-destructive"
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>
@@ -148,9 +155,9 @@ export function InputFormat({
name='name'
value={field.name}
onChange={(e) => updateField(field.id, 'name', e.target.value)}
placeholder="firstName"
placeholder='firstName'
disabled={isPreview}
className="h-9 placeholder:text-muted-foreground/50"
className='h-9 placeholder:text-muted-foreground/50'
/>
</div>
@@ -158,8 +165,12 @@ export function InputFormat({
<Label className='text-xs'>Type</Label>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" disabled={isPreview} className="w-full justify-between h-9 font-normal">
<div className="flex items-center">
<Button
variant='outline'
disabled={isPreview}
className='h-9 w-full justify-between font-normal'
>
<div className='flex items-center'>
<span>{field.type}</span>
</div>
<ChevronDown className='h-4 w-4 opacity-50' />
@@ -1,4 +1,3 @@
import { useEffect } from 'react'
import { Label } from '@/components/ui/label'
import { Switch as UISwitch } from '@/components/ui/switch'
import { useSubBlockValue } from '../hooks/use-sub-block-value'
@@ -12,18 +11,18 @@ interface SwitchProps {
previewValue?: boolean | null
}
export function Switch({
blockId,
subBlockId,
title,
export function Switch({
blockId,
subBlockId,
title,
value: propValue,
isPreview = false,
previewValue
previewValue,
}: SwitchProps) {
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 value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue
const handleChange = (checked: boolean) => {
// Only update store when not in preview mode
@@ -33,16 +32,16 @@ export function Switch({
}
return (
<div className="flex items-center space-x-3">
<UISwitch
<div className='flex items-center space-x-3'>
<UISwitch
id={`${blockId}-${subBlockId}`}
checked={Boolean(value)}
onCheckedChange={handleChange}
disabled={isPreview}
/>
<Label
<Label
htmlFor={`${blockId}-${subBlockId}`}
className="text-sm font-normal leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
className='cursor-pointer font-normal text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
>
{title}
</Label>
@@ -8,7 +8,6 @@ import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
import { cn } from '@/lib/utils'
import { useSubBlockValue } from '../hooks/use-sub-block-value'
interface TableProps {
blockId: string
subBlockId: string
@@ -22,15 +21,15 @@ interface TableRow {
cells: Record<string, string>
}
export function Table({
blockId,
subBlockId,
export function Table({
blockId,
subBlockId,
columns,
isPreview = false,
previewValue
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
@@ -84,7 +83,7 @@ export function Table({
const handleCellChange = (rowIndex: number, column: string, value: string) => {
if (isPreview) return
const updatedRows = [...rows].map((row, idx) =>
idx === rowIndex
? {
@@ -187,7 +186,7 @@ export function Table({
}
}}
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"
className='w-full border-0 text-transparent caret-foreground placeholder:text-muted-foreground/50 focus-visible:ring-0 focus-visible:ring-offset-0'
/>
<div
data-overlay={cellKey}
@@ -201,8 +200,9 @@ export function Table({
}
const renderDeleteButton = (rowIndex: number) =>
rows.length > 1 && !isPreview && (
<td className="w-0 p-0">
rows.length > 1 &&
!isPreview && (
<td className='w-0 p-0'>
<Button
variant='ghost'
size='icon'
@@ -8,7 +8,6 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
import { cn } from '@/lib/utils'
import { useSubBlockValue } from '../hooks/use-sub-block-value'
interface TimeInputProps {
blockId: string
subBlockId: string
@@ -18,16 +17,16 @@ interface TimeInputProps {
className?: string
}
export function TimeInput({
blockId,
subBlockId,
export function TimeInput({
blockId,
subBlockId,
placeholder,
isPreview = false,
previewValue,
className,
className,
}: TimeInputProps) {
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)
@@ -55,8 +54,8 @@ export function TimeInput({
// Update the time when any component changes
const updateTime = (newHour?: string, newMinute?: string, newAmpm?: 'AM' | 'PM') => {
if (isPreview) return
const h = parseInt(newHour ?? hour) || 12
const m = parseInt(newMinute ?? minute) || 0
const h = Number.parseInt(newHour ?? hour) || 12
const m = Number.parseInt(newMinute ?? minute) || 0
const p = newAmpm ?? ampm
setStoreValue(formatStorageTime(h, m, p))
}
@@ -91,7 +90,7 @@ export function TimeInput({
>
<PopoverTrigger asChild>
<Button
variant="outline"
variant='outline'
disabled={isPreview}
className={cn(
'w-full justify-start text-left font-normal',
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useState } from 'react'
import { PlusIcon, WrenchIcon, XIcon } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
@@ -27,8 +27,6 @@ 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
@@ -278,7 +276,7 @@ export function ToolInput({
blockId,
subBlockId,
isPreview = false,
previewValue
previewValue,
}: ToolInputProps) {
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
const [open, setOpen] = useState(false)
@@ -604,50 +602,115 @@ export function ToolInput({
</div>
</div>
</PopoverTrigger>
<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
<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'
onSelect={() => {
if (!isPreview) {
setCustomToolModalOpen(true)
setOpen(false)
}
}}
className='mb-1 flex cursor-pointer items-center gap-2'
disabled={isPreview}
>
<div className="flex items-center gap-2">
<Plus className="h-4 w-4" />
<span>Create Custom Tool</span>
<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>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
<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) {
setStoreValue([
...selectedTools.map((tool, index) => ({
...tool,
isExpanded:
Math.floor(selectedTools.length / 2) ===
Math.floor(index / 2),
})),
newTool,
])
} else {
setStoreValue([
...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>
</PopoverContent>
</Popover>
) : (
@@ -295,12 +295,12 @@ interface WebhookConfigProps {
}
}
export function WebhookConfig({
blockId,
subBlockId,
isConnecting,
export function WebhookConfig({
blockId,
subBlockId,
isConnecting,
isPreview = false,
value: propValue
value: propValue,
}: WebhookConfigProps) {
const [isModalOpen, setIsModalOpen] = useState(false)
const [isSaving, setIsSaving] = useState(false)
@@ -323,7 +323,7 @@ export function WebhookConfig({
// Store provider-specific configuration
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
@@ -570,7 +570,7 @@ export function WebhookConfig({
'https://www.googleapis.com/auth/gmail.modify',
'https://www.googleapis.com/auth/gmail.labels',
]}
label="Select Gmail account"
label='Select Gmail account'
disabled={isConnecting || isSaving || isDeleting || isPreview}
/>
</div>
@@ -153,8 +153,8 @@ function storeApiKeyValue(
export function useSubBlockValue<T = any>(
blockId: string,
subBlockId: string,
triggerWorkflowUpdate: boolean = false,
isPreview: boolean = false,
triggerWorkflowUpdate = false,
isPreview = false,
directValue?: T
): readonly [T | null, (value: T) => void] {
const blockType = useWorkflowStore(
@@ -300,4 +300,4 @@ export function useSubBlockValue<T = any>(
}, [storeValue, initialValue])
return [valueRef.current as T | null, setValue] as const
}
}
@@ -1,10 +1,10 @@
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'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import { CheckboxList } from './components/checkbox-list'
import { Code } from './components/code'
import { ConditionInput } from './components/condition-input'
@@ -35,12 +35,12 @@ interface SubBlockProps {
subBlockValues?: Record<string, any>
}
export function SubBlock({
blockId,
config,
export function SubBlock({
blockId,
config,
isConnecting,
isPreview = false,
subBlockValues
subBlockValues,
}: SubBlockProps) {
const handleMouseDown = (e: React.MouseEvent) => {
e.stopPropagation()
@@ -66,7 +66,7 @@ export function SubBlock({
const renderInput = () => {
const previewValue = getPreviewValue()
switch (config.type) {
case 'short-input':
return (
@@ -121,13 +121,15 @@ export function SubBlock({
/>
)
case 'table':
return <Table
blockId={blockId}
subBlockId={config.id}
columns={config.columns ?? []}
isPreview={isPreview}
previewValue={previewValue}
/>
return (
<Table
blockId={blockId}
subBlockId={config.id}
columns={config.columns ?? []}
isPreview={isPreview}
previewValue={previewValue}
/>
)
case 'code':
return (
<Code
@@ -142,20 +144,24 @@ export function SubBlock({
/>
)
case 'switch':
return <Switch
blockId={blockId}
subBlockId={config.id}
title={config.title ?? ''}
isPreview={isPreview}
previewValue={previewValue}
/>
return (
<Switch
blockId={blockId}
subBlockId={config.id}
title={config.title ?? ''}
isPreview={isPreview}
previewValue={previewValue}
/>
)
case 'tool-input':
return <ToolInput
blockId={blockId}
subBlockId={config.id}
isPreview={isPreview}
previewValue={previewValue}
/>
return (
<ToolInput
blockId={blockId}
subBlockId={config.id}
isPreview={isPreview}
previewValue={previewValue}
/>
)
case 'checkbox-list':
return (
<CheckboxList
@@ -170,26 +176,28 @@ export function SubBlock({
)
case 'condition-input':
return (
<ConditionInput
blockId={blockId}
subBlockId={config.id}
<ConditionInput
blockId={blockId}
subBlockId={config.id}
isConnecting={isConnecting}
isPreview={isPreview}
previewValue={previewValue}
/>
)
case 'eval-input':
return <EvalInput
blockId={blockId}
subBlockId={config.id}
isPreview={isPreview}
previewValue={previewValue}
/>
return (
<EvalInput
blockId={blockId}
subBlockId={config.id}
isPreview={isPreview}
previewValue={previewValue}
/>
)
case 'date-input':
return (
<DateInput
blockId={blockId}
subBlockId={config.id}
<DateInput
blockId={blockId}
subBlockId={config.id}
placeholder={config.placeholder}
isPreview={isPreview}
previewValue={previewValue}
@@ -197,9 +205,9 @@ export function SubBlock({
)
case 'time-input':
return (
<TimeInput
blockId={blockId}
subBlockId={config.id}
<TimeInput
blockId={blockId}
subBlockId={config.id}
placeholder={config.placeholder}
isPreview={isPreview}
previewValue={previewValue}
@@ -217,28 +225,32 @@ export function SubBlock({
previewValue={previewValue}
/>
)
case 'webhook-config':
case 'webhook-config': {
// For webhook config, we need to construct the value from multiple subblock values
const webhookValue = isPreview && subBlockValues ? {
webhookProvider: subBlockValues['webhookProvider']?.value,
webhookPath: subBlockValues['webhookPath']?.value,
providerConfig: subBlockValues['providerConfig']?.value,
} : previewValue
const webhookValue =
isPreview && subBlockValues
? {
webhookProvider: subBlockValues.webhookProvider?.value,
webhookPath: subBlockValues.webhookPath?.value,
providerConfig: subBlockValues.providerConfig?.value,
}
: previewValue
return (
<WebhookConfig
blockId={blockId}
subBlockId={config.id}
<WebhookConfig
blockId={blockId}
subBlockId={config.id}
isConnecting={isConnecting}
isPreview={isPreview}
value={webhookValue}
/>
)
}
case 'schedule-config':
return (
<ScheduleConfig
blockId={blockId}
subBlockId={config.id}
<ScheduleConfig
blockId={blockId}
subBlockId={config.id}
isConnecting={isConnecting}
isPreview={isPreview}
previewValue={previewValue}
@@ -247,7 +259,9 @@ export function SubBlock({
case 'oauth-input':
return (
<CredentialSelector
value={isPreview ? (previewValue || '') : (typeof config.value === 'string' ? config.value : '')}
value={
isPreview ? previewValue || '' : typeof config.value === 'string' ? config.value : ''
}
onChange={(value) => {
// Only allow changes in non-preview mode
if (!isPreview) {
@@ -269,36 +283,44 @@ export function SubBlock({
/>
)
case 'file-selector':
return <FileSelectorInput
blockId={blockId}
subBlock={config}
disabled={isConnecting || isPreview}
isPreview={isPreview}
previewValue={previewValue}
/>
return (
<FileSelectorInput
blockId={blockId}
subBlock={config}
disabled={isConnecting || isPreview}
isPreview={isPreview}
previewValue={previewValue}
/>
)
case 'project-selector':
return <ProjectSelectorInput
blockId={blockId}
subBlock={config}
disabled={isConnecting || isPreview}
isPreview={isPreview}
previewValue={previewValue}
/>
return (
<ProjectSelectorInput
blockId={blockId}
subBlock={config}
disabled={isConnecting || isPreview}
isPreview={isPreview}
previewValue={previewValue}
/>
)
case 'folder-selector':
return <FolderSelectorInput
blockId={blockId}
subBlock={config}
disabled={isConnecting || isPreview}
isPreview={isPreview}
previewValue={previewValue}
/>
return (
<FolderSelectorInput
blockId={blockId}
subBlock={config}
disabled={isConnecting || isPreview}
isPreview={isPreview}
previewValue={previewValue}
/>
)
case 'input-format':
return <InputFormat
blockId={blockId}
subBlockId={config.id}
isPreview={isPreview}
previewValue={previewValue}
/>
return (
<InputFormat
blockId={blockId}
subBlockId={config.id}
isPreview={isPreview}
previewValue={previewValue}
/>
)
default:
return <div>Unknown input type: {config.type}</div>
}
@@ -343,4 +365,4 @@ export function SubBlock({
{renderInput()}
</div>
)
}
}
@@ -32,14 +32,20 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
// State management
const [isConnecting, setIsConnecting] = useState(false)
const [isEditing, setIsEditing] = useState(false)
const [editedName, setEditedName] = useState('')
const [isLoadingScheduleInfo, setIsLoadingScheduleInfo] = useState(false)
const [scheduleInfo, setScheduleInfo] = useState<{
scheduleTiming: string
nextRunAt: string | null
lastRanAt: string | null
timezone: string
status?: string
isDisabled?: boolean
id?: string
} | null>(null)
const [isLoadingWebhookInfo, setIsLoadingWebhookInfo] = useState(false)
const [webhookInfo, setWebhookInfo] = useState<{
webhookPath: string
provider: string
@@ -59,8 +65,9 @@ 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)
@@ -71,47 +78,97 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
const isActiveBlock = useExecutionStore((state) => state.activeBlockIds.has(id))
const isActive = dataIsActive || isActiveBlock
// Get schedule information for the tooltip
useEffect(() => {
if (type === 'starter' && hasActiveSchedule) {
const fetchScheduleInfo = async () => {
try {
const workflowId = useWorkflowRegistry.getState().activeWorkflowId
if (!workflowId) return
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' }),
})
const response = await fetch(`/api/schedules?workflowId=${workflowId}&mode=schedule`, {
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache',
},
})
if (response.ok) {
fetchScheduleInfo()
} else {
console.error('Failed to reactivate schedule')
}
} catch (error) {
console.error('Error reactivating schedule:', error)
}
}
if (response.ok) {
const data = await response.json()
if (data.schedule) {
let scheduleTiming = 'Unknown schedule'
if (data.schedule.cronExpression) {
scheduleTiming = parseCronToHumanReadable(data.schedule.cronExpression)
}
const fetchScheduleInfo = async () => {
try {
setIsLoadingScheduleInfo(true)
const workflowId = useWorkflowRegistry.getState().activeWorkflowId
if (!workflowId) return
setScheduleInfo({
scheduleTiming,
nextRunAt: data.schedule.nextRunAt,
lastRanAt: data.schedule.lastRanAt,
timezone: data.schedule.timezone || 'UTC',
})
}
}
} catch (error) {
console.error('Error fetching schedule info:', error)
}
const response = await fetch(`/api/schedules?workflowId=${workflowId}&mode=schedule`, {
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache',
},
})
if (!response.ok) {
setScheduleInfo(null)
return
}
fetchScheduleInfo()
} else if (!hasActiveSchedule) {
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)
}
}, [type, hasActiveSchedule])
}
// Get webhook information for the tooltip
useEffect(() => {
@@ -202,7 +259,7 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
// Get the appropriate state for conditional evaluation
let stateToUse: Record<string, any> = {}
if (data.isPreview && data.subBlockValues) {
// In preview mode, use the preview values
stateToUse = data.subBlockValues
@@ -315,7 +372,6 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
// 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 => {
@@ -332,6 +388,8 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
return providers[providerId] || 'Webhook'
}
const shouldShowScheduleBadge = isStarterBlock && !isLoadingScheduleInfo && scheduleInfo !== null
return (
<div className='group relative'>
<Card
@@ -437,7 +495,7 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
</Badge>
)}
{/* Schedule indicator badge - displayed for starter blocks with active schedules */}
{showScheduleIndicator && (
{shouldShowScheduleBadge && (
<Tooltip>
<TooltipTrigger asChild>
<Badge
@@ -650,9 +708,9 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
key={`${id}-${rowIndex}-${blockIndex}`}
className={cn('space-y-1', subBlock.layout === 'half' ? 'flex-1' : 'w-full')}
>
<SubBlock
blockId={id}
config={subBlock}
<SubBlock
blockId={id}
config={subBlock}
isConnecting={isConnecting}
isPreview={data.isPreview}
subBlockValues={data.subBlockValues}
@@ -740,4 +798,4 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
</Card>
</div>
)
}
}
@@ -54,7 +54,6 @@ export function useDeploymentChangeDetection(activeWorkflowId: string | null, is
// Always update the needsRedeployment flag based on API response to handle both true and false
// This ensures it's updated when changes are detected and when changes are no longer detected
if (data.needsRedeployment) {
// Update local state
setNeedsRedeployment(true)
@@ -66,7 +65,6 @@ export function useDeploymentChangeDetection(activeWorkflowId: string | null, is
.getState()
.getWorkflowDeploymentStatus(requestedWorkflowId)
if (currentStatus?.needsRedeployment) {
// Update local state
setNeedsRedeployment(false)
@@ -7,27 +7,19 @@ import ReactFlow, {
ConnectionLineType,
type Edge,
type EdgeTypes,
Handle,
type Node,
type NodeProps,
type NodeTypes,
Position,
ReactFlowProvider,
} from 'reactflow'
import 'reactflow/dist/style.css'
import { Card } from '@/components/ui/card'
import { Label } from '@/components/ui/label'
import { createLogger } from '@/lib/logs/console-logger'
import { cn } from '@/lib/utils'
import { LoopTool } from '@/app/w/[id]/components/loop-node/loop-config'
import { createLogger } from '@/lib/logs/console-logger'
import { WorkflowBlock } from '@/app/w/[id]/components/workflow-block/workflow-block'
import { WorkflowEdge } from '@/app/w/[id]/components/workflow-edge/workflow-edge'
// import { LoopInput } from '@/app/w/[id]/components/workflow-loop/components/loop-input/loop-input'
// 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 { WorkflowState } from '@/stores/workflows/workflow/types'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
const logger = createLogger('WorkflowPreview')
@@ -68,20 +60,37 @@ export function WorkflowPreview({
defaultZoom,
}: WorkflowPreviewProps) {
// Track structure changes efficiently
const blocksStructure = useMemo(() => ({
count: Object.keys(workflowState.blocks || {}).length,
ids: Object.keys(workflowState.blocks || {}).join(',')
}), [workflowState.blocks]);
const loopsStructure = useMemo(() => ({
count: Object.keys(workflowState.loops || {}).length,
ids: Object.keys(workflowState.loops || {}).join(',')
}), [workflowState.loops]);
const edgesStructure = useMemo(() => ({
count: workflowState.edges.length,
ids: workflowState.edges.map(e => e.id).join(',')
}), [workflowState.edges]);
const blocksStructure = useMemo(
() => ({
count: Object.keys(workflowState.blocks || {}).length,
ids: Object.keys(workflowState.blocks || {}).join(','),
}),
[workflowState.blocks]
)
const loopsStructure = useMemo(
() => ({
count: Object.keys(workflowState.loops || {}).length,
ids: Object.keys(workflowState.loops || {}).join(','),
}),
[workflowState.loops]
)
const parallelsStructure = useMemo(
() => ({
count: Object.keys(workflowState.parallels || {}).length,
ids: Object.keys(workflowState.parallels || {}).join(','),
}),
[workflowState.parallels]
)
const edgesStructure = useMemo(
() => ({
count: workflowState.edges.length,
ids: workflowState.edges.map((e) => e.id).join(','),
}),
[workflowState.edges]
)
// Transform blocks and loops into ReactFlow nodes
const nodes: Node[] = useMemo(() => {
@@ -105,18 +114,18 @@ export function WorkflowPreview({
// Add block nodes using the same approach as workflow.tsx
Object.entries(workflowState.blocks).forEach(([blockId, block]) => {
if (!block || !block.type) {
logger.warn(`Skipping invalid block: ${blockId}`);
return;
logger.warn(`Skipping invalid block: ${blockId}`)
return
}
const blockConfig = getBlock(block.type)
if (!blockConfig) {
logger.error(`No configuration found for block type: ${block.type}`, { blockId })
return;
return
}
// Create a deep clone of subBlocks to avoid any references to the original state
const subBlocksClone = block.subBlocks ? cloneDeep(block.subBlocks) : {};
const subBlocksClone = block.subBlocks ? cloneDeep(block.subBlocks) : {}
nodeArray.push({
id: blockId,
@@ -166,15 +175,22 @@ export function WorkflowPreview({
})
})
}
logger.info(`Preview node created: ${blockId}`, {
logger.info(`Preview node created: ${blockId}`, {
blockType: block.type,
hasSubBlocks: block.subBlocks && Object.keys(block.subBlocks).length > 0
});
hasSubBlocks: block.subBlocks && Object.keys(block.subBlocks).length > 0,
})
})
return nodeArray
}, [blocksStructure, loopsStructure, showSubBlocks, workflowState.blocks, workflowState.loops])
}, [
blocksStructure,
loopsStructure,
parallelsStructure,
showSubBlocks,
workflowState.blocks,
workflowState.loops,
workflowState.parallels,
])
// Transform edges
const edges: Edge[] = useMemo(() => {
@@ -186,14 +202,11 @@ export function WorkflowPreview({
targetHandle: edge.targetHandle,
type: 'workflowEdge',
}))
}, [edgesStructure, workflowState.edges])
}, [edgesStructure, workflowState.edges, workflowState.parallels])
return (
<ReactFlowProvider>
<div
style={{ height, width }}
className={cn('preview-mode')}
>
<div style={{ height, width }} className={cn('preview-mode')}>
<ReactFlow
nodes={nodes}
edges={edges}
@@ -4,9 +4,9 @@ import { useEffect, useState } from 'react'
import { Eye } from 'lucide-react'
import { useRouter } from 'next/navigation'
import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { WorkflowPreview } from '@/app/w/components/workflow-preview/workflow-preview'
import { Workflow } from '../marketplace'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import type { Workflow } from '../marketplace'
/**
* WorkflowCardProps interface - defines the properties for the WorkflowCard component
@@ -96,7 +96,12 @@ export function WorkflowCard({ workflow, onHover }: WorkflowCardProps) {
// Interactive Preview
<div className='absolute inset-0 flex items-center justify-center'>
<div className='h-full w-full scale-[0.9] transform-gpu'>
<WorkflowPreview workflowState={workflow.workflowState} />
<WorkflowPreview
workflowState={{
...workflow.workflowState,
parallels: workflow.workflowState.parallels || {},
}}
/>
</div>
</div>
) : workflow.thumbnail ? (
@@ -30,6 +30,7 @@ export interface Workflow {
targetHandle?: string
}>
loops: Record<string, any>
parallels?: Record<string, any>
}
}
@@ -54,6 +55,7 @@ export interface MarketplaceWorkflow {
targetHandle?: string
}>
loops: Record<string, any>
parallels?: Record<string, any>
}
}
+9 -3
View File
@@ -1,6 +1,6 @@
import { Edge } from 'reactflow'
import { BlockOutput, SubBlockType } from '@/blocks/types'
import { DeploymentStatus } from '../registry/types'
import type { Edge } from 'reactflow'
import type { BlockOutput, SubBlockType } from '@/blocks/types'
import type { DeploymentStatus } from '../registry/types'
export interface Position {
x: number
@@ -64,6 +64,12 @@ 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 WorkflowState {
blocks: Record<string, BlockState>
edges: Edge[]