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/
|
* Endpoint: GET /api/workflows/
|
||||||
*/
|
*/
|
||||||
export async function fetchWorkflows(request: ApiRequestFunction): Promise<Workflow[]> {
|
export async function fetchWorkflows(request: ApiRequestFunction): Promise<Workflow[]> {
|
||||||
const data = await request<Workflow[]>({
|
console.log('📤 fetchWorkflows: Making API request to /api/workflows/');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await request<any>({
|
||||||
url: '/api/workflows/',
|
url: '/api/workflows/',
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
return Array.isArray(data) ? data : [];
|
|
||||||
|
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') {
|
if (mode === 'workflow') {
|
||||||
// Workflow mode: select workflow and navigate
|
// Workflow mode: reset workflow state and select workflow
|
||||||
const workflowId = (row as any)[idField];
|
const workflowId = (row as any)[idField];
|
||||||
if (!workflowId) {
|
if (!workflowId) {
|
||||||
console.error('Workflow ID not found in row');
|
console.error('Workflow ID not found in row');
|
||||||
return;
|
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);
|
selectWorkflow(workflowId);
|
||||||
|
|
||||||
|
// Also dispatch workflowSelected event for any other listeners
|
||||||
|
window.dispatchEvent(new CustomEvent('workflowSelected', {
|
||||||
|
detail: { workflowId }
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
// Prompt mode: set input value in dashboard
|
// Prompt mode: set input value in dashboard
|
||||||
const content = (row as any)[contentField];
|
const content = (row as any)[contentField];
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,7 @@ export const workflowsPageData: GenericPageData = {
|
||||||
idField: 'id',
|
idField: 'id',
|
||||||
nameField: 'name',
|
nameField: 'name',
|
||||||
navigateTo: 'start/dashboard',
|
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')
|
// Only show if user has read permission (permissions.read !== 'n')
|
||||||
disabled: (hookData: any) => {
|
disabled: (hookData: any) => {
|
||||||
if (!hookData?.permissions) return { disabled: false };
|
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 { useApiRequest } from '../useApi';
|
||||||
import { useWorkflowSelection } from '../../contexts/WorkflowSelectionContext';
|
import { useWorkflowSelection } from '../../contexts/WorkflowSelectionContext';
|
||||||
import { useFileContext } from '../../contexts/FileContext';
|
import { useFileContext } from '../../contexts/FileContext';
|
||||||
|
|
@ -52,6 +52,9 @@ export function useDashboardInputForm() {
|
||||||
setWorkflowStatusOptimistic
|
setWorkflowStatusOptimistic
|
||||||
} = useWorkflowLifecycle();
|
} = useWorkflowLifecycle();
|
||||||
|
|
||||||
|
// Ref to prevent infinite sync loops
|
||||||
|
const isSyncingRef = useRef(false);
|
||||||
|
|
||||||
const fileContext = useFileContext();
|
const fileContext = useFileContext();
|
||||||
const { request } = useApiRequest();
|
const { request } = useApiRequest();
|
||||||
const { prompts, loading: promptsLoading, permissions: promptsPermissions, fetchPromptById } = usePrompts();
|
const { prompts, loading: promptsLoading, permissions: promptsPermissions, fetchPromptById } = usePrompts();
|
||||||
|
|
@ -83,11 +86,38 @@ export function useDashboardInputForm() {
|
||||||
checkPermissions();
|
checkPermissions();
|
||||||
}, [canView, checkPermission]);
|
}, [canView, checkPermission]);
|
||||||
|
|
||||||
|
// Sync context -> lifecycle: When context selection changes, update lifecycle
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isSyncingRef.current) return;
|
||||||
|
|
||||||
if (selectedWorkflowId && selectedWorkflowId !== workflowId) {
|
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(() => {
|
useEffect(() => {
|
||||||
const handleSetInput = (event: CustomEvent<{ value: string }>) => {
|
const handleSetInput = (event: CustomEvent<{ value: string }>) => {
|
||||||
|
|
@ -425,7 +455,7 @@ export function useDashboardInputForm() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedMode = workflowMode || 'Automation';
|
const selectedMode = workflowMode || 'Dynamic';
|
||||||
const apiWorkflowMode: 'Dynamic' | 'Automation' = selectedMode;
|
const apiWorkflowMode: 'Dynamic' | 'Automation' = selectedMode;
|
||||||
|
|
||||||
const workflowOptions: { workflowId?: string; workflowMode: 'Dynamic' | 'Automation' } = {
|
const workflowOptions: { workflowId?: string; workflowMode: 'Dynamic' | 'Automation' } = {
|
||||||
|
|
@ -451,15 +481,22 @@ export function useDashboardInputForm() {
|
||||||
if (wasNewWorkflow && result.data) {
|
if (wasNewWorkflow && result.data) {
|
||||||
const workflow = result.data as Workflow;
|
const workflow = result.data as Workflow;
|
||||||
|
|
||||||
|
// Dispatch event first to trigger refetch in useWorkflows
|
||||||
window.dispatchEvent(new CustomEvent('workflowCreated', {
|
window.dispatchEvent(new CustomEvent('workflowCreated', {
|
||||||
detail: { workflow }
|
detail: { workflow }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Refetch workflows list to ensure dropdown is updated
|
||||||
await refetchWorkflows();
|
await refetchWorkflows();
|
||||||
|
|
||||||
|
// Update context first (this will trigger the sync effect to update lifecycle)
|
||||||
selectWorkflowFromContext(workflow.id);
|
selectWorkflowFromContext(workflow.id);
|
||||||
|
|
||||||
|
// Also directly update lifecycle to ensure immediate state update
|
||||||
await selectWorkflow(workflow.id);
|
await selectWorkflow(workflow.id);
|
||||||
} else if (workflowId) {
|
} 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);
|
await selectWorkflow(workflowId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -478,15 +515,20 @@ export function useDashboardInputForm() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleWorkflowCleared = () => {
|
const handleWorkflowCleared = () => {
|
||||||
|
// Reset all workflow-related state
|
||||||
setPendingFiles([]);
|
setPendingFiles([]);
|
||||||
setOptimisticMessage(null);
|
setOptimisticMessage(null);
|
||||||
|
// Reset workflow lifecycle state
|
||||||
|
resetWorkflow();
|
||||||
|
// Clear context selection
|
||||||
|
clearWorkflowFromContext();
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('workflowCleared', handleWorkflowCleared);
|
window.addEventListener('workflowCleared', handleWorkflowCleared);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('workflowCleared', handleWorkflowCleared);
|
window.removeEventListener('workflowCleared', handleWorkflowCleared);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [resetWorkflow, clearWorkflowFromContext]);
|
||||||
|
|
||||||
const handleWorkflowSelect = useCallback(async (item: { id: string | number; label: string; value: any; metadata?: Record<string, any> } | null) => {
|
const handleWorkflowSelect = useCallback(async (item: { id: string | number; label: string; value: any; metadata?: Record<string, any> } | null) => {
|
||||||
if (item === null) {
|
if (item === null) {
|
||||||
|
|
@ -543,11 +585,19 @@ export function useDashboardInputForm() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const workflowItems = useMemo(() => {
|
const workflowItems = useMemo(() => {
|
||||||
|
console.log('🔄 useDashboardInputForm: Computing workflowItems from workflows:', workflows);
|
||||||
|
|
||||||
if (!workflows || !Array.isArray(workflows)) {
|
if (!workflows || !Array.isArray(workflows)) {
|
||||||
|
console.warn('⚠️ useDashboardInputForm: workflows is not an array:', workflows);
|
||||||
return [];
|
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,
|
id: workflow.id,
|
||||||
label: workflow.name || workflow.id,
|
label: workflow.name || workflow.id,
|
||||||
value: workflow,
|
value: workflow,
|
||||||
|
|
@ -556,6 +606,9 @@ export function useDashboardInputForm() {
|
||||||
workflowMode: workflow.workflowMode
|
workflowMode: workflow.workflowMode
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
console.log(`✅ useDashboardInputForm: Created ${items.length} workflow items:`, items);
|
||||||
|
return items;
|
||||||
}, [workflows]);
|
}, [workflows]);
|
||||||
|
|
||||||
const promptItems = useMemo(() => {
|
const promptItems = useMemo(() => {
|
||||||
|
|
@ -606,7 +659,7 @@ export function useDashboardInputForm() {
|
||||||
messages: displayMessages || [],
|
messages: displayMessages || [],
|
||||||
logs: logs || [],
|
logs: logs || [],
|
||||||
workflowItems,
|
workflowItems,
|
||||||
selectedWorkflowId: selectedWorkflowId || workflowId || null,
|
selectedWorkflowId: workflowId || selectedWorkflowId || null,
|
||||||
onWorkflowSelect: handleWorkflowSelect,
|
onWorkflowSelect: handleWorkflowSelect,
|
||||||
workflowsLoading,
|
workflowsLoading,
|
||||||
promptItems,
|
promptItems,
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,19 @@ export function useWorkflows() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
|
console.log('🔄 useWorkflows: Fetching workflows from API...');
|
||||||
const workflowList = await fetchWorkflowsApi(request);
|
const workflowList = await fetchWorkflowsApi(request);
|
||||||
|
console.log('✅ useWorkflows: Fetched workflows:', workflowList);
|
||||||
|
|
||||||
|
if (Array.isArray(workflowList)) {
|
||||||
setWorkflows(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) {
|
} catch (error: any) {
|
||||||
|
console.error('❌ useWorkflows: Error fetching workflows:', error);
|
||||||
setError(error.message || 'Failed to fetch workflows');
|
setError(error.message || 'Failed to fetch workflows');
|
||||||
setWorkflows([]);
|
setWorkflows([]);
|
||||||
} finally {
|
} 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('workflowDeleted', handleWorkflowDeleted as EventListener);
|
||||||
|
window.addEventListener('workflowCreated', handleWorkflowCreated as EventListener);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('workflowDeleted', handleWorkflowDeleted as EventListener);
|
window.removeEventListener('workflowDeleted', handleWorkflowDeleted as EventListener);
|
||||||
|
window.removeEventListener('workflowCreated', handleWorkflowCreated as EventListener);
|
||||||
};
|
};
|
||||||
}, [fetchWorkflows, selectedWorkflowId, clearWorkflow]);
|
}, [fetchWorkflows, selectedWorkflowId, clearWorkflow]);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue