From 94e8681e134d1ad96d511d8fc80363df4f771a3f Mon Sep 17 00:00:00 2001 From: Ida Dittrich Date: Tue, 30 Dec 2025 09:46:04 +0100 Subject: [PATCH] fix: centralized workflow state management on dashboard page --- src/api/workflowApi.ts | 57 +++++++++++++-- .../PlayActionButton/PlayActionButton.tsx | 15 +++- src/core/PageManager/data/pages/workflows.ts | 1 + src/hooks/playground/useDashboardInputForm.ts | 69 ++++++++++++++++--- src/hooks/playground/useWorkflows.ts | 19 ++++- 5 files changed, 146 insertions(+), 15 deletions(-) diff --git a/src/api/workflowApi.ts b/src/api/workflowApi.ts index b57f673..119943e 100644 --- a/src/api/workflowApi.ts +++ b/src/api/workflowApi.ts @@ -72,11 +72,58 @@ export type ApiRequestFunction = (options: ApiRequestOptions) => Promise { - const data = await request({ - url: '/api/workflows/', - method: 'get' - }); - return Array.isArray(data) ? data : []; + console.log('📤 fetchWorkflows: Making API request to /api/workflows/'); + + try { + const data = await request({ + url: '/api/workflows/', + method: 'get' + }); + + console.log('đŸ“Ĩ fetchWorkflows: API response:', data); + + // Handle different response formats + let workflows: Workflow[] = []; + + if (Array.isArray(data)) { + // Direct array response + workflows = data; + } else if (data && typeof data === 'object') { + // Check for common wrapper properties + if (Array.isArray(data.workflows)) { + workflows = data.workflows; + } else if (Array.isArray(data.data)) { + workflows = data.data; + } else if (Array.isArray(data.items)) { + workflows = data.items; + } else if (Array.isArray(data.results)) { + workflows = data.results; + } else { + // Try to find any array property + const keys = Object.keys(data); + for (const key of keys) { + if (Array.isArray(data[key])) { + workflows = data[key]; + console.log(`â„šī¸ fetchWorkflows: Found workflows array in property '${key}'`); + break; + } + } + } + } + + // Validate that we have workflow objects with id property + const validWorkflows = workflows.filter((w: any) => w && typeof w === 'object' && w.id); + + if (validWorkflows.length !== workflows.length) { + console.warn(`âš ī¸ fetchWorkflows: Filtered out ${workflows.length - validWorkflows.length} invalid workflows`); + } + + console.log(`✅ fetchWorkflows: Returning ${validWorkflows.length} valid workflows`); + return validWorkflows; + } catch (error) { + console.error('❌ fetchWorkflows: Error fetching workflows:', error); + throw error; + } } /** diff --git a/src/components/FormGenerator/ActionButtons/PlayActionButton/PlayActionButton.tsx b/src/components/FormGenerator/ActionButtons/PlayActionButton/PlayActionButton.tsx index d3ae4ea..42d0224 100644 --- a/src/components/FormGenerator/ActionButtons/PlayActionButton/PlayActionButton.tsx +++ b/src/components/FormGenerator/ActionButtons/PlayActionButton/PlayActionButton.tsx @@ -55,13 +55,26 @@ export function PlayActionButton({ } if (mode === 'workflow') { - // Workflow mode: select workflow and navigate + // Workflow mode: reset workflow state and select workflow const workflowId = (row as any)[idField]; if (!workflowId) { console.error('Workflow ID not found in row'); return; } + + // Dispatch event to reset workflow state before selecting new one + // This ensures the dashboard resets and loads the selected workflow + window.dispatchEvent(new CustomEvent('workflowCleared', { + detail: { workflowId: null } + })); + + // Select the workflow in context (this will trigger sync in dashboard) selectWorkflow(workflowId); + + // Also dispatch workflowSelected event for any other listeners + window.dispatchEvent(new CustomEvent('workflowSelected', { + detail: { workflowId } + })); } else { // Prompt mode: set input value in dashboard const content = (row as any)[contentField]; diff --git a/src/core/PageManager/data/pages/workflows.ts b/src/core/PageManager/data/pages/workflows.ts index 274539f..5e02cce 100644 --- a/src/core/PageManager/data/pages/workflows.ts +++ b/src/core/PageManager/data/pages/workflows.ts @@ -174,6 +174,7 @@ export const workflowsPageData: GenericPageData = { idField: 'id', nameField: 'name', navigateTo: 'start/dashboard', + mode: 'workflow', // Set mode to 'workflow' to select workflow instead of setting prompt // Only show if user has read permission (permissions.read !== 'n') disabled: (hookData: any) => { if (!hookData?.permissions) return { disabled: false }; diff --git a/src/hooks/playground/useDashboardInputForm.ts b/src/hooks/playground/useDashboardInputForm.ts index bcb325d..8ce1356 100644 --- a/src/hooks/playground/useDashboardInputForm.ts +++ b/src/hooks/playground/useDashboardInputForm.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback, useMemo } from 'react'; +import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { useApiRequest } from '../useApi'; import { useWorkflowSelection } from '../../contexts/WorkflowSelectionContext'; import { useFileContext } from '../../contexts/FileContext'; @@ -52,6 +52,9 @@ export function useDashboardInputForm() { setWorkflowStatusOptimistic } = useWorkflowLifecycle(); + // Ref to prevent infinite sync loops + const isSyncingRef = useRef(false); + const fileContext = useFileContext(); const { request } = useApiRequest(); const { prompts, loading: promptsLoading, permissions: promptsPermissions, fetchPromptById } = usePrompts(); @@ -83,11 +86,38 @@ export function useDashboardInputForm() { checkPermissions(); }, [canView, checkPermission]); + // Sync context -> lifecycle: When context selection changes, update lifecycle useEffect(() => { + if (isSyncingRef.current) return; + if (selectedWorkflowId && selectedWorkflowId !== workflowId) { - selectWorkflow(selectedWorkflowId); + isSyncingRef.current = true; + selectWorkflow(selectedWorkflowId).finally(() => { + isSyncingRef.current = false; + }); + } else if (!selectedWorkflowId && workflowId) { + // If context is cleared but lifecycle still has a workflow, reset lifecycle + isSyncingRef.current = true; + resetWorkflow(); + isSyncingRef.current = false; } - }, [selectedWorkflowId, workflowId, selectWorkflow]); + }, [selectedWorkflowId, workflowId, selectWorkflow, resetWorkflow]); + + // Sync lifecycle -> context: When lifecycle workflowId changes, update context + useEffect(() => { + if (isSyncingRef.current) return; + + if (workflowId && workflowId !== selectedWorkflowId) { + isSyncingRef.current = true; + selectWorkflowFromContext(workflowId); + isSyncingRef.current = false; + } else if (!workflowId && selectedWorkflowId) { + // If lifecycle is cleared but context still has selection, clear context + isSyncingRef.current = true; + clearWorkflowFromContext(); + isSyncingRef.current = false; + } + }, [workflowId, selectedWorkflowId, selectWorkflowFromContext, clearWorkflowFromContext]); useEffect(() => { const handleSetInput = (event: CustomEvent<{ value: string }>) => { @@ -425,7 +455,7 @@ export function useDashboardInputForm() { return; } - const selectedMode = workflowMode || 'Automation'; + const selectedMode = workflowMode || 'Dynamic'; const apiWorkflowMode: 'Dynamic' | 'Automation' = selectedMode; const workflowOptions: { workflowId?: string; workflowMode: 'Dynamic' | 'Automation' } = { @@ -451,15 +481,22 @@ export function useDashboardInputForm() { if (wasNewWorkflow && result.data) { const workflow = result.data as Workflow; + // Dispatch event first to trigger refetch in useWorkflows window.dispatchEvent(new CustomEvent('workflowCreated', { detail: { workflow } })); + // Refetch workflows list to ensure dropdown is updated await refetchWorkflows(); + + // Update context first (this will trigger the sync effect to update lifecycle) selectWorkflowFromContext(workflow.id); + + // Also directly update lifecycle to ensure immediate state update await selectWorkflow(workflow.id); } else if (workflowId) { - // For resumed workflows, selectWorkflow will update status from server + // For resumed workflows, ensure context is synced and update lifecycle + selectWorkflowFromContext(workflowId); await selectWorkflow(workflowId); } } else { @@ -478,15 +515,20 @@ export function useDashboardInputForm() { useEffect(() => { const handleWorkflowCleared = () => { + // Reset all workflow-related state setPendingFiles([]); setOptimisticMessage(null); + // Reset workflow lifecycle state + resetWorkflow(); + // Clear context selection + clearWorkflowFromContext(); }; window.addEventListener('workflowCleared', handleWorkflowCleared); return () => { window.removeEventListener('workflowCleared', handleWorkflowCleared); }; - }, []); + }, [resetWorkflow, clearWorkflowFromContext]); const handleWorkflowSelect = useCallback(async (item: { id: string | number; label: string; value: any; metadata?: Record } | null) => { if (item === null) { @@ -543,11 +585,19 @@ export function useDashboardInputForm() { }, []); const workflowItems = useMemo(() => { + console.log('🔄 useDashboardInputForm: Computing workflowItems from workflows:', workflows); + if (!workflows || !Array.isArray(workflows)) { + console.warn('âš ī¸ useDashboardInputForm: workflows is not an array:', workflows); return []; } - return workflows.map(workflow => ({ + if (workflows.length === 0) { + console.log('â„šī¸ useDashboardInputForm: workflows array is empty'); + return []; + } + + const items = workflows.map(workflow => ({ id: workflow.id, label: workflow.name || workflow.id, value: workflow, @@ -556,6 +606,9 @@ export function useDashboardInputForm() { workflowMode: workflow.workflowMode } })); + + console.log(`✅ useDashboardInputForm: Created ${items.length} workflow items:`, items); + return items; }, [workflows]); const promptItems = useMemo(() => { @@ -606,7 +659,7 @@ export function useDashboardInputForm() { messages: displayMessages || [], logs: logs || [], workflowItems, - selectedWorkflowId: selectedWorkflowId || workflowId || null, + selectedWorkflowId: workflowId || selectedWorkflowId || null, onWorkflowSelect: handleWorkflowSelect, workflowsLoading, promptItems, diff --git a/src/hooks/playground/useWorkflows.ts b/src/hooks/playground/useWorkflows.ts index 43f6cdb..c7cfe6f 100644 --- a/src/hooks/playground/useWorkflows.ts +++ b/src/hooks/playground/useWorkflows.ts @@ -16,9 +16,19 @@ export function useWorkflows() { setLoading(true); setError(null); + console.log('🔄 useWorkflows: Fetching workflows from API...'); const workflowList = await fetchWorkflowsApi(request); - setWorkflows(workflowList); + console.log('✅ useWorkflows: Fetched workflows:', workflowList); + + if (Array.isArray(workflowList)) { + setWorkflows(workflowList); + console.log(`✅ useWorkflows: Set ${workflowList.length} workflows in state`); + } else { + console.warn('âš ī¸ useWorkflows: API returned non-array data:', workflowList); + setWorkflows([]); + } } catch (error: any) { + console.error('❌ useWorkflows: Error fetching workflows:', error); setError(error.message || 'Failed to fetch workflows'); setWorkflows([]); } finally { @@ -39,9 +49,16 @@ export function useWorkflows() { } }; + const handleWorkflowCreated = () => { + // Immediately refetch workflows list to include the newly created workflow + fetchWorkflows(); + }; + window.addEventListener('workflowDeleted', handleWorkflowDeleted as EventListener); + window.addEventListener('workflowCreated', handleWorkflowCreated as EventListener); return () => { window.removeEventListener('workflowDeleted', handleWorkflowDeleted as EventListener); + window.removeEventListener('workflowCreated', handleWorkflowCreated as EventListener); }; }, [fetchWorkflows, selectedWorkflowId, clearWorkflow]);