mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 23:17:59 -05:00
fix: good except for subblocks
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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%'
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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
23502
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user