/** * GraphicalEditorWorkflowsTasksPage * Tasks only (no workflow grouping). * Open tasks at top, completed tasks at bottom (expandable, scrollable). * Each task shows workflow, created, due, step, type, and action. * Right column: active workflows with manual or form entry point — start via execute (same as Workflows page). */ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Link } from 'react-router-dom'; import { FaChevronDown, FaChevronRight, FaPlay, FaSpinner, FaTimes, FaUpload } from 'react-icons/fa'; import { useInstanceId } from '../../../hooks/useCurrentInstance'; import { useApiRequest } from '../../../hooks/useApi'; import { fetchTasks, cancelPendingTaskStopRun, completeTask, fetchCompletedRuns, fetchWorkflows, executeGraph, loadClickupListTasksForDropdown, type Automation2Task, type Automation2Workflow, type CompletedRun, type ApiRequestFunction, } from '../../../api/workflowApi'; import { useToast } from '../../../contexts/ToastContext'; import { Popup } from '../../../components/UiComponents/Popup'; import { getAcceptStringFromConfig, fileMatchesAccept } from '../../../components/FlowEditor'; import { useFileOperations } from '../../../hooks/useFiles'; import styles from './Automation2WorkflowsTasks.module.css'; import { normalizeFormFieldOptions } from '../../../components/FlowEditor/nodes/form'; import { useLanguage } from '../../../providers/language/LanguageContext'; function _nodeTypeLabel(nodeType: string, t: (k: string) => string): string { switch (nodeType) { case 'input.form': return t('Formular'); case 'input.approval': return t('Genehmigung'); case 'input.upload': return t('Upload'); case 'input.comment': return t('Kommentar'); case 'input.review': return t('Prüfung'); case 'input.selection': return t('Auswahl'); case 'input.confirmation': return t('Bestätigung'); default: return nodeType; } } function formatTimestamp(ts?: number): string { if (ts == null || ts <= 0) return '—'; const d = new Date(typeof ts === 'number' && ts < 1e12 ? ts * 1000 : ts); return d.toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }); } function getNodeStepLabel(config: Record): string { const title = config?.title; if (typeof title === 'string' && title.trim()) return title; const label = config?.label; if (typeof label === 'string' && label.trim()) return label; if (typeof label === 'object' && label != null && 'de' in (label as Record)) { return (label as Record).de ?? (label as Record).en ?? ''; } return ''; } /** Active workflow with at least one enabled manual or form start (same idea as Tasks / editor on-demand). */ function hasManualOrFormInvocation(wf: Automation2Workflow): boolean { const invs = wf.invocations || []; return invs.some( (i) => i.enabled !== false && (i.kind === 'manual' || i.kind === 'form') ); } /** * Primary entry for execute — POST /api/workflows/{instanceId}/execute with collected inputs. * (manual first, then form or api). */ function getPrimaryEntryPoint(wf: Automation2Workflow) { const invs = wf.invocations || []; return ( invs.find((i) => i.enabled !== false && i.kind === 'manual') || invs.find((i) => i.enabled !== false && (i.kind === 'form' || i.kind === 'api')) ); } function primaryKindLabel(kind: string): string { if (kind === 'form') return 'Formular'; if (kind === 'manual') return 'Manuell'; return kind; } export const GraphicalEditorWorkflowsTasksPage: React.FC = () => { const { t } = useLanguage(); const instanceId = useInstanceId(); const { request } = useApiRequest(); const { showSuccess, showError } = useToast(); const [tasks, setTasks] = useState([]); const [completedRuns, setCompletedRuns] = useState([]); const [startableWorkflows, setStartableWorkflows] = useState([]); const [loading, setLoading] = useState(true); const [completedExpanded, setCompletedExpanded] = useState(false); const [outputExpanded, setOutputExpanded] = useState(true); const [submitting, setSubmitting] = useState(null); const [dismissingTaskId, setDismissingTaskId] = useState(null); const [executingWorkflowId, setExecutingWorkflowId] = useState(null); const load = useCallback(async () => { if (!instanceId) return; setLoading(true); try { const [taskList, runs] = await Promise.all([ fetchTasks(request, instanceId), fetchCompletedRuns(request, instanceId, 20), ]); setTasks(taskList); setCompletedRuns(runs); try { const activeWfs = await fetchWorkflows(request, instanceId, { active: true }); const list: Automation2Workflow[] = Array.isArray(activeWfs) ? activeWfs : (activeWfs && typeof activeWfs === 'object' && 'items' in activeWfs && Array.isArray((activeWfs as { items: Automation2Workflow[] }).items) ? (activeWfs as { items: Automation2Workflow[] }).items : []); setStartableWorkflows( list.filter( (w) => w.active !== false && hasManualOrFormInvocation(w) ) ); } catch (we) { console.error('[graphicalEditor] load startable workflows failed', we); setStartableWorkflows([]); } } catch (e) { console.error('[graphicalEditor] load failed', e); } finally { setLoading(false); } }, [instanceId, request]); useEffect(() => { load(); }, [load]); const handleComplete = async (taskId: string, result: Record) => { if (!instanceId) return; setSubmitting(taskId); try { await completeTask(request, instanceId, taskId, result); await load(); } catch (e) { console.error('[graphicalEditor] complete failed', e); } finally { setSubmitting(null); } }; const handleDismissOpenTask = async (taskId: string) => { if (!instanceId) return; setDismissingTaskId(taskId); try { const res = await cancelPendingTaskStopRun(request, instanceId, taskId); if (res.success) { showSuccess(t('Ausführung abgebrochen')); await load(); } else { showError(t('Abbrechen fehlgeschlagen')); } } catch (e: unknown) { const msg = (e as { message?: string })?.message ?? t('Abbrechen fehlgeschlagen'); showError(msg); console.error('[graphicalEditor] cancel task failed', e); } finally { setDismissingTaskId(null); } }; const handleStartWorkflow = useCallback( async (wf: Automation2Workflow) => { if (!instanceId || !wf.graph) return; const primary = getPrimaryEntryPoint(wf); setExecutingWorkflowId(wf.id); try { const result = await executeGraph(request, instanceId, wf.graph, wf.id, { ...(primary ? { entryPointId: primary.id } : {}), }); if (result?.success) { if (result?.paused) { showSuccess(t('Workflow gestartet und bei Human Task pausiert.')); } else { showSuccess(t('Workflow gestartet')); } await load(); } else { showError(result?.error || t('Ausführung fehlgeschlagen')); } } catch (e: unknown) { const msg = (e as { message?: string })?.message ?? t('Ausführung fehlgeschlagen'); showError(msg); } finally { setExecutingWorkflowId(null); } }, [instanceId, request, showSuccess, showError, load, t] ); const openTasks = tasks.filter((task) => task.status === 'pending'); const completedTasks = tasks.filter((task) => task.status !== 'pending'); if (!instanceId) { return (

{t('keine Featureinstanz gefunden')}

); } if (loading) { return (

{t('lade Tasks')}

); } return (
{/* Open tasks */}

{t('Offene Tasks')} {openTasks.length > 0 && {openTasks.length}}

{openTasks.length === 0 ? (

{t('keine offenen Tasks')}

) : (
{openTasks.map((task) => ( handleComplete(task.id, result)} submitting={submitting === task.id} showDismiss onDismiss={() => handleDismissOpenTask(task.id)} dismissing={dismissingTaskId === task.id} /> ))}
)}
{/* Completed tasks */}
{completedExpanded && (
{completedTasks.length === 0 ? (

{t('keine erledigten Tasks')}

) : ( completedTasks.map((task) => ( handleComplete(task.id, result)} submitting={submitting === task.id} readOnly /> )) )}
)}
{/* Output – abgeschlossene Workflows mit Ergebnis */}
{outputExpanded && (
{completedRuns.length === 0 ? (

{t('Keine abgeschlossenen Workflows. Führen Sie einen Workflow aus (z.B. im Editor), um hier die Ergebnisse zu sehen.')}

) : ( completedRuns.map((run) => ( )) )}
)}
); }; /** Output card for completed workflow runs – zeigt nur die erstellten Dateien (mit fileId). */ const OutputCard: React.FC<{ run: CompletedRun; instanceId?: string; }> = ({ run }) => { const { t } = useLanguage(); const ts = run.sysModifiedAt ?? run.sysCreatedAt ?? 0; const files: Array<{ name: string; fileId: string }> = []; const nodeOutputs = run.nodeOutputs ?? {}; for (const [, out] of Object.entries(nodeOutputs)) { if (!out || typeof out !== 'object') continue; const o = out as Record; const docs = (o.documents ?? o.documentList ?? []) as Array>; if (!Array.isArray(docs)) continue; for (const d of docs) { const fileId = (d.validationMetadata as Record)?.fileId as string | undefined; if (fileId) { files.push({ name: String(d.documentName ?? d.fileName ?? t('Datei')), fileId, }); } } } return (
{t('Workflow')} {run.workflowLabel || run.workflowId || '—'}
{t('Abgeschlossen')} {formatTimestamp(ts)}
{files.length > 0 ? (
{t('Dateien')}
    {files.map((f, j) => (
  • {f.name}
  • ))}
) : (

{t('kein Output, z.B. Workflow ohne')}

)}
); }; interface TaskCardProps { task: Automation2Task; instanceId?: string; onSubmit: (result: Record) => void; submitting: boolean; readOnly?: boolean; /** Open-task card: show top-right control to cancel run and remove from list. */ showDismiss?: boolean; onDismiss?: () => void; dismissing?: boolean; } /** Check if file matches accept string (e.g. ".pdf,image/*"). */ function relationshipTaskIdFromFormValue(v: unknown): string { if (v && typeof v === 'object' && !Array.isArray(v) && 'add' in v) { const a = (v as { add?: unknown[] }).add; if (Array.isArray(a) && a[0] != null && String(a[0]).trim()) return String(a[0]); } return ''; } function InputFormClickupTaskField({ connectionId, listId, value, onChange, request, }: { connectionId: string; listId: string; value: unknown; onChange: (v: unknown) => void; request: ApiRequestFunction; }) { const { t } = useLanguage(); const [tasks, setTasks] = useState>([]); const [loading, setLoading] = useState(false); const [err, setErr] = useState(null); useEffect(() => { const cid = connectionId.trim(); const lid = listId.trim(); if (!cid || !lid) { setTasks([]); return; } let cancelled = false; setLoading(true); setErr(null); loadClickupListTasksForDropdown(request, cid, lid) .then((rows) => { if (!cancelled) setTasks(rows); }) .catch(() => { if (!cancelled) { setTasks([]); setErr(t('Aufgaben konnten nicht geladen werden.')); } }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [request, connectionId, listId]); const sel = relationshipTaskIdFromFormValue(value); if (!connectionId.trim() || !listId.trim()) { return (

{t('Für dieses Feld sind im Formular-Node ClickUp-Verbindung und Listen-ID gesetzt — bitte Workflow prüfen.')}

); } return ( <> {err ? (

{err}

) : null} {loading ? (

{t('lade Aufgaben')}

) : ( )} ); } const TaskCard: React.FC = ({ task, instanceId, onSubmit, submitting, readOnly = false, showDismiss = false, onDismiss, dismissing = false, }) => { const { t } = useLanguage(); const { request } = useApiRequest(); const { handleFileUpload } = useFileOperations(); const [formData, setFormData] = useState>({}); const [formPopupOpen, setFormPopupOpen] = useState(false); const [uploadedFiles, setUploadedFiles] = useState }>>([]); const [uploading, setUploading] = useState(false); const [uploadError, setUploadError] = useState(null); const fileInputRef = useRef(null); const config = task.config ?? {}; const nodeType = task.nodeType; const stepLabel = getNodeStepLabel(config); useEffect(() => { setUploadedFiles([]); setUploadError(null); }, [task.id]); const renderInput = () => { if (readOnly) return null; switch (nodeType) { case 'input.form': { const fields = (config.fields as Array<{ name: string; type: string; label: string; required?: boolean; options?: unknown; clickupConnectionId?: string; clickupListId?: string; clickupStatusOptions?: Array<{ value: string; label: string }>; }>) ?? []; const requiredFields = fields.filter((f) => f.required); const allRequiredFilled = requiredFields.every((f) => { const v = formData[f.name]; if (f.type === 'boolean') return true; if (f.type === 'clickup_tasks') { return relationshipTaskIdFromFormValue(v) !== ''; } if (f.type === 'clickup_status') { return v !== undefined && v !== null && String(v).trim() !== ''; } if ((f.type === 'select' || f.type === 'enum') && normalizeFormFieldOptions(f.options).some((o) => String(o.value).trim() !== '')) { return v !== undefined && v !== null && String(v).trim() !== ''; } return v !== undefined && v !== null && String(v).trim() !== ''; }); const renderFormControl = ( field: (typeof fields)[number], ): React.ReactNode => { const selectChoices = normalizeFormFieldOptions(field.options).filter( (o) => String(o.value).trim() !== '', ); if (field.type === 'boolean') { return ( setFormData((p) => ({ ...p, [field.name]: e.target.checked })) } /> ); } if (field.type === 'clickup_tasks' && request) { return ( setFormData((p) => ({ ...p, [field.name]: v }))} request={request} /> ); } if ( field.type === 'clickup_status' && Array.isArray(field.clickupStatusOptions) && field.clickupStatusOptions.length > 0 ) { return ( ); } if ((field.type === 'select' || field.type === 'enum') && selectChoices.length > 0) { return ( ); } return ( setFormData((p) => ({ ...p, [field.name]: e.target.value })) } /> ); }; const formContent = (
{fields.map((f) => (
{renderFormControl(f)}
))}
); return ( <> setFormPopupOpen(false)} size="medium" footerContent={ } > {formContent} ); } case 'input.approval': return (
{config.title != null && String(config.title) !== '' &&

{String(config.title)}

} {config.description != null && String(config.description) !== '' &&

{String(config.description)}

}
); case 'input.comment': return (