mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
cleaning up
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -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}
|
||||
|
||||
+28
-29
@@ -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>
|
||||
|
||||
+13
-19
@@ -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>
|
||||
|
||||
+11
-11
@@ -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)
|
||||
|
||||
+8
-6
@@ -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
|
||||
|
||||
+13
-16
@@ -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
|
||||
|
||||
+8
-6
@@ -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'
|
||||
|
||||
+28
-16
@@ -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 && (
|
||||
|
||||
+7
-8
@@ -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,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(() => {
|
||||
|
||||
+31
-24
@@ -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>
|
||||
|
||||
+16
-16
@@ -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) => {
|
||||
|
||||
+3
-4
@@ -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
|
||||
|
||||
+11
-15
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+5
-10
@@ -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 = (
|
||||
|
||||
+6
-6
@@ -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)
|
||||
|
||||
|
||||
+7
-10
@@ -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(() => {
|
||||
|
||||
+29
-22
@@ -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>
|
||||
)
|
||||
|
||||
+27
-16
@@ -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' />
|
||||
|
||||
+10
-11
@@ -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>
|
||||
|
||||
+10
-10
@@ -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
-9
@@ -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',
|
||||
|
||||
+102
-39
@@ -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>
|
||||
) : (
|
||||
|
||||
+7
-7
@@ -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>
|
||||
|
||||
+3
-3
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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[]
|
||||
|
||||
Reference in New Issue
Block a user