fix(registry-state): fixed bug on add workflow / improvement(ux): added autoconnect

This commit is contained in:
Emir Karabeg
2025-02-16 17:34:19 -08:00
parent 3b1788a469
commit 6004c8d070
11 changed files with 192 additions and 202 deletions

View File

@@ -7,12 +7,14 @@ import ReactFlow, {
ConnectionLineType,
EdgeTypes,
NodeTypes,
Position,
ReactFlowProvider,
useOnViewportChange,
useReactFlow,
} from 'reactflow'
import 'reactflow/dist/style.css'
import { useNotificationStore } from '@/stores/notifications/store'
import { useGeneralStore } from '@/stores/settings/general/store'
import { initializeStateLogger } from '@/stores/workflow/logger'
import { useWorkflowRegistry } from '@/stores/workflow/registry/store'
import { useWorkflowStore } from '@/stores/workflow/store'
@@ -207,6 +209,26 @@ function WorkflowContent() {
)
// Handle drops
const findClosestOutput = useCallback(
(newNodePosition: { x: number; y: number }) => {
const existingBlocks = Object.entries(blocks)
.filter(([_, block]) => block.enabled && block.type !== 'condition')
.map(([id, block]) => ({
id,
position: block.position,
distance: Math.sqrt(
Math.pow(block.position.x - newNodePosition.x, 2) +
Math.pow(block.position.y - newNodePosition.y, 2)
),
}))
.sort((a, b) => a.distance - b.distance)
return existingBlocks[0]?.id
},
[blocks]
)
// Update the onDrop handler
const onDrop = useCallback(
(event: React.DragEvent) => {
event.preventDefault()
@@ -233,11 +255,27 @@ function WorkflowContent() {
}`
addBlock(id, data.type, name, position)
// Auto-connect logic
const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled
if (isAutoConnectEnabled && data.type !== 'starter') {
const closestBlockId = findClosestOutput(position)
if (closestBlockId) {
addEdge({
id: crypto.randomUUID(),
source: closestBlockId,
target: id,
sourceHandle: 'source',
targetHandle: 'target',
type: 'custom',
})
}
}
} catch (err) {
console.error('Error dropping block:', err)
}
},
[project, blocks, addBlock]
[project, blocks, addBlock, addEdge, findClosestOutput]
)
// Node selection

View File

@@ -14,8 +14,8 @@ import {
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { useEnvironmentStore } from '@/stores/environment/store'
import { EnvironmentVariable as StoreEnvironmentVariable } from '@/stores/environment/types'
import { useEnvironmentStore } from '@/stores/settings/environment/store'
import { EnvironmentVariable as StoreEnvironmentVariable } from '@/stores/settings/environment/types'
// Extend the store type with our UI-specific fields
interface UIEnvironmentVariable extends StoreEnvironmentVariable {

View File

@@ -13,10 +13,13 @@ import {
import { Button } from '@/components/ui/button'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { useGeneralStore } from '@/stores/settings/general/store'
import { resetAllStores } from '@/stores'
export function General() {
const router = useRouter()
const isAutoConnectEnabled = useGeneralStore((state) => state.isAutoConnectEnabled)
const toggleAutoConnect = useGeneralStore((state) => state.toggleAutoConnect)
const handleResetData = () => {
resetAllStores()
@@ -35,10 +38,14 @@ export function General() {
<Switch id="debug-mode" />
</div>
<div className="flex items-center justify-between py-1">
<Label htmlFor="auto-save" className="font-medium">
Auto-save workflows
<Label htmlFor="auto-connect" className="font-medium">
Auto-connect on drop
</Label>
<Switch id="auto-save" />
<Switch
id="auto-connect"
checked={isAutoConnectEnabled}
onCheckedChange={toggleAutoConnect}
/>
</div>
</div>
</div>

View File

@@ -1,7 +1,7 @@
import { useCallback, useState } from 'react'
import { useConsoleStore } from '@/stores/console/store'
import { useEnvironmentStore } from '@/stores/environment/store'
import { useNotificationStore } from '@/stores/notifications/store'
import { useEnvironmentStore } from '@/stores/settings/environment/store'
import { useWorkflowRegistry } from '@/stores/workflow/registry/store'
import { useWorkflowStore } from '@/stores/workflow/store'
import { Executor } from '@/executor'

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'
import { cn } from '@/lib/utils'
import { useEnvironmentStore } from '@/stores/environment/store'
import { useEnvironmentStore } from '@/stores/settings/environment/store'
interface EnvVarDropdownProps {
visible: boolean

View File

@@ -1,6 +1,6 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { useEnvironmentStore } from '../environment/store'
import { useEnvironmentStore } from '../settings/environment/store'
import { useWorkflowStore } from '../workflow/store'
import { ChatMessage, ChatStore } from './types'
import { calculateBlockPosition, getNextBlockNumber } from './utils'

View File

@@ -1,14 +1,13 @@
import { useChatStore } from './chat/store'
import { useConsoleStore } from './console/store'
import { useEnvironmentStore } from './environment/store'
import { useExecutionStore } from './execution/store'
import { useNotificationStore } from './notifications/store'
import { useEnvironmentStore } from './settings/environment/store'
import { useGeneralStore } from './settings/general/store'
import { useWorkflowRegistry } from './workflow/registry/store'
import { useWorkflowStore } from './workflow/store'
/**
* Reset all application stores to their initial state
*/
// Reset all application stores to their initial state
export const resetAllStores = () => {
// Selectively clear localStorage items
if (typeof window !== 'undefined') {
@@ -30,12 +29,11 @@ export const resetAllStores = () => {
useEnvironmentStore.setState({ variables: {} })
useExecutionStore.getState().reset()
useConsoleStore.setState({ entries: [], isOpen: false })
useGeneralStore.setState({ isAutoConnectEnabled: true })
useChatStore.setState({ messages: [], isProcessing: false, error: null })
}
/**
* Log the current state of all stores
*/
// Log the current state of all stores
export const logAllStores = () => {
const state = {
workflow: useWorkflowStore.getState(),

View File

@@ -0,0 +1,29 @@
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
interface General {
isAutoConnectEnabled: boolean
}
interface GeneralActions {
toggleAutoConnect: () => void
}
type GeneralStore = General & GeneralActions
export const useGeneralStore = create<GeneralStore>()(
devtools(
persist(
(set) => ({
isAutoConnectEnabled: true,
toggleAutoConnect: () =>
set((state) => ({ isAutoConnectEnabled: !state.isAutoConnectEnabled })),
}),
{
name: 'general-settings',
}
),
{ name: 'general-store' }
)
)

View File

@@ -84,101 +84,103 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
error: null,
}))
// Add starter block to new workflow
// Create starter block for new workflow
const starterId = crypto.randomUUID()
useWorkflowStore.setState({
blocks: {
[starterId]: {
id: starterId,
type: 'starter',
name: 'Starter',
position: { x: 100, y: 100 },
subBlocks: {
startWorkflow: {
id: 'startWorkflow',
type: 'dropdown',
value: 'manual',
},
webhookPath: {
id: 'webhookPath',
type: 'short-input',
value: '',
},
webhookSecret: {
id: 'webhookSecret',
type: 'short-input',
value: '',
},
scheduleType: {
id: 'scheduleType',
type: 'dropdown',
value: 'daily',
},
minutesInterval: {
id: 'minutesInterval',
type: 'short-input',
value: '',
},
minutesStartingAt: {
id: 'minutesStartingAt',
type: 'short-input',
value: '',
},
hourlyMinute: {
id: 'hourlyMinute',
type: 'short-input',
value: '',
},
dailyTime: {
id: 'dailyTime',
type: 'short-input',
value: '',
},
weeklyDay: {
id: 'weeklyDay',
type: 'dropdown',
value: 'MON',
},
weeklyDayTime: {
id: 'weeklyDayTime',
type: 'short-input',
value: '',
},
monthlyDay: {
id: 'monthlyDay',
type: 'short-input',
value: '',
},
monthlyTime: {
id: 'monthlyTime',
type: 'short-input',
value: '',
},
cronExpression: {
id: 'cronExpression',
type: 'short-input',
value: '',
},
timezone: {
id: 'timezone',
type: 'dropdown',
value: 'UTC',
},
},
outputs: {
response: {
type: {
result: 'any',
stdout: 'string',
executionTime: 'number',
},
},
},
enabled: true,
horizontalHandles: true,
isWide: false,
height: 0,
const starterBlock = {
id: starterId,
type: 'starter' as const,
name: 'Start',
position: { x: 100, y: 100 },
subBlocks: {
startWorkflow: {
id: 'startWorkflow',
type: 'dropdown' as const,
value: 'manual',
},
webhookPath: {
id: 'webhookPath',
type: 'short-input' as const,
value: '',
},
webhookSecret: {
id: 'webhookSecret',
type: 'short-input' as const,
value: '',
},
scheduleType: {
id: 'scheduleType',
type: 'dropdown' as const,
value: 'daily',
},
minutesInterval: {
id: 'minutesInterval',
type: 'short-input' as const,
value: '',
},
minutesStartingAt: {
id: 'minutesStartingAt',
type: 'short-input' as const,
value: '',
},
hourlyMinute: {
id: 'hourlyMinute',
type: 'short-input' as const,
value: '',
},
dailyTime: {
id: 'dailyTime',
type: 'short-input' as const,
value: '',
},
weeklyDay: {
id: 'weeklyDay',
type: 'dropdown' as const,
value: 'MON',
},
weeklyDayTime: {
id: 'weeklyDayTime',
type: 'short-input' as const,
value: '',
},
monthlyDay: {
id: 'monthlyDay',
type: 'short-input' as const,
value: '',
},
monthlyTime: {
id: 'monthlyTime',
type: 'short-input' as const,
value: '',
},
cronExpression: {
id: 'cronExpression',
type: 'short-input' as const,
value: '',
},
timezone: {
id: 'timezone',
type: 'dropdown' as const,
value: 'UTC',
},
},
outputs: {
response: {
type: {
result: 'any',
stdout: 'string',
executionTime: 'number',
},
},
},
enabled: true,
horizontalHandles: true,
isWide: false,
height: 0,
}
const initialState = {
blocks: {
[starterId]: starterBlock,
},
edges: [],
loops: {},
@@ -187,97 +189,7 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
present: {
state: {
blocks: {
[starterId]: {
id: starterId,
type: 'starter',
name: 'Starter',
position: { x: 100, y: 100 },
subBlocks: {
startWorkflow: {
id: 'startWorkflow',
type: 'dropdown',
value: 'manual',
},
webhookPath: {
id: 'webhookPath',
type: 'short-input',
value: '',
},
webhookSecret: {
id: 'webhookSecret',
type: 'short-input',
value: '',
},
scheduleType: {
id: 'scheduleType',
type: 'dropdown',
value: 'daily',
},
minutesInterval: {
id: 'minutesInterval',
type: 'short-input',
value: '',
},
minutesStartingAt: {
id: 'minutesStartingAt',
type: 'short-input',
value: '',
},
hourlyMinute: {
id: 'hourlyMinute',
type: 'short-input',
value: '',
},
dailyTime: {
id: 'dailyTime',
type: 'short-input',
value: '',
},
weeklyDay: {
id: 'weeklyDay',
type: 'dropdown',
value: 'MON',
},
weeklyDayTime: {
id: 'weeklyDayTime',
type: 'short-input',
value: '',
},
monthlyDay: {
id: 'monthlyDay',
type: 'short-input',
value: '',
},
monthlyTime: {
id: 'monthlyTime',
type: 'short-input',
value: '',
},
cronExpression: {
id: 'cronExpression',
type: 'short-input',
value: '',
},
timezone: {
id: 'timezone',
type: 'dropdown',
value: 'UTC',
},
},
outputs: {
response: {
type: {
result: 'any',
stdout: 'string',
executionTime: 'number',
},
},
},
enabled: true,
horizontalHandles: true,
isWide: false,
height: 0,
},
[starterId]: starterBlock,
},
edges: [],
loops: {},
@@ -288,14 +200,20 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
future: [],
},
lastSaved: Date.now(),
})
}
// Save workflow list to localStorage
const workflows = get().workflows
localStorage.setItem('workflow-registry', JSON.stringify(workflows))
// Save initial workflow state to localStorage
localStorage.setItem(`workflow-${metadata.id}`, JSON.stringify(useWorkflowStore.getState()))
localStorage.setItem(`workflow-${metadata.id}`, JSON.stringify(initialState))
// If this is the first workflow, set it as active and update workflow store
if (Object.keys(workflows).length === 1) {
set({ activeWorkflowId: metadata.id })
useWorkflowStore.setState(initialState)
}
},
removeWorkflow: (id: string) => {