import { useState, useEffect, useCallback } from 'react'; import { useApiRequest } from './useApi'; import { fetchAttributes as fetchAttributesApi } from '../api/attributesApi'; import type { AttributeDefinition, ApiRequestFunction } from '../api/attributesApi'; import { fetchWorkflows as fetchWorkflowsFromApi, fetchWorkflow as fetchWorkflowFromApi, deleteWorkflow as deleteWorkflowFromApi, updateWorkflow as updateWorkflowFromApi, } from '../api/workflowApi'; import { useWorkflowSelection } from '../contexts/WorkflowSelectionContext'; import { usePermissions, type UserPermissions } from './usePermissions'; export type StartWorkflowRequest = Record; function _workflowsInstanceIdFromBaseUrl(apiBaseUrl: string | undefined): string | null { if (!apiBaseUrl) return null; const m = apiBaseUrl.match(/^\/api\/workflows\/([^/]+)$/); return m ? m[1] : null; } async function _deleteWorkflowsSequential( request: ApiRequestFunction, instanceId: string, workflowIds: string[], ) { for (const id of workflowIds) { await deleteWorkflowFromApi(request, instanceId, id); } } async function startWorkflowApi( request: ApiRequestFunction, instanceId: string, workflowData: StartWorkflowRequest, options?: { workflowId?: string; workflowMode?: 'Dynamic' | 'Automation' }, ) { return await request({ url: `/api/workflows/${instanceId}/execute`, method: 'post', data: { workflowId: options?.workflowId ?? (workflowData as { workflowId?: string }).workflowId, payload: workflowData, }, }); } async function stopWorkflowApi(request: ApiRequestFunction, instanceId: string, workflowId: string) { await request({ url: `/api/workspace/${instanceId}/${workflowId}/stop`, method: 'post', }); } async function deleteMessageApi( request: ApiRequestFunction, instanceId: string, workflowId: string, messageId: string, ) { await request({ url: `/api/workspace/${instanceId}/workflows/${workflowId}/messages/${messageId}`, method: 'delete', }); } async function deleteFileFromMessageApi( request: ApiRequestFunction, instanceId: string, workflowId: string, messageId: string, fileId: string, ) { await request({ url: `/api/workspace/${instanceId}/workflows/${workflowId}/messages/${messageId}/files/${fileId}`, method: 'delete', }); } // Workflow interface matching backend export interface UserWorkflow { id: string; mandateId: string; status: string; name?: string; workflowMode?: string; [key: string]: any; // Allow additional properties } // Re-export AttributeDefinition from attributesApi export type { AttributeDefinition } from '../api/attributesApi'; // Attribute option interface (from backend) export interface AttributeOption { value: string | number; label: string; } // Pagination parameters export interface PaginationParams { page?: number; pageSize?: number; sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; filters?: Record; search?: string; } /** Get apiBaseUrl from instanceId and featureCode for feature-scoped workflow APIs */ export function getWorkflowApiBaseUrl(instanceId: string | undefined, featureCode: string | undefined): string | undefined { if (!instanceId || !featureCode) return undefined; if (featureCode === 'graphicalEditor') return `/api/workflows/${instanceId}`; return undefined; } // Workflows list hook - pass instanceId and featureCode when in feature context for feature-scoped API export function useUserWorkflows(options?: { instanceId?: string; featureCode?: string }) { const apiBaseUrl = getWorkflowApiBaseUrl(options?.instanceId, options?.featureCode); const [workflows, setWorkflows] = useState([]); const [attributes, setAttributes] = useState([]); const [permissions, setPermissions] = useState(null); const [pagination, setPagination] = useState<{ currentPage: number; pageSize: number; totalItems: number; totalPages: number; } | null>(null); const { request, isLoading: loading, error } = useApiRequest(); const { checkPermission } = usePermissions(); // Fetch attributes from backend const fetchAttributes = useCallback(async () => { try { const attrs = await fetchAttributesApi(request, 'ChatWorkflow'); setAttributes(attrs); return attrs; } catch (error: any) { console.error('Error fetching attributes:', error); setAttributes([]); return []; } }, [request]); // Fetch permissions from backend const fetchPermissions = useCallback(async () => { try { const perms = await checkPermission('DATA', 'ChatWorkflow'); setPermissions(perms); return perms; } catch (error: any) { console.error('Error fetching permissions:', error); const defaultPerms: UserPermissions = { view: false, read: 'n', create: 'n', update: 'n', delete: 'n', }; setPermissions(defaultPerms); return defaultPerms; } }, [checkPermission]); const fetchWorkflowsData = useCallback(async (params?: PaginationParams) => { try { if (!apiBaseUrl) { console.error('useUserWorkflows: apiBaseUrl is required (missing instanceId/featureCode)'); return; } const instanceId = _workflowsInstanceIdFromBaseUrl(apiBaseUrl); if (!instanceId) { console.error('useUserWorkflows: could not parse instanceId from apiBaseUrl'); return; } let listParams: { pagination?: Record } | undefined = undefined; if (params) { const paginationObj: Record = {}; if (params.page !== undefined) paginationObj.page = params.page; if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize; if (params.sort) paginationObj.sort = params.sort; if (params.filters) paginationObj.filters = params.filters; if (params.search) paginationObj.search = params.search; if (Object.keys(paginationObj).length > 0) { listParams = { pagination: paginationObj }; } } const data: unknown = await fetchWorkflowsFromApi(request, instanceId, listParams); // Handle paginated response if (data && typeof data === 'object' && data !== null && 'items' in data) { const d = data as { items?: unknown; pagination?: unknown }; const items = Array.isArray(d.items) ? d.items : []; // Map API response to our frontend model const mappedWorkflows = items.map((apiWorkflow: any): UserWorkflow => { return { id: apiWorkflow.id, mandateId: apiWorkflow.mandateId || '', status: apiWorkflow.status || 'unknown', name: apiWorkflow.name, workflowMode: apiWorkflow.workflowMode, ...apiWorkflow // Include any additional properties }; }); setWorkflows(mappedWorkflows); if (d.pagination && typeof d.pagination === 'object') { setPagination(d.pagination as any); } } else { // Handle non-paginated response (backward compatibility) const items = Array.isArray(data) ? data : []; const mappedWorkflows = items.map((apiWorkflow: any): UserWorkflow => { return { id: apiWorkflow.id, mandateId: apiWorkflow.mandateId || '', status: apiWorkflow.status || 'unknown', name: apiWorkflow.name, workflowMode: apiWorkflow.workflowMode, ...apiWorkflow }; }); setWorkflows(mappedWorkflows); setPagination(null); } } catch (error: any) { // Error is already handled by useApiRequest setWorkflows([]); setPagination(null); } }, [request, apiBaseUrl]); // Optimistically remove a workflow from the local state const removeOptimistically = (workflowId: string) => { setWorkflows(prevWorkflows => prevWorkflows.filter(workflow => workflow.id !== workflowId)); }; // Optimistically update a workflow in the local state const updateOptimistically = (workflowId: string, updateData: Partial) => { setWorkflows(prevWorkflows => prevWorkflows.map(workflow => workflow.id === workflowId ? { ...workflow, ...updateData } : workflow ) ); }; // Fetch a single workflow by ID const fetchWorkflowById = useCallback(async (workflowId: string): Promise => { try { const instanceId = _workflowsInstanceIdFromBaseUrl(apiBaseUrl); if (!instanceId) return null; const workflow = await fetchWorkflowFromApi(request, instanceId, workflowId); return workflow as unknown as UserWorkflow | null; } catch (error: any) { console.error('Error fetching workflow by ID:', error); return null; } }, [request, apiBaseUrl]); // Generate edit fields from attributes dynamically const generateEditFieldsFromAttributes = useCallback((): Array<{ key: string; label: string; type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly'; editable?: boolean; required?: boolean; validator?: (value: any) => string | null; minRows?: number; maxRows?: number; options?: Array<{ value: string | number; label: string }>; optionsReference?: string; // For options that need to be fetched (e.g., "user.role") }> => { if (!attributes || attributes.length === 0) { return []; } const editableFields = attributes .filter(attr => { // Filter out non-editable fields based on readonly/editable flags if (attr.readonly === true || attr.editable === false) { return false; // Don't show readonly fields in edit form } // Also filter out common non-editable fields const nonEditableFields = ['id', 'mandateId', 'sysCreatedBy', '_hideDelete']; return !nonEditableFields.includes(attr.name); }) .map(attr => { // Map backend attribute type to form field type let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' = 'string'; let options: Array<{ value: string | number; label: string }> | undefined = undefined; let optionsReference: string | undefined = undefined; // Map backend types to form field types if (attr.type === 'checkbox') { fieldType = 'boolean'; } else if (attr.type === 'email') { fieldType = 'email'; } else if (attr.type === 'date') { fieldType = 'date'; } else if (attr.type === 'select') { fieldType = 'enum'; // Handle options - can be array or string reference if (Array.isArray(attr.options)) { options = attr.options.map(opt => ({ value: opt.value, label: opt.label || String(opt.value) })); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } } else if (attr.type === 'multiselect') { fieldType = 'multiselect'; // Handle options - can be array or string reference if (Array.isArray(attr.options)) { options = attr.options.map(opt => ({ value: opt.value, label: opt.label || String(opt.value) })); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } } else if (attr.type === 'textarea') { fieldType = 'textarea'; } else if (attr.type === 'text') { fieldType = (attr as any).multiline === true ? 'textarea' : 'string'; } // Note: Legacy 'boolean' and 'enum' types are not in the AttributeDefinition type union // If needed, they should be handled via type casting: (attr as any).type === 'boolean' // Define validators and required fields let required = attr.required === true; let validator: ((value: any) => string | null) | undefined = undefined; let minRows: number | undefined = undefined; let maxRows: number | undefined = undefined; if (attr.name === 'name') { required = true; validator = (value: any) => { if (!value || (typeof value === 'string' && value.trim() === '')) { return 'Workflow name cannot be empty'; } if (typeof value === 'string' && value.length > 100) { return 'Workflow name cannot exceed 100 characters'; } return null; }; } else if (fieldType === 'textarea') { minRows = 4; maxRows = 8; } // Multiselect validation else if (fieldType === 'multiselect' && required) { validator = (value: any[]) => { if (!value || !Array.isArray(value) || value.length === 0) { return `${attr.label} is required`; } return null; }; } return { key: attr.name, label: attr.label || attr.name, type: fieldType, editable: attr.editable !== false && attr.readonly !== true, required, validator, minRows, maxRows, options, optionsReference }; }); return editableFields; }, [attributes]); // Ensure attributes are loaded - can be called by EditActionButton const ensureAttributesLoaded = useCallback(async () => { // If attributes are already loaded, return them if (attributes && attributes.length > 0) { return attributes; } // Otherwise, fetch them and return the result const fetchedAttributes = await fetchAttributes(); return fetchedAttributes; }, [attributes, fetchAttributes]); // Fetch attributes and permissions on mount // Note: Do NOT fetch workflows here - let the table component control pagination useEffect(() => { fetchAttributes(); fetchPermissions(); }, [fetchAttributes, fetchPermissions]); // Listen for workflow creation events to refetch workflows list useEffect(() => { const handleWorkflowCreated = (_event: CustomEvent<{ workflow: UserWorkflow }>) => { // Refetch to ensure we have the latest data fetchWorkflowsData(); }; window.addEventListener('workflowCreated', handleWorkflowCreated as EventListener); return () => { window.removeEventListener('workflowCreated', handleWorkflowCreated as EventListener); }; }, [fetchWorkflowsData]); return { data: workflows, loading, error, refetch: fetchWorkflowsData, removeOptimistically, updateOptimistically, attributes, permissions, pagination, fetchWorkflowById, generateEditFieldsFromAttributes, ensureAttributesLoaded }; } // Workflow operations hook - pass instanceId and featureCode when in feature context for feature-scoped API export function useWorkflowOperations(options?: { instanceId?: string; featureCode?: string }) { const instanceId = options?.instanceId; const [startingWorkflow, setStartingWorkflow] = useState(false); const [stoppingWorkflows, setStoppingWorkflows] = useState>(new Set()); const [deletingWorkflows, setDeletingWorkflows] = useState>(new Set()); const [editingWorkflows, setEditingWorkflows] = useState>(new Set()); const [deletingMessages, setDeletingMessages] = useState>(new Set()); const [deletingFiles, setDeletingFiles] = useState>(new Set()); const [startError, setStartError] = useState(null); const [stopError, setStopError] = useState(null); const [deleteError, setDeleteError] = useState(null); const [updateError, setUpdateError] = useState(null); const [deleteMessageError, setDeleteMessageError] = useState(null); const [deleteFileError, setDeleteFileError] = useState(null); // Workflow selection context - to clear selection if deleted workflow is selected const { selectedWorkflowId, clearWorkflow } = useWorkflowSelection(); const { request } = useApiRequest(); // Generic delete operation handler const handleDeleteOperation = async ( operationKey: string, setLoadingSet: React.Dispatch>>, setErrorState: React.Dispatch>, operation: () => Promise, errorMessages: { default: string; notFound: string; forbidden: string } ): Promise<{ success: boolean; error?: string }> => { setErrorState(null); setLoadingSet(prev => new Set(prev).add(operationKey)); try { await operation(); return { success: true }; } catch (error: any) { let errorMessage = error.message || errorMessages.default; if (error.response?.status === 404) { errorMessage = errorMessages.notFound; return { success: true }; } else if (error.response?.status === 403) { errorMessage = errorMessages.forbidden; } setErrorState(errorMessage); return { success: false, error: errorMessage }; } finally { setLoadingSet(prev => { const newSet = new Set(prev); newSet.delete(operationKey); return newSet; }); } }; const startWorkflow = async ( instanceId: string, workflowData: StartWorkflowRequest, options?: { workflowId?: string; workflowMode?: 'Dynamic' | 'Automation' } ) => { setStartError(null); setStartingWorkflow(true); try { const response = await startWorkflowApi(request, instanceId, workflowData, options); return { success: true, data: response }; } catch (error: any) { const errorMessage = error.message || 'Failed to start workflow'; setStartError(errorMessage); return { success: false, error: errorMessage }; } finally { setStartingWorkflow(false); } }; const stopWorkflow = async (instanceId: string, workflowId: string) => { setStopError(null); setStoppingWorkflows(prev => new Set(prev).add(workflowId)); try { await stopWorkflowApi(request, instanceId, workflowId); return { success: true }; } catch (error: any) { const errorMessage = error.message || 'Failed to stop workflow'; setStopError(errorMessage); return { success: false, error: errorMessage }; } finally { setStoppingWorkflows(prev => { const newSet = new Set(prev); newSet.delete(workflowId); return newSet; }); } }; const handleWorkflowDelete = async (workflowId: string) => { const result = await handleDeleteOperation( workflowId, setDeletingWorkflows, setDeleteError, () => { if (!instanceId) throw new Error('instanceId required'); return deleteWorkflowFromApi(request, instanceId, workflowId); }, { default: 'Failed to delete workflow', notFound: 'Workflow not found or has already been deleted.', forbidden: 'No permission to delete this workflow.' } ); if (result.success) { // Add a small delay to ensure backend has time to process await new Promise(resolve => setTimeout(resolve, 300)); // Dispatch event to notify other components (e.g., dashboard dropdown) window.dispatchEvent(new CustomEvent('workflowDeleted', { detail: { workflowIds: [workflowId] } })); // Clear workflow selection if the deleted workflow was selected if (selectedWorkflowId === workflowId) { clearWorkflow(); } } return result.success; }; const handleWorkflowDeleteMultiple = async (workflowIds: string[]) => { setDeleteError(null); setDeletingWorkflows(prev => { const newSet = new Set(prev); workflowIds.forEach(id => newSet.add(id)); return newSet; }); try { if (!instanceId) throw new Error('instanceId required'); await _deleteWorkflowsSequential(request, instanceId, workflowIds); // Add a small delay to ensure backend has time to process await new Promise(resolve => setTimeout(resolve, 300)); // Dispatch event to notify other components (e.g., dashboard dropdown) window.dispatchEvent(new CustomEvent('workflowDeleted', { detail: { workflowIds } })); // Clear workflow selection if the selected workflow was deleted if (selectedWorkflowId && workflowIds.includes(selectedWorkflowId)) { clearWorkflow(); } return true; } catch (error: any) { console.error(`❌ Bulk delete failed:`, error); setDeleteError(error.message || 'Bulk delete failed'); return false; } finally { setDeletingWorkflows(prev => { const newSet = new Set(prev); workflowIds.forEach(id => newSet.delete(id)); return newSet; }); } }; const deleteMessage = async (workflowId: string, messageId: string) => { const operationKey = `${workflowId}:${messageId}`; return handleDeleteOperation( operationKey, setDeletingMessages, setDeleteMessageError, () => { if (!instanceId) throw new Error('instanceId required'); return deleteMessageApi(request, instanceId, workflowId, messageId); }, { default: 'Failed to delete message', notFound: 'Message not found or has already been deleted.', forbidden: 'No permission to delete this message.' } ); }; const deleteFileFromMessage = async ( workflowId: string, messageId: string, fileId: string ) => { const operationKey = `${workflowId}:${messageId}:${fileId}`; return handleDeleteOperation( operationKey, setDeletingFiles, setDeleteFileError, () => { if (!instanceId) throw new Error('instanceId required'); return deleteFileFromMessageApi(request, instanceId, workflowId, messageId, fileId); }, { default: 'Failed to delete file', notFound: 'File not found or has already been deleted.', forbidden: 'No permission to delete this file.' } ); }; const handleWorkflowUpdate = async (workflowId: string, updateData: Partial<{ name: string; description?: string; tags?: string[] }>, _originalWorkflowData?: any) => { setUpdateError(null); setEditingWorkflows(prev => new Set(prev).add(workflowId)); try { if (!instanceId) throw new Error('instanceId required'); const updatedWorkflow = await updateWorkflowFromApi(request, instanceId, workflowId, { label: updateData.name, }); return { success: true, workflowData: updatedWorkflow }; } catch (error: any) { console.error(`Update failed for workflow ID ${workflowId}:`, error); const errorMessage = error.response?.data?.message || error.message || 'Failed to update workflow'; const statusCode = error.response?.status; setUpdateError(errorMessage); // Return detailed error information for proper handling return { success: false, error: errorMessage, statusCode, isPermissionError: statusCode === 403, isValidationError: statusCode === 400 }; } finally { setEditingWorkflows(prev => { const newSet = new Set(prev); newSet.delete(workflowId); return newSet; }); } }; // Generic inline update handler for FormGeneratorTable // Must merge changes with existing row data because backend requires full object const handleInlineUpdate = async (workflowId: string, changes: Partial, existingRow?: any) => { if (!existingRow) { throw new Error(`Existing row data required for inline update`); } // Merge changes with existing row data const mergedData = { name: existingRow.name, ...changes }; const result = await handleWorkflowUpdate(workflowId, mergedData); if (!result.success) { throw new Error(result.error || 'Failed to update'); } return result; }; return { // Loading states startingWorkflow, stoppingWorkflows, deletingWorkflows, editingWorkflows, deletingMessages, deletingFiles, // Error states startError, stopError, deleteError, updateError, deleteMessageError, deleteFileError, // Operations startWorkflow, stopWorkflow, handleWorkflowDelete, handleWorkflowDeleteMultiple, handleWorkflowUpdate, handleInlineUpdate, deleteMessage, deleteFileFromMessage }; }