frontend_nyla/src/pages/workflows/WorkflowsPage.tsx
2026-02-09 23:45:05 +01:00

257 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* WorkflowsPage
*
* Page for viewing and managing workflows using FormGeneratorTable.
* Follows the pattern established in AdminUsersPage.
*/
import React, { useState, useMemo, useEffect } from 'react';
import { useUserWorkflows, useWorkflowOperations } from '../../hooks/useWorkflows';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
import { FaSync, FaList, FaPlay } from 'react-icons/fa';
import { useNavigate } from 'react-router-dom';
import styles from '../admin/Admin.module.css';
interface Workflow {
id: string;
name?: string;
status: string;
workflowMode?: string;
[key: string]: any;
}
export const WorkflowsPage: React.FC = () => {
const navigate = useNavigate();
// Data hook
const {
data: workflows,
attributes,
permissions,
pagination,
loading,
error,
refetch,
fetchWorkflowById,
updateOptimistically,
} = useUserWorkflows();
// Operations hook
const {
handleWorkflowDelete,
handleWorkflowDeleteMultiple,
handleWorkflowUpdate,
handleInlineUpdate,
deletingWorkflows,
} = useWorkflowOperations();
const [editingWorkflow, setEditingWorkflow] = useState<Workflow | null>(null);
// Initial fetch on mount
useEffect(() => {
refetch();
}, []);
// Generate columns from attributes
const columns = useMemo(() => {
return (attributes || []).map(attr => ({
key: attr.name,
label: attr.label || attr.name,
type: attr.type as any,
sortable: attr.sortable !== false,
filterable: attr.filterable !== false,
searchable: attr.searchable !== false,
width: attr.width || 150,
minWidth: attr.minWidth || 100,
maxWidth: attr.maxWidth || 400,
fkSource: (attr as any).fkSource,
fkDisplayField: (attr as any).fkDisplayField,
}));
}, [attributes]);
// Check permissions
const canUpdate = permissions?.update !== 'n';
const canDelete = permissions?.delete !== 'n';
// Handle edit click - fetch full workflow data
const handleEditClick = async (workflow: Workflow) => {
const fullWorkflow = await fetchWorkflowById(workflow.id);
if (fullWorkflow) {
setEditingWorkflow(fullWorkflow as Workflow);
}
};
// Handle continue workflow - navigate to playground within same feature instance
// Uses relative navigation since WorkflowsPage is rendered under same instance route as playground
const handleContinueWorkflow = (workflow: Workflow) => {
// Navigate relatively to playground (sibling route under same instance)
navigate(`../playground?workflowId=${workflow.id}`);
};
// Handle edit submit
const handleEditSubmit = async (data: Partial<Workflow>) => {
if (!editingWorkflow) return;
const result = await handleWorkflowUpdate(editingWorkflow.id, data);
if (result.success) {
setEditingWorkflow(null);
refetch();
}
};
// Handle delete single workflow (confirmation handled by DeleteActionButton)
const handleDelete = async (workflow: Workflow) => {
const success = await handleWorkflowDelete(workflow.id);
if (success) {
refetch();
}
};
// Handle delete multiple workflows (confirmation handled by FormGenerator)
const handleDeleteMultiple = async (workflowsToDelete: Workflow[]) => {
const ids = workflowsToDelete.map(w => w.id);
const success = await handleWorkflowDeleteMultiple(ids);
if (success) {
refetch();
}
};
// Form attributes for edit modal - filter out non-editable fields
const formAttributes = useMemo(() => {
const excludedFields = ['id', 'mandateId', '_createdBy', '_createdAt', '_modifiedAt'];
return (attributes || [])
.filter(attr => !excludedFields.includes(attr.name));
}, [attributes]);
if (error) {
return (
<div className={styles.adminPage}>
<div className={styles.errorContainer}>
<span className={styles.errorIcon}></span>
<p className={styles.errorMessage}>Fehler beim Laden der Workflows: {error}</p>
<button className={styles.secondaryButton} onClick={() => refetch()}>
<FaSync /> Erneut versuchen
</button>
</div>
</div>
);
}
return (
<div className={styles.adminPage}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>Workflows</h1>
<p className={styles.pageSubtitle}>Übersicht aller Workflows</p>
</div>
<div className={styles.headerActions}>
<button
className={styles.secondaryButton}
onClick={() => refetch()}
disabled={loading}
>
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
</button>
</div>
</div>
<div className={styles.tableContainer}>
{loading && (!workflows || workflows.length === 0) ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>Lade Workflows...</span>
</div>
) : !workflows || workflows.length === 0 ? (
<div className={styles.emptyState}>
<FaList className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>Keine Workflows vorhanden</h3>
<p className={styles.emptyDescription}>
Starten Sie einen neuen Workflow im Chat Playground.
</p>
</div>
) : (
<FormGeneratorTable
data={workflows}
columns={columns}
apiEndpoint="/api/workflows/"
loading={loading}
pagination={true}
pageSize={25}
searchable={true}
filterable={true}
sortable={true}
selectable={true}
actionButtons={[
...(canUpdate ? [{
type: 'edit' as const,
onAction: handleEditClick,
title: 'Bearbeiten',
}] : []),
...(canDelete ? [{
type: 'delete' as const,
title: 'Löschen',
loading: (row: Workflow) => deletingWorkflows.has(row.id),
}] : []),
]}
customActions={[
{
id: 'continue',
icon: <FaPlay />,
onClick: handleContinueWorkflow,
title: 'Workflow fortsetzen',
}
]}
onDelete={handleDelete}
onDeleteMultiple={handleDeleteMultiple}
hookData={{
refetch,
permissions,
pagination,
handleDelete: handleWorkflowDelete,
handleInlineUpdate,
updateOptimistically,
}}
emptyMessage="Keine Workflows gefunden"
/>
)}
</div>
{/* Edit Modal */}
{editingWorkflow && (
<div className={styles.modalOverlay} onClick={() => setEditingWorkflow(null)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>Workflow bearbeiten</h2>
<button
className={styles.modalClose}
onClick={() => setEditingWorkflow(null)}
>
</button>
</div>
<div className={styles.modalContent}>
{formAttributes.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>Lade Formular...</span>
</div>
) : (
<FormGeneratorForm
attributes={formAttributes}
data={editingWorkflow}
mode="edit"
onSubmit={handleEditSubmit}
onCancel={() => setEditingWorkflow(null)}
submitButtonText="Speichern"
cancelButtonText="Abbrechen"
/>
)}
</div>
</div>
</div>
)}
</div>
);
};
export default WorkflowsPage;