fix: good except for subblocks

This commit is contained in:
Adam Gough
2025-05-15 18:51:50 -07:00
parent d9e99a4fab
commit dc1433eecf
9 changed files with 23712 additions and 532 deletions

View File

@@ -6,7 +6,7 @@ import { workflow } from '@/db/schema'
import { validateWorkflowAccess } from '../../middleware'
import { createErrorResponse, createSuccessResponse } from '../../utils'
const logger = createLogger('WorkflowDeployedAPI')
const logger = createLogger('WorkflowDeployedStateAPI')
export const dynamic = 'force-dynamic'
export const runtime = 'nodejs'
@@ -24,7 +24,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
return createErrorResponse(validation.error.message, validation.error.status)
}
// Fetch just the deployed state
// Fetch the workflow's deployed state
const result = await db
.select({
deployedState: workflow.deployedState,
@@ -46,17 +46,16 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
logger.info(`[${requestId}] No deployed state available for workflow: ${id}`)
return createSuccessResponse({
deployedState: null,
isDeployed: false,
message: 'Workflow is not deployed or has no deployed state',
})
}
logger.info(`[${requestId}] Successfully retrieved DEPLOYED state: ${id}`)
logger.info(`[${requestId}] Successfully retrieved deployed state for: ${id}`)
return createSuccessResponse({
deployedState: workflowData.deployedState,
isDeployed: true,
})
} catch (error: any) {
logger.error(`[${requestId}] Error fetching deployed state: ${id}`, error)
return createErrorResponse(error.message || 'Failed to fetch deployed state', 500)
}
}
}

View File

@@ -21,9 +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'
const logger = createLogger('DeploymentInfo')
interface DeploymentInfoProps {
isLoading: boolean
isLoading?: boolean
deploymentInfo: {
isDeployed: boolean
deployedAt?: string
@@ -36,7 +39,10 @@ interface DeploymentInfoProps {
onUndeploy: () => void
isSubmitting: boolean
isUndeploying: boolean
workflowId?: string
needsRedeployment: boolean
workflowId: string | null
deployedState: any
isLoadingDeployedState: boolean
}
export function DeploymentInfo({
@@ -46,10 +52,12 @@ export function DeploymentInfo({
onUndeploy,
isSubmitting,
isUndeploying,
needsRedeployment,
workflowId,
deployedState,
isLoadingDeployedState
}: DeploymentInfoProps) {
const [isViewingDeployed, setIsViewingDeployed] = useState(false)
const [deployedWorkflowState, setDeployedWorkflowState] = useState<any>(null)
const { addNotification } = useNotificationStore()
const handleViewDeployed = async () => {
@@ -57,29 +65,15 @@ export function DeploymentInfo({
addNotification('error', 'Cannot view deployment: Workflow ID is missing', null)
return
}
try {
const response = await fetch(`/api/workflows/${workflowId}/deployed`)
if (!response.ok) {
throw new Error('Failed to fetch deployed workflow')
}
const data = await response.json()
if (data?.deployedState) {
setDeployedWorkflowState(data.deployedState)
setIsViewingDeployed(true)
} else {
addNotification('error', 'Failed to view deployment: No deployment state found', workflowId)
}
} catch (error) {
console.error('Error fetching deployed workflow:', error)
addNotification(
'error',
`Failed to fetch deployed workflow: ${(error as Error).message}`,
workflowId
)
// If deployedState is already loaded, use it directly
if (deployedState) {
logger.info(`Using cached deployed state for workflow: ${workflowId}`)
setIsViewingDeployed(true)
return
} else {
logger.debug(`[${workflowId}] No deployed state found`)
addNotification('error', 'Cannot view deployment: No deployed state available', workflowId)
}
}
@@ -168,11 +162,11 @@ export function DeploymentInfo({
</div>
</div>
{deployedWorkflowState && (
{deployedState && (
<DeployedWorkflowModal
isOpen={isViewingDeployed}
onClose={() => setIsViewingDeployed(false)}
deployedWorkflowState={deployedWorkflowState}
deployedWorkflowState={deployedState}
/>
)}
</>

View File

@@ -36,6 +36,8 @@ interface DeployModalProps {
workflowId: string | null
needsRedeployment: boolean
setNeedsRedeployment: (value: boolean) => void
deployedState: any
isLoadingDeployedState: boolean
}
interface ApiKey {
@@ -69,6 +71,8 @@ export function DeployModal({
workflowId,
needsRedeployment,
setNeedsRedeployment,
deployedState,
isLoadingDeployedState,
}: DeployModalProps) {
// Store hooks
const { addNotification } = useNotificationStore()
@@ -604,40 +608,46 @@ export function DeployModal({
</div>
</div>
<div className='flex-1 overflow-y-auto'>
<div className='p-6'>
{activeTab === 'api' &&
(isDeployed ? (
<DeploymentInfo
isLoading={isLoading}
deploymentInfo={deploymentInfo}
onRedeploy={handleRedeploy}
onUndeploy={handleUndeploy}
isSubmitting={isSubmitting}
isUndeploying={isUndeploying}
workflowId={workflowId || undefined}
/>
) : (
<>
{apiDeployError && (
<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 className="flex-1 overflow-y-auto">
<div className="p-6">
{activeTab === 'api' && (
<>
{isDeployed ? (
<DeploymentInfo
isLoading={isLoading}
deploymentInfo={deploymentInfo}
onRedeploy={handleRedeploy}
onUndeploy={handleUndeploy}
isSubmitting={isSubmitting}
isUndeploying={isUndeploying}
needsRedeployment={needsRedeployment}
workflowId={workflowId}
deployedState={deployedState}
isLoadingDeployedState={isLoadingDeployedState}
/>
) : (
<>
{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>{apiDeployError}</div>
</div>
)}
<div className="px-1 -mx-1">
<DeployForm
apiKeys={apiKeys}
keysLoaded={keysLoaded}
endpointUrl={`${env.NEXT_PUBLIC_APP_URL}/api/workflows/${workflowId}/execute`}
workflowId={workflowId || ''}
onSubmit={onDeploy}
getInputFormatExample={getInputFormatExample}
onApiKeyCreated={fetchApiKeys}
/>
</div>
)}
<div className='-mx-1 px-1'>
<DeployForm
apiKeys={apiKeys}
keysLoaded={keysLoaded}
endpointUrl={`${env.NEXT_PUBLIC_APP_URL}/api/workflows/${workflowId}/execute`}
workflowId={workflowId || ''}
onSubmit={onDeploy}
getInputFormatExample={getInputFormatExample}
onApiKeyCreated={fetchApiKeys}
/>
</div>
</>
))}
</>
)}
</>
)}
{activeTab === 'chat' && (
<ChatDeploy

View File

@@ -64,6 +64,7 @@ export function DeployedWorkflowCard({
<div className='h-[500px] w-full'>
{workflowToShow ? (
<WorkflowPreview
key={showingDeployed ? 'deployed-preview' : 'current-preview'}
workflowState={workflowToShow}
showSubBlocks={true}
height='100%'

View File

@@ -1,6 +1,6 @@
'use client'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import {
AlertDialog,
AlertDialogAction,
@@ -13,12 +13,25 @@ import {
AlertDialogTrigger,
} from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
<<<<<<< HEAD
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
=======
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { createLogger } from '@/lib/logs/console-logger'
>>>>>>> 2e4f4a91 (fix: good except for subblocks)
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'
const logger = createLogger('DeployedWorkflowModal')
interface DeployedWorkflowModalProps {
isOpen: boolean
onClose: () => void
@@ -36,9 +49,21 @@ export function DeployedWorkflowModal({
deployedWorkflowState,
}: DeployedWorkflowModalProps) {
const [showRevertDialog, setShowRevertDialog] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const { revertToDeployedState } = useWorkflowStore()
const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
// Add debug logging to check deployedWorkflowState
useEffect(() => {
if (isOpen) {
if (deployedWorkflowState) {
logger.info(`DeployedWorkflowModal received state with ${Object.keys(deployedWorkflowState.blocks || {}).length} blocks`)
} else {
logger.warn('DeployedWorkflowModal opened but deployedWorkflowState is null or undefined')
}
}
}, [isOpen, deployedWorkflowState]);
// Get current workflow state to compare with deployed state
const currentWorkflowState = useWorkflowStore((state) => ({
blocks: activeWorkflowId ? mergeSubblockState(state.blocks, activeWorkflowId) : state.blocks,
@@ -48,10 +73,19 @@ export function DeployedWorkflowModal({
}))
const handleRevert = () => {
<<<<<<< HEAD
// Revert to the deployed state
revertToDeployedState(deployedWorkflowState)
setShowRevertDialog(false)
onClose()
=======
if (activeWorkflowId) {
logger.info(`Reverting to deployed state for workflow: ${activeWorkflowId}`)
revertToDeployedState(deployedWorkflowState)
setShowRevertDialog(false)
onClose()
}
>>>>>>> 9594f7db (fix: good except for subblocks)
}
return (
@@ -67,10 +101,16 @@ export function DeployedWorkflowModal({
</DialogHeader>
</div>
<DeployedWorkflowCard
currentWorkflowState={currentWorkflowState}
deployedWorkflowState={deployedWorkflowState}
/>
{isLoading ? (
<div className="flex justify-center items-center h-[500px]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
) : (
<DeployedWorkflowCard
currentWorkflowState={currentWorkflowState}
deployedWorkflowState={deployedWorkflowState}
/>
)}
<div className='mt-6 flex justify-between'>
<AlertDialog open={showRevertDialog} onOpenChange={setShowRevertDialog}>

View File

@@ -15,12 +15,16 @@ interface DeploymentControlsProps {
activeWorkflowId: string | null
needsRedeployment: boolean
setNeedsRedeployment: (value: boolean) => void
deployedState: any
isLoadingDeployedState: boolean
}
export function DeploymentControls({
activeWorkflowId,
needsRedeployment,
setNeedsRedeployment,
deployedState,
isLoadingDeployedState,
}: DeploymentControlsProps) {
// Use workflow-specific deployment status
const deploymentStatus = useWorkflowRegistry((state) =>
@@ -100,6 +104,8 @@ export function DeploymentControls({
workflowId={activeWorkflowId}
needsRedeployment={workflowNeedsRedeployment}
setNeedsRedeployment={setNeedsRedeployment}
deployedState={deployedState}
isLoadingDeployedState={isLoadingDeployedState}
/>
</>
)

View File

@@ -107,6 +107,10 @@ export function ControlBar() {
const [mounted, setMounted] = useState(false)
const [, forceUpdate] = useState({})
// Add deployedState management
const [deployedState, setDeployedState] = useState<any>(null)
const [isLoadingDeployedState, setIsLoadingDeployedState] = useState<boolean>(false)
// Workflow name editing state
const [isEditing, setIsEditing] = useState(false)
const [editedName, setEditedName] = useState('')
@@ -534,6 +538,8 @@ export function ControlBar() {
activeWorkflowId={activeWorkflowId}
needsRedeployment={needsRedeployment}
setNeedsRedeployment={setNeedsRedeployment}
deployedState={deployedState}
isLoadingDeployedState={isLoadingDeployedState}
/>
)
@@ -995,6 +1001,63 @@ export function ControlBar() {
</div>
)
// Fetch deployed state when the workflow ID changes or deployment status changes
useEffect(() => {
async function fetchDeployedState() {
if (!activeWorkflowId || !isDeployed) {
setDeployedState(null)
return
}
try {
setIsLoadingDeployedState(true)
logger.info(`[CENTRAL] Fetching deployed state for workflow: ${activeWorkflowId} (Control Bar - Single Source of Truth)`)
const response = await fetch(`/api/workflows/${activeWorkflowId}/deployed`)
if (!response.ok) {
throw new Error(`Failed to fetch deployed state: ${response.status}`)
}
const data = await response.json()
if (data.deployedState) {
logger.info('Successfully fetched deployed state from DB - This is the only place that should fetch deployed state')
// Create a deep clone to ensure no reference sharing with current state
const deepClonedState = JSON.parse(JSON.stringify(data.deployedState))
setDeployedState(deepClonedState)
} else {
logger.warn('No deployed state found in the database')
setDeployedState(null)
}
} catch (error) {
logger.error('Error fetching deployed state:', error)
setDeployedState(null)
} finally {
setIsLoadingDeployedState(false)
}
}
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
if (isDeployed) {
setNeedsRedeployment(false)
useWorkflowStore.getState().setNeedsRedeploymentFlag(false)
// When a workflow is newly deployed, we need to fetch the deployed state
if (activeWorkflowId && deployedState === null) {
// We'll fetch the deployed state in the other useEffect
}
} else {
// If workflow is undeployed, clear the deployed state
setDeployedState(null)
}
}, [isDeployed, activeWorkflowId])
return (
<div className='flex h-16 w-full items-center justify-between border-b bg-background'>
{/* Left Section - Workflow Info */}

View File

@@ -19,12 +19,17 @@ import { Card } from '@/components/ui/card'
import { Label } from '@/components/ui/label'
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 type { SubBlockConfig } from '@/blocks/types'
import { useEffect } from 'react'
const logger = createLogger('WorkflowPreview')
interface WorkflowPreviewProps {
// The workflow state to render
@@ -51,13 +56,9 @@ interface WorkflowPreviewProps {
defaultZoom?: number
}
interface ExtendedSubBlockConfig extends SubBlockConfig {
value?: any
}
// Define node types
// Define node types - using the actual workflow components
const nodeTypes: NodeTypes = {
workflowBlock: PreviewWorkflowBlock,
workflowBlock: WorkflowBlock,
// loopLabel: LoopLabel,
// loopInput: LoopInput,
}
@@ -67,455 +68,6 @@ const edgeTypes: EdgeTypes = {
workflowEdge: WorkflowEdge,
}
/**
* Prepares subblocks by combining block state with block configuration
*/
function prepareSubBlocks(blockSubBlocks: Record<string, any>, blockConfig: any) {
const configSubBlocks = blockConfig?.subBlocks || []
return Object.entries(blockSubBlocks)
.map(([id, subBlock]) => {
const matchingConfig = configSubBlocks.find((config: any) => config.id === id)
const value = subBlock.value
const hasValue = value !== undefined && value !== null && value !== ''
if (!hasValue) return null
return {
...matchingConfig,
...subBlock,
id,
}
})
.filter(Boolean)
}
/**
* Groups subblocks into rows for layout
*/
function groupSubBlocks(subBlocks: ExtendedSubBlockConfig[]) {
const rows: ExtendedSubBlockConfig[][] = []
let currentRow: ExtendedSubBlockConfig[] = []
let currentRowWidth = 0
const visibleSubBlocks = subBlocks.filter((block) => !block.hidden)
visibleSubBlocks.forEach((block) => {
const blockWidth = block.layout === 'half' ? 0.5 : 1
if (currentRowWidth + blockWidth > 1) {
if (currentRow.length > 0) {
rows.push([...currentRow])
}
currentRow = [block]
currentRowWidth = blockWidth
} else {
currentRow.push(block)
currentRowWidth += blockWidth
}
})
if (currentRow.length > 0) {
rows.push(currentRow)
}
return rows
}
/**
* PreviewSubBlock component - Renders a simplified version of a subblock input
* @param config - The configuration for the subblock
*/
function PreviewSubBlock({ config }: { config: ExtendedSubBlockConfig }) {
/**
* Renders a simplified input based on the subblock type
* Creates visual representations of different input types
*/
const renderSimplifiedInput = () => {
switch (config.type) {
case 'short-input':
return (
<div className='h-7 rounded-md border border-input bg-background px-3 py-1.5 text-muted-foreground text-xs'>
{config.password
? '**********************'
: config.id === 'providerConfig' && config.value && typeof config.value === 'object'
? Object.keys(config.value).length === 0
? 'Webhook pending configuration'
: 'Webhook configured'
: config.value || config.placeholder || 'Text input'}
</div>
)
case 'long-input':
return (
<div className='h-16 rounded-md border border-input bg-background p-2 text-muted-foreground text-xs'>
{typeof config.value === 'string'
? config.value.length > 50
? `${config.value.substring(0, 50)}...`
: config.value
: config.placeholder || 'Text area'}
</div>
)
case 'dropdown':
return (
<div className='flex h-7 items-center justify-between rounded-md border border-input bg-background px-3 py-1 text-muted-foreground text-xs'>
<span>
{config.value ||
(Array.isArray(config.options) ? config.options[0] : 'Select option')}
</span>
<svg
xmlns='http://www.w3.org/2000/svg'
width='12'
height='12'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className='ml-2'
>
<path d='m6 9 6 6 6-6' />
</svg>
</div>
)
case 'switch':
return (
<div className='flex items-center space-x-2'>
<div
className={`h-4 w-8 rounded-full ${config.value ? 'bg-primary' : 'bg-muted'} flex items-center`}
>
<div
className={`h-3 w-3 rounded-full bg-background transition-all ${config.value ? 'ml-4' : 'ml-0.5'}`}
/>
</div>
<span className='text-xs'>{config.title}</span>
</div>
)
case 'checkbox-list':
return (
<div className='h-7 rounded-md border border-input bg-background px-3 py-1 text-muted-foreground text-xs'>
Checkbox list
</div>
)
case 'code':
return (
<div className='h-12 rounded-md border border-input bg-background p-2 font-mono text-muted-foreground text-xs'>
{typeof config.value === 'string'
? 'Code content'
: config.placeholder || 'Code editor'}
</div>
)
case 'tool-input':
return (
<div className='h-7 rounded-md border border-input bg-background px-3 py-1 text-muted-foreground text-xs'>
Tool configuration
</div>
)
case 'oauth-input':
return (
<div className='flex h-7 items-center justify-between rounded-md border border-input bg-background px-3 py-1 text-muted-foreground text-xs'>
<span>
{config.value ? 'Connected account' : config.placeholder || 'Select account'}
</span>
<svg
xmlns='http://www.w3.org/2000/svg'
width='12'
height='12'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className='ml-2'
>
<path d='m6 9 6 6 6-6' />
</svg>
</div>
)
case 'file-selector':
return (
<div className='flex h-7 items-center justify-between rounded-md border border-input bg-background px-3 py-1 text-muted-foreground text-xs'>
<span>{config.value ? 'File selected' : config.placeholder || 'Select file'}</span>
<svg
xmlns='http://www.w3.org/2000/svg'
width='12'
height='12'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className='ml-2'
>
<path d='m6 9 6 6 6-6' />
</svg>
</div>
)
case 'folder-selector':
return (
<div className='flex h-7 items-center justify-between rounded-md border border-input bg-background px-3 py-1 text-muted-foreground text-xs'>
<span>{config.value ? 'Folder selected' : config.placeholder || 'Select folder'}</span>
<svg
xmlns='http://www.w3.org/2000/svg'
width='12'
height='12'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className='ml-2'
>
<path d='m6 9 6 6 6-6' />
</svg>
</div>
)
case 'project-selector':
return (
<div className='flex h-7 items-center justify-between rounded-md border border-input bg-background px-3 py-1 text-muted-foreground text-xs'>
<span>
{config.value ? 'Project selected' : config.placeholder || 'Select project'}
</span>
<svg
xmlns='http://www.w3.org/2000/svg'
width='12'
height='12'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className='ml-2'
>
<path d='m6 9 6 6 6-6' />
</svg>
</div>
)
case 'condition-input':
return (
<div className='h-16 rounded-md border border-input bg-background p-2 text-muted-foreground text-xs'>
Condition configuration
</div>
)
case 'eval-input':
return (
<div className='h-12 rounded-md border border-input bg-background p-2 font-mono text-muted-foreground text-xs'>
Eval expression
</div>
)
case 'date-input':
return (
<div className='flex h-7 items-center justify-between rounded-md border border-input bg-background px-3 py-1 text-muted-foreground text-xs'>
<span>{config.value || config.placeholder || 'Select date'}</span>
<svg
xmlns='http://www.w3.org/2000/svg'
width='12'
height='12'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className='ml-2'
>
<rect width='18' height='18' x='3' y='4' rx='2' ry='2' />
<line x1='16' x2='16' y1='2' y2='6' />
<line x1='8' x2='8' y1='2' y2='6' />
<line x1='3' x2='21' y1='10' y2='10' />
</svg>
</div>
)
case 'time-input':
return (
<div className='flex h-7 items-center justify-between rounded-md border border-input bg-background px-3 py-1 text-muted-foreground text-xs'>
<span>{config.value || config.placeholder || 'Select time'}</span>
<svg
xmlns='http://www.w3.org/2000/svg'
width='12'
height='12'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className='ml-2'
>
<circle cx='12' cy='12' r='10' />
<polyline points='12 6 12 12 16 14' />
</svg>
</div>
)
case 'file-upload':
return (
<div className='flex h-7 items-center justify-center rounded-md border border-input border-dashed bg-background px-3 py-1 text-muted-foreground text-xs'>
{config.value ? 'File uploaded' : 'Upload file'}
</div>
)
case 'webhook-config':
return (
<div className='h-7 rounded-md border border-input bg-background px-3 py-1 text-muted-foreground text-xs'>
Webhook configuration
</div>
)
case 'schedule-config':
return (
<div className='h-7 rounded-md border border-input bg-background px-3 py-1 text-muted-foreground text-xs'>
Schedule configuration
</div>
)
case 'input-format':
return (
<div className='h-7 rounded-md border border-input bg-background px-3 py-1 text-muted-foreground text-xs'>
Input format configuration
</div>
)
case 'slider':
return (
<div className='h-7 px-1 py-2'>
<div className='relative h-2 w-full rounded-full bg-muted'>
<div
className='absolute h-2 rounded-full bg-primary'
style={{ width: `${((config.value || 50) / 100) * 100}%` }}
/>
<div
className='-translate-x-1/2 -translate-y-1/2 absolute top-1/2 h-4 w-4 rounded-full border-2 border-primary bg-background'
style={{ left: `${((config.value || 50) / 100) * 100}%` }}
/>
</div>
</div>
)
default:
return (
<div className='h-7 rounded-md border border-input bg-background px-3 py-1 text-muted-foreground text-xs'>
{config.value !== undefined ? String(config.value) : config.title || 'Input field'}
</div>
)
}
}
return (
<div className='space-y-1'>
{config.type !== 'switch' && <Label className='text-xs'>{config.title}</Label>}
{renderSimplifiedInput()}
</div>
)
}
function PreviewWorkflowBlock({ id, data }: NodeProps<any>) {
const { type, config, name, blockState, showSubBlocks = true, isLoopBlock } = data
// Get block configuration - use LoopTool for loop blocks if config is missing
const blockConfig = useMemo(() => {
if (type === 'loop' && !config) {
return LoopTool
}
return config
}, [type, config])
// Only prepare subblocks if they should be shown
const preparedSubBlocks = useMemo(() => {
if (!showSubBlocks) return []
return prepareSubBlocks(blockState?.subBlocks, blockConfig)
}, [blockState?.subBlocks, blockConfig, showSubBlocks])
// Group subblocks for layout
const subBlockRows = useMemo(() => {
return groupSubBlocks(preparedSubBlocks)
}, [preparedSubBlocks])
return (
<div className='relative'>
<Card
className={cn(
'relative select-none shadow-md',
'transition-block-bg transition-ring',
blockState?.isWide ? 'w-[400px]' : 'w-[260px]'
)}
>
{/* Block Header */}
<div className='flex items-center justify-between border-b p-2'>
<div className='flex items-center gap-2'>
<div
className='flex h-6 w-6 items-center justify-center rounded'
style={{ backgroundColor: config.bgColor }}
>
<config.icon className='h-4 w-4 text-white' />
</div>
<span className='max-w-[180px] truncate font-medium text-sm' title={name}>
{name}
</span>
</div>
{type === 'loop' && (
<div className='text-muted-foreground text-xs'>
{blockState?.data?.loopType === 'forEach' ? 'For Each' : 'For'}
{blockState?.data?.count && ` (${blockState.data.count}x)`}
</div>
)}
</div>
{/* Block Content */}
{showSubBlocks && (
<div className='space-y-2 px-3 py-2'>
{subBlockRows.length > 0 ? (
subBlockRows.map((row, rowIndex) => (
<div key={`row-${rowIndex}`} className='flex gap-2'>
{row.map((subBlock, blockIndex) => (
<div
key={`${id}-${rowIndex}-${blockIndex}`}
className={cn('space-y-1', subBlock.layout === 'half' ? 'flex-1' : 'w-full')}
>
<PreviewSubBlock config={subBlock} />
</div>
))}
</div>
))
) : (
<div className='py-2 text-muted-foreground text-xs'>
{type === 'loop' ? 'Loop configuration' : 'No configured items'}
</div>
)}
</div>
)}
{/* Handles */}
{type !== 'starter' && (
<Handle
type='target'
position={blockState?.horizontalHandles ? Position.Left : Position.Top}
id='target'
className={cn(
'!w-3 !h-3',
'!bg-white !rounded-full !border !border-gray-200',
blockState?.horizontalHandles ? '!left-[-6px]' : '!top-[-6px]'
)}
isConnectable={false}
/>
)}
{type !== 'condition' && (
<Handle
type='source'
position={blockState?.horizontalHandles ? Position.Right : Position.Bottom}
id='source'
className={cn(
'!w-3 !h-3',
'!bg-white !rounded-full !border !border-gray-200',
blockState?.horizontalHandles ? '!right-[-6px]' : '!bottom-[-6px]'
)}
isConnectable={false}
/>
)}
</Card>
</div>
)
}
function WorkflowPreviewContent({
workflowState,
showSubBlocks = true,
@@ -545,22 +97,28 @@ function WorkflowPreviewContent({
}
})
// Process top-level blocks first
Object.entries(topLevelBlocks).forEach(([blockId, block]) => {
// Add block nodes using the same approach as workflow.tsx
Object.entries(workflowState.blocks).forEach(([blockId, block]) => {
const blockConfig = getBlock(block.type)
if (!blockConfig) {
logger.error(`No configuration found for block type: ${block.type}`, { blockId })
return
}
nodeArray.push({
id: blockId,
type: 'workflowBlock',
position: block.position,
draggable: false,
data: {
type: block.type,
config: blockConfig || (block.type === 'loop' ? LoopTool : null),
name: block.name,
blockState: block,
isReadOnly: true, // Set read-only mode for preview
isPreview: true, // Indicate this is a preview
showSubBlocks,
},
draggable: false,
})
// Add children of this block if it's a loop
@@ -598,7 +156,7 @@ function WorkflowPreviewContent({
})
return nodeArray
}, [workflowState.blocks, showSubBlocks])
}, [JSON.stringify(workflowState.blocks), JSON.stringify(workflowState.loops), showSubBlocks])
// Transform edges
const edges: Edge[] = useMemo(() => {
@@ -610,7 +168,11 @@ function WorkflowPreviewContent({
targetHandle: edge.targetHandle,
type: 'workflowEdge',
}))
}, [workflowState.edges])
}, [JSON.stringify(workflowState.edges)])
useEffect(() => {
logger.info('Rendering workflow state', { workflowState })
}, [workflowState])
return (
<div style={{ height, width }} className={className}>
@@ -633,6 +195,9 @@ function WorkflowPreviewContent({
minZoom={0.1}
maxZoom={2}
proOptions={{ hideAttribution: true }}
elementsSelectable={false}
nodesDraggable={false}
nodesConnectable={false}
>
<Background />
</ReactFlow>

23502
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff