From 6004c8d070b73f68c024b3206ade5c2dcf2c0968 Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Sun, 16 Feb 2025 17:34:19 -0800 Subject: [PATCH] fix(registry-state): fixed bug on add workflow / improvement(ux): added autoconnect --- app/w/[id]/workflow.tsx | 40 ++- .../components/environment/environment.tsx | 4 +- .../components/general/general.tsx | 13 +- app/w/hooks/use-workflow-execution.ts | 2 +- components/ui/env-var-dropdown.tsx | 2 +- stores/chat/store.ts | 2 +- stores/index.ts | 12 +- stores/{ => settings}/environment/store.ts | 0 stores/{ => settings}/environment/types.ts | 0 stores/settings/general/store.ts | 29 ++ stores/workflow/registry/store.ts | 290 +++++++----------- 11 files changed, 192 insertions(+), 202 deletions(-) rename stores/{ => settings}/environment/store.ts (100%) rename stores/{ => settings}/environment/types.ts (100%) create mode 100644 stores/settings/general/store.ts diff --git a/app/w/[id]/workflow.tsx b/app/w/[id]/workflow.tsx index a62ee1b54..224a2534e 100644 --- a/app/w/[id]/workflow.tsx +++ b/app/w/[id]/workflow.tsx @@ -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 diff --git a/app/w/components/sidebar/components/settings-modal/components/environment/environment.tsx b/app/w/components/sidebar/components/settings-modal/components/environment/environment.tsx index b8930324c..31ab6fe5e 100644 --- a/app/w/components/sidebar/components/settings-modal/components/environment/environment.tsx +++ b/app/w/components/sidebar/components/settings-modal/components/environment/environment.tsx @@ -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 { diff --git a/app/w/components/sidebar/components/settings-modal/components/general/general.tsx b/app/w/components/sidebar/components/settings-modal/components/general/general.tsx index f6d782674..31203e563 100644 --- a/app/w/components/sidebar/components/settings-modal/components/general/general.tsx +++ b/app/w/components/sidebar/components/settings-modal/components/general/general.tsx @@ -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() {
-
diff --git a/app/w/hooks/use-workflow-execution.ts b/app/w/hooks/use-workflow-execution.ts index 685d0b564..526dbc918 100644 --- a/app/w/hooks/use-workflow-execution.ts +++ b/app/w/hooks/use-workflow-execution.ts @@ -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' diff --git a/components/ui/env-var-dropdown.tsx b/components/ui/env-var-dropdown.tsx index 5caa265d0..a250cc68e 100644 --- a/components/ui/env-var-dropdown.tsx +++ b/components/ui/env-var-dropdown.tsx @@ -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 diff --git a/stores/chat/store.ts b/stores/chat/store.ts index 24c02b350..1ed09dfed 100644 --- a/stores/chat/store.ts +++ b/stores/chat/store.ts @@ -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' diff --git a/stores/index.ts b/stores/index.ts index 7633e093a..9aacab6c8 100644 --- a/stores/index.ts +++ b/stores/index.ts @@ -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(), diff --git a/stores/environment/store.ts b/stores/settings/environment/store.ts similarity index 100% rename from stores/environment/store.ts rename to stores/settings/environment/store.ts diff --git a/stores/environment/types.ts b/stores/settings/environment/types.ts similarity index 100% rename from stores/environment/types.ts rename to stores/settings/environment/types.ts diff --git a/stores/settings/general/store.ts b/stores/settings/general/store.ts new file mode 100644 index 000000000..31b7e3e7d --- /dev/null +++ b/stores/settings/general/store.ts @@ -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()( + devtools( + persist( + (set) => ({ + isAutoConnectEnabled: true, + + toggleAutoConnect: () => + set((state) => ({ isAutoConnectEnabled: !state.isAutoConnectEnabled })), + }), + { + name: 'general-settings', + } + ), + { name: 'general-store' } + ) +) diff --git a/stores/workflow/registry/store.ts b/stores/workflow/registry/store.ts index 1f543c777..521ba84f6 100644 --- a/stores/workflow/registry/store.ts +++ b/stores/workflow/registry/store.ts @@ -84,101 +84,103 @@ export const useWorkflowRegistry = create()( 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()( 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()( 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) => {