fix: centralized workflow state management on dashboard page

This commit is contained in:
Ida Dittrich 2025-12-30 09:46:04 +01:00
parent 78889bf963
commit 94e8681e13
5 changed files with 146 additions and 15 deletions

View file

@ -72,11 +72,58 @@ export type ApiRequestFunction = (options: ApiRequestOptions<any>) => Promise<an
* Endpoint: GET /api/workflows/
*/
export async function fetchWorkflows(request: ApiRequestFunction): Promise<Workflow[]> {
const data = await request<Workflow[]>({
url: '/api/workflows/',
method: 'get'
});
return Array.isArray(data) ? data : [];
console.log('📤 fetchWorkflows: Making API request to /api/workflows/');
try {
const data = await request<any>({
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;
}
}
/**

View file

@ -55,13 +55,26 @@ export function PlayActionButton<T = any>({
}
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];

View file

@ -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 };

View file

@ -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<string, any> } | 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,

View file

@ -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]);