fix: centralized workflow state management on dashboard page
This commit is contained in:
parent
78889bf963
commit
94e8681e13
5 changed files with 146 additions and 15 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue