diff --git a/app/api/settings/environment/route.ts b/app/api/db/environment/route.ts similarity index 59% rename from app/api/settings/environment/route.ts rename to app/api/db/environment/route.ts index 68c03a602e..10bed29ffb 100644 --- a/app/api/settings/environment/route.ts +++ b/app/api/db/environment/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server' import { eq } from 'drizzle-orm' import { z } from 'zod' import { getSession } from '@/lib/auth' -import { encryptSecret } from '@/lib/utils' +import { decryptSecret, encryptSecret } from '@/lib/utils' import { EnvironmentVariable } from '@/stores/settings/environment/types' import { db } from '@/db' import { environment } from '@/db/schema' @@ -22,14 +22,17 @@ export async function POST(req: NextRequest) { const body = await req.json() const { variables } = EnvVarSchema.parse(body) - // Encrypt each environment variable value - const encryptedVariables: Record = {} - for (const [key, value] of Object.entries(variables)) { - const { encrypted } = await encryptSecret(value) - encryptedVariables[key] = encrypted - } + // Encrypt all variables + const encryptedVariables = await Object.entries(variables).reduce( + async (accPromise, [key, value]) => { + const acc = await accPromise + const { encrypted } = await encryptSecret(value) + return { ...acc, [key]: encrypted } + }, + Promise.resolve({}) + ) - // Upsert the environment variables + // Replace all environment variables for user await db .insert(environment) .values({ @@ -61,13 +64,14 @@ export async function POST(req: NextRequest) { export async function GET(request: Request) { try { - const { searchParams } = new URL(request.url) - const userId = searchParams.get('userId') - - if (!userId) { - return NextResponse.json({ error: 'userId is required' }, { status: 400 }) + // Get the session directly in the API route + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } + const userId = session.user.id + const result = await db .select() .from(environment) @@ -78,13 +82,23 @@ export async function GET(request: Request) { return NextResponse.json({ data: {} }, { status: 200 }) } - // Update the type handling for variables - const variables = result[0].variables as Record - const sanitizedVariables = Object.fromEntries( - Object.entries(variables).map(([key, value]) => [key, { key, value: '••••••••' }]) - ) + // Decrypt the variables for client-side use + const encryptedVariables = result[0].variables as Record + const decryptedVariables: Record = {} - return NextResponse.json({ data: sanitizedVariables }, { status: 200 }) + // Decrypt each variable + for (const [key, encryptedValue] of Object.entries(encryptedVariables)) { + try { + const { decrypted } = await decryptSecret(encryptedValue) + decryptedVariables[key] = { key, value: decrypted } + } catch (error) { + console.error(`Error decrypting variable ${key}:`, error) + // If decryption fails, provide a placeholder + decryptedVariables[key] = { key, value: '' } + } + } + + return NextResponse.json({ data: decryptedVariables }, { status: 200 }) } catch (error: any) { console.error('Environment fetch error:', error) return NextResponse.json({ error: error.message }, { status: 500 }) diff --git a/app/api/settings/general/route.ts b/app/api/db/settings/route.ts similarity index 100% rename from app/api/settings/general/route.ts rename to app/api/db/settings/route.ts diff --git a/app/api/db/sync/route.ts b/app/api/db/sync/route.ts deleted file mode 100644 index be09b36a87..0000000000 --- a/app/api/db/sync/route.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { NextResponse } from 'next/server' -import { eq, sql } from 'drizzle-orm' -import { z } from 'zod' -import { getSession } from '@/lib/auth' -import { db } from '@/db' -import { workflow } from '@/db/schema' - -// Define the schema for a single workflow -const WorkflowSchema = z.object({ - id: z.string(), - name: z.string(), - description: z.string().optional(), - state: z.record(z.any()), -}) - -// Define the schema for batch sync -const BatchSyncSchema = z.object({ - workflows: z.array(WorkflowSchema), - deletedWorkflowIds: z.array(z.string()).optional(), -}) - -export async function POST(request: Request) { - try { - const session = await getSession() - if (!session?.user?.id) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const body = await request.json() - const { workflows, deletedWorkflowIds } = BatchSyncSchema.parse(body) - const now = new Date() - - // Process all operations in a single transaction - await db.transaction(async (tx) => { - // Handle deletions first - if (deletedWorkflowIds?.length) { - await tx - .delete(workflow) - .where( - sql`${workflow.id} IN ${deletedWorkflowIds} AND ${workflow.userId} = ${session.user.id}` - ) - } - - // Handle updates/inserts - for (const workflowData of workflows) { - await tx - .insert(workflow) - .values({ - id: workflowData.id, - userId: session.user.id, - name: workflowData.name, - description: workflowData.description, - state: workflowData.state, - lastSynced: now, - createdAt: now, - updatedAt: now, - }) - .onConflictDoUpdate({ - target: [workflow.id], - set: { - name: workflowData.name, - description: workflowData.description, - state: workflowData.state, - lastSynced: now, - updatedAt: now, - }, - where: eq(workflow.userId, session.user.id), - }) - } - }) - - return NextResponse.json({ success: true }) - } catch (error) { - console.error('Batch sync error:', error) - if (error instanceof z.ZodError) { - return NextResponse.json( - { error: 'Invalid request data', details: error.errors }, - { status: 400 } - ) - } - return NextResponse.json({ error: 'Batch sync failed' }, { status: 500 }) - } -} diff --git a/app/api/db/workflow/route.ts b/app/api/db/workflow/route.ts new file mode 100644 index 0000000000..f3326b737c --- /dev/null +++ b/app/api/db/workflow/route.ts @@ -0,0 +1,138 @@ +import { NextRequest, NextResponse } from 'next/server' +import { eq } from 'drizzle-orm' +import { z } from 'zod' +import { getSession } from '@/lib/auth' +import { db } from '@/db' +import { workflow } from '@/db/schema' + +// Schema for workflow data +const WorkflowStateSchema = z.object({ + blocks: z.record(z.any()), + edges: z.array(z.any()), + loops: z.record(z.any()), + lastSaved: z.number().optional(), + isDeployed: z.boolean().optional(), + deployedAt: z.date().optional(), +}) + +const WorkflowSchema = z.object({ + id: z.string(), + name: z.string(), + description: z.string().optional(), + color: z.string().optional(), + state: WorkflowStateSchema, +}) + +const SyncPayloadSchema = z.object({ + workflows: z.record(z.string(), WorkflowSchema), +}) + +export async function GET(request: Request) { + try { + // Get the session directly in the API route + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const userId = session.user.id + + // Fetch all workflows for the user + const workflows = await db.select().from(workflow).where(eq(workflow.userId, userId)) + + // Return the workflows + return NextResponse.json({ data: workflows }, { status: 200 }) + } catch (error: any) { + console.error('Workflow fetch error:', error) + return NextResponse.json({ error: error.message }, { status: 500 }) + } +} + +export async function POST(req: NextRequest) { + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const body = await req.json() + const { workflows: clientWorkflows } = SyncPayloadSchema.parse(body) + + // Get all workflows for the user from the database + const dbWorkflows = await db.select().from(workflow).where(eq(workflow.userId, session.user.id)) + + const now = new Date() + const operations: Promise[] = [] + + // Create a map of DB workflows for easier lookup + const dbWorkflowMap = new Map(dbWorkflows.map((w) => [w.id, w])) + const processedIds = new Set() + + // Process client workflows + for (const [id, clientWorkflow] of Object.entries(clientWorkflows)) { + processedIds.add(id) + const dbWorkflow = dbWorkflowMap.get(id) + + if (!dbWorkflow) { + // New workflow - create + operations.push( + db.insert(workflow).values({ + id: clientWorkflow.id, + userId: session.user.id, + name: clientWorkflow.name, + description: clientWorkflow.description, + color: clientWorkflow.color, + state: clientWorkflow.state, + lastSynced: now, + createdAt: now, + updatedAt: now, + }) + ) + } else { + // Existing workflow - update if needed + const needsUpdate = + JSON.stringify(dbWorkflow.state) !== JSON.stringify(clientWorkflow.state) || + dbWorkflow.name !== clientWorkflow.name || + dbWorkflow.description !== clientWorkflow.description || + dbWorkflow.color !== clientWorkflow.color + + if (needsUpdate) { + operations.push( + db + .update(workflow) + .set({ + name: clientWorkflow.name, + description: clientWorkflow.description, + color: clientWorkflow.color, + state: clientWorkflow.state, + lastSynced: now, + updatedAt: now, + }) + .where(eq(workflow.id, id)) + ) + } + } + } + + // Handle deletions - workflows in DB but not in client + for (const dbWorkflow of dbWorkflows) { + if (!processedIds.has(dbWorkflow.id)) { + operations.push(db.delete(workflow).where(eq(workflow.id, dbWorkflow.id))) + } + } + + // Execute all operations in parallel + await Promise.all(operations) + + return NextResponse.json({ success: true }) + } catch (error) { + console.error('Workflow sync error:', error) + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + return NextResponse.json({ error: 'Workflow sync failed' }, { status: 500 }) + } +} diff --git a/app/api/scheduled/execute/route.ts b/app/api/scheduled/execute/route.ts index 71b24a8365..1004b61232 100644 --- a/app/api/scheduled/execute/route.ts +++ b/app/api/scheduled/execute/route.ts @@ -5,8 +5,8 @@ import { v4 as uuidv4 } from 'uuid' import { z } from 'zod' import { persistLog } from '@/lib/logging' import { decryptSecret } from '@/lib/utils' -import { BlockState, WorkflowState } from '@/stores/workflow/types' -import { mergeSubblockState } from '@/stores/workflow/utils' +import { mergeSubblockState } from '@/stores/workflows/utils' +import { BlockState, WorkflowState } from '@/stores/workflows/workflow/types' import { db } from '@/db' import { environment, workflow, workflowSchedule } from '@/db/schema' import { Executor } from '@/executor' diff --git a/app/api/scheduled/schedule/route.ts b/app/api/scheduled/schedule/route.ts index 72541d4cb4..d35796eca2 100644 --- a/app/api/scheduled/schedule/route.ts +++ b/app/api/scheduled/schedule/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server' import { eq } from 'drizzle-orm' import { z } from 'zod' import { getSession } from '@/lib/auth' -import { BlockState } from '@/stores/workflow/types' +import { BlockState } from '@/stores/workflows/workflow/types' import { db } from '@/db' import { workflow, workflowSchedule } from '@/db/schema' diff --git a/app/api/workflow/[id]/execute/route.ts b/app/api/workflow/[id]/execute/route.ts index 9488bc6290..aaa565f649 100644 --- a/app/api/workflow/[id]/execute/route.ts +++ b/app/api/workflow/[id]/execute/route.ts @@ -4,8 +4,8 @@ import { v4 as uuidv4 } from 'uuid' import { z } from 'zod' import { persistLog } from '@/lib/logging' import { decryptSecret } from '@/lib/utils' -import { WorkflowState } from '@/stores/workflow/types' -import { mergeSubblockState } from '@/stores/workflow/utils' +import { mergeSubblockState } from '@/stores/workflows/utils' +import { WorkflowState } from '@/stores/workflows/workflow/types' import { db } from '@/db' import { environment } from '@/db/schema' import { Executor } from '@/executor' diff --git a/app/w/[id]/components/workflow-block/components/action-bar/action-bar.tsx b/app/w/[id]/components/workflow-block/components/action-bar/action-bar.tsx index 5ef92b527e..6a9e30bfb5 100644 --- a/app/w/[id]/components/workflow-block/components/action-bar/action-bar.tsx +++ b/app/w/[id]/components/workflow-block/components/action-bar/action-bar.tsx @@ -2,7 +2,7 @@ import { ArrowLeftRight, ArrowUpDown, Circle, CircleOff, Copy, Play, Trash2 } fr import { Button } from '@/components/ui/button' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { cn } from '@/lib/utils' -import { useWorkflowStore } from '@/stores/workflow/store' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' interface ActionBarProps { blockId: string diff --git a/app/w/[id]/components/workflow-block/components/sub-block/components/condition-input.tsx b/app/w/[id]/components/workflow-block/components/sub-block/components/condition-input.tsx index 7b619bfe00..110875afd2 100644 --- a/app/w/[id]/components/workflow-block/components/sub-block/components/condition-input.tsx +++ b/app/w/[id]/components/workflow-block/components/sub-block/components/condition-input.tsx @@ -11,7 +11,7 @@ import { EnvVarDropdown, checkEnvVarTrigger } from '@/components/ui/env-var-drop import { TagDropdown, checkTagTrigger } from '@/components/ui/tag-dropdown' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { cn } from '@/lib/utils' -import { useWorkflowStore } from '@/stores/workflow/store' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' import { useSubBlockValue } from '../hooks/use-sub-block-value' interface ConditionalBlock { diff --git a/app/w/[id]/components/workflow-block/components/sub-block/components/long-input.tsx b/app/w/[id]/components/workflow-block/components/sub-block/components/long-input.tsx index da01423e54..c4254ed15c 100644 --- a/app/w/[id]/components/workflow-block/components/sub-block/components/long-input.tsx +++ b/app/w/[id]/components/workflow-block/components/sub-block/components/long-input.tsx @@ -4,7 +4,7 @@ import { formatDisplayText } from '@/components/ui/formatted-text' import { TagDropdown, checkTagTrigger } from '@/components/ui/tag-dropdown' import { Textarea } from '@/components/ui/textarea' import { cn } from '@/lib/utils' -import { useWorkflowStore } from '@/stores/workflow/store' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' import { SubBlockConfig } from '@/blocks/types' import { useSubBlockValue } from '../hooks/use-sub-block-value' diff --git a/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx b/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx index 21aa50e700..a68b49f249 100644 --- a/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx +++ b/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx @@ -20,7 +20,7 @@ import { } from '@/components/ui/select' import { cn } from '@/lib/utils' import { useCustomToolsStore } from '@/stores/custom-tools/store' -import { useWorkflowStore } from '@/stores/workflow/store' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' import { getAllBlocks } from '@/blocks' import { getTool } from '@/tools' import { useSubBlockValue } from '../../hooks/use-sub-block-value' diff --git a/app/w/[id]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts b/app/w/[id]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts index 8fdae8d31d..f07fb72c7e 100644 --- a/app/w/[id]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts +++ b/app/w/[id]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts @@ -1,6 +1,6 @@ import { useCallback } from 'react' -import { useWorkflowStore } from '@/stores/workflow/store' -import { useSubBlockStore } from '@/stores/workflow/subblock/store' +import { useSubBlockStore } from '@/stores/workflows/subblock/store' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' export function useSubBlockValue( blockId: string, diff --git a/app/w/[id]/components/workflow-block/workflow-block.tsx b/app/w/[id]/components/workflow-block/workflow-block.tsx index 097218f1c8..9990e16cda 100644 --- a/app/w/[id]/components/workflow-block/workflow-block.tsx +++ b/app/w/[id]/components/workflow-block/workflow-block.tsx @@ -7,9 +7,9 @@ import { Card } from '@/components/ui/card' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { cn } from '@/lib/utils' import { useExecutionStore } from '@/stores/execution/store' -import { useWorkflowRegistry } from '@/stores/workflow/registry/store' -import { useWorkflowStore } from '@/stores/workflow/store' -import { mergeSubblockState } from '@/stores/workflow/utils' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +import { mergeSubblockState } from '@/stores/workflows/utils' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' import { BlockConfig, SubBlockConfig } from '@/blocks/types' import { ActionBar } from './components/action-bar/action-bar' import { ConnectionBlocks } from './components/connection-blocks/connection-blocks' diff --git a/app/w/[id]/components/workflow-loop/components/loop-input/loop-input.tsx b/app/w/[id]/components/workflow-loop/components/loop-input/loop-input.tsx index 8b832fdc1a..721ec37d2b 100644 --- a/app/w/[id]/components/workflow-loop/components/loop-input/loop-input.tsx +++ b/app/w/[id]/components/workflow-loop/components/loop-input/loop-input.tsx @@ -5,7 +5,7 @@ import { Badge } from '@/components/ui/badge' import { Input } from '@/components/ui/input' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { cn } from '@/lib/utils' -import { useWorkflowStore } from '@/stores/workflow/store' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' export function LoopInput({ id }: NodeProps) { // Extract the loop ID from the node ID diff --git a/app/w/[id]/components/workflow-loop/workflow-loop.tsx b/app/w/[id]/components/workflow-loop/workflow-loop.tsx index db16412d7d..8e0c6d949a 100644 --- a/app/w/[id]/components/workflow-loop/workflow-loop.tsx +++ b/app/w/[id]/components/workflow-loop/workflow-loop.tsx @@ -1,5 +1,5 @@ -import { useWorkflowStore } from '@/stores/workflow/store' -import { Loop } from '@/stores/workflow/types' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' +import { Loop } from '@/stores/workflows/workflow/types' interface WorkflowLoopProps { loopId: string diff --git a/app/w/[id]/workflow.tsx b/app/w/[id]/workflow.tsx index 5288689310..14f3164536 100644 --- a/app/w/[id]/workflow.tsx +++ b/app/w/[id]/workflow.tsx @@ -14,9 +14,9 @@ import 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' +import { getSyncManagers, initializeSyncManagers, isSyncInitialized } from '@/stores/sync-registry' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' import { NotificationList } from '@/app/w/components/notifications/notifications' import { getBlock } from '../../../blocks' import { ErrorBoundary } from '../components/error-boundary/error-boundary' @@ -53,11 +53,19 @@ function WorkflowContent() { // Initialize workflow useEffect(() => { if (typeof window !== 'undefined') { - const savedRegistry = localStorage.getItem('workflow-registry') - if (savedRegistry) { - useWorkflowRegistry.setState({ workflows: JSON.parse(savedRegistry) }) + // Ensure sync system is initialized before proceeding + const initSync = async () => { + // Initialize sync system if not already initialized + await initializeSyncManagers() + setIsInitialized(true) + } + + // Check if already initialized + if (isSyncInitialized()) { + setIsInitialized(true) + } else { + initSync() } - setIsInitialized(true) } }, []) @@ -299,11 +307,6 @@ function WorkflowContent() { return () => window.removeEventListener('keydown', handleKeyDown) }, [selectedEdgeId, removeEdge]) - // Initialize state logging - // useEffect(() => { - // initializeStateLogger() - // }, []) - if (!isInitialized) return null return ( diff --git a/app/w/components/console/console.tsx b/app/w/components/console/console.tsx index 357a6b4b39..18c83d3cf4 100644 --- a/app/w/components/console/console.tsx +++ b/app/w/components/console/console.tsx @@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button' import { ScrollArea } from '@/components/ui/scroll-area' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { useConsoleStore } from '@/stores/console/store' -import { useWorkflowRegistry } from '@/stores/workflow/registry/store' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { ConsoleEntry } from './components/console-entry/console-entry' export function Console() { diff --git a/app/w/components/control-bar/control-bar.tsx b/app/w/components/control-bar/control-bar.tsx index 0bf02b92c8..b79ba45ed5 100644 --- a/app/w/components/control-bar/control-bar.tsx +++ b/app/w/components/control-bar/control-bar.tsx @@ -25,9 +25,8 @@ import { import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { cn } from '@/lib/utils' import { useNotificationStore } from '@/stores/notifications/store' -import { performSync } from '@/stores/sync-manager' -import { useWorkflowRegistry } from '@/stores/workflow/registry/store' -import { useWorkflowStore } from '@/stores/workflow/store' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' import { useWorkflowExecution } from '../../hooks/use-workflow-execution' import { HistoryDropdownItem } from './components/history-dropdown-item' import { NotificationDropdownItem } from './components/notification-dropdown-item' @@ -207,8 +206,6 @@ export function ControlBar() { try { setIsDeploying(true) - await performSync() - const response = await fetch(`/api/workflow/${activeWorkflowId}/deploy`, { method: 'POST', }) diff --git a/app/w/components/notifications/notifications.tsx b/app/w/components/notifications/notifications.tsx index f4675fe0a4..5896235c7d 100644 --- a/app/w/components/notifications/notifications.tsx +++ b/app/w/components/notifications/notifications.tsx @@ -16,7 +16,7 @@ import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' import { useNotificationStore } from '@/stores/notifications/store' import { Notification, NotificationType } from '@/stores/notifications/types' -import { useWorkflowStore } from '@/stores/workflow/store' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' // Constants const NOTIFICATION_TIMEOUT = 4000 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 4f4d2f1583..8d004e2c56 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 @@ -17,7 +17,10 @@ import { Label } from '@/components/ui/label' 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 +// Constants +const GRID_COLS = 'grid grid-cols-[minmax(0,1fr),minmax(0,1fr),40px] gap-4' +const INITIAL_ENV_VAR: UIEnvironmentVariable = { key: '', value: '' } + interface UIEnvironmentVariable extends StoreEnvironmentVariable { id?: number } @@ -26,19 +29,21 @@ interface EnvironmentVariablesProps { onOpenChange: (open: boolean) => void } -const GRID_COLS = 'grid grid-cols-[minmax(0,1fr),minmax(0,1fr),40px] gap-4' -const INITIAL_ENV_VAR: UIEnvironmentVariable = { key: '', value: '' } - export function EnvironmentVariables({ onOpenChange }: EnvironmentVariablesProps) { - const { variables, setVariable, removeVariable } = useEnvironmentStore() + // Store access + const { variables } = useEnvironmentStore() + + // State const [envVars, setEnvVars] = useState([]) const [focusedValueIndex, setFocusedValueIndex] = useState(null) const [showUnsavedChanges, setShowUnsavedChanges] = useState(false) + + // Refs const scrollContainerRef = useRef(null) const pendingClose = useRef(false) const initialVarsRef = useRef([]) - // Check if there are unsaved changes by comparing with initial state + // Derived state const hasChanges = useMemo(() => { const initialVars = initialVarsRef.current.filter((v) => v.key || v.value) const currentVars = envVars.filter((v) => v.key || v.value) @@ -60,7 +65,7 @@ export function EnvironmentVariables({ onOpenChange }: EnvironmentVariablesProps return false }, [envVars]) - // Initialize environment variables + // Initialization effect useEffect(() => { const existingVars = Object.values(variables) const initialVars = existingVars.length ? existingVars : [INITIAL_ENV_VAR] @@ -69,33 +74,34 @@ export function EnvironmentVariables({ onOpenChange }: EnvironmentVariablesProps pendingClose.current = false }, [variables]) - const handleClose = () => { - if (hasChanges) { - setShowUnsavedChanges(true) - pendingClose.current = true - } else { - onOpenChange(false) - } - } - - const handleCancel = () => { - setEnvVars(JSON.parse(JSON.stringify(initialVarsRef.current))) - setShowUnsavedChanges(false) - if (pendingClose.current) { - onOpenChange(false) - } - } - + // Scroll effect useEffect(() => { if (scrollContainerRef.current) { - // Smooth scroll to bottom when new variables are added scrollContainerRef.current.scrollTo({ top: scrollContainerRef.current.scrollHeight, behavior: 'smooth', }) } - }, [envVars.length]) // Only trigger on length changes + }, [envVars.length]) + // Variable management functions + const addEnvVar = () => { + const newVar = { key: '', value: '', id: Date.now() } + setEnvVars([...envVars, newVar]) + } + + const updateEnvVar = (index: number, field: 'key' | 'value', value: string) => { + const newEnvVars = [...envVars] + newEnvVars[index][field] = value + setEnvVars(newEnvVars) + } + + const removeEnvVar = (index: number) => { + const newEnvVars = envVars.filter((_, i) => i !== index) + setEnvVars(newEnvVars.length ? newEnvVars : [INITIAL_ENV_VAR]) + } + + // Input event handlers const handleValueFocus = (index: number, e: React.FocusEvent) => { setFocusedValueIndex(index) e.target.scrollLeft = 0 @@ -153,45 +159,49 @@ export function EnvironmentVariables({ onOpenChange }: EnvironmentVariablesProps } } - const addEnvVar = () => { - const newVar = { key: '', value: '', id: Date.now() } - setEnvVars([...envVars, newVar]) - } - - const updateEnvVar = (index: number, field: 'key' | 'value', value: string) => { - const newEnvVars = [...envVars] - newEnvVars[index][field] = value - setEnvVars(newEnvVars) - } - - const removeEnvVar = (index: number) => { - const newEnvVars = envVars.filter((_, i) => i !== index) - setEnvVars(newEnvVars.length ? newEnvVars : [INITIAL_ENV_VAR]) - } - - const handleSave = async () => { - try { - const validVars = envVars.filter((v) => v.key && v.value) - validVars.forEach((v) => setVariable(v.key, v.value)) - - const currentKeys = new Set(validVars.map((v) => v.key)) - Object.keys(variables).forEach((key) => { - if (!currentKeys.has(key)) { - removeVariable(key) - } - }) - - // Sync with database - await useEnvironmentStore.getState().syncWithDatabase() - - setShowUnsavedChanges(false) + // Dialog management + const handleClose = () => { + if (hasChanges) { + setShowUnsavedChanges(true) + pendingClose.current = true + } else { onOpenChange(false) - } catch (error) { - console.error('Failed to save environment variables:', error) - // You might want to show an error notification here } } + const handleCancel = () => { + setEnvVars(JSON.parse(JSON.stringify(initialVarsRef.current))) + setShowUnsavedChanges(false) + if (pendingClose.current) { + onOpenChange(false) + } + } + + const handleSave = () => { + try { + // Close modal immediately for optimistic updates + setShowUnsavedChanges(false) + onOpenChange(false) + + // Convert valid env vars to Record + const validVariables = envVars + .filter((v) => v.key && v.value) + .reduce( + (acc, { key, value }) => ({ + ...acc, + [key]: value, + }), + {} + ) + + // Single store update that triggers sync + useEnvironmentStore.getState().setVariables(validVariables) + } catch (error) { + console.error('Failed to save environment variables:', error) + } + } + + // UI rendering const renderEnvVarRow = (envVar: UIEnvironmentVariable, index: number) => (
{ + return Object.values(workflows).sort((a, b) => { + // Ensure we're comparing dates properly by converting to timestamps + const dateA = + a.lastModified instanceof Date + ? a.lastModified.getTime() + : new Date(a.lastModified).getTime() + const dateB = + b.lastModified instanceof Date + ? b.lastModified.getTime() + : new Date(b.lastModified).getTime() + return dateA - dateB // Ascending order (oldest first, newest last) + }) + }, [workflows]) + const handleCreateWorkflow = () => { const id = createWorkflow() router.push(`/w/${id}`) @@ -50,7 +67,7 @@ export function Sidebar() { {/* Scrollable workflows section */}