260 lines
8.1 KiB
TypeScript
260 lines
8.1 KiB
TypeScript
/**
|
||
* 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,
|
||
editingWorkflows,
|
||
} = 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
|
||
const handleContinueWorkflow = (workflow: Workflow) => {
|
||
navigate(`/workflows/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
|
||
const handleDelete = async (workflow: Workflow) => {
|
||
if (window.confirm(`Möchten Sie den Workflow "${workflow.name || workflow.id}" wirklich löschen?`)) {
|
||
const success = await handleWorkflowDelete(workflow.id);
|
||
if (success) {
|
||
refetch();
|
||
}
|
||
}
|
||
};
|
||
|
||
// Handle delete multiple workflows
|
||
const handleDeleteMultiple = async (workflowsToDelete: Workflow[]) => {
|
||
const count = workflowsToDelete.length;
|
||
if (window.confirm(`Möchten Sie ${count} Workflow(s) wirklich löschen?`)) {
|
||
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}
|
||
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;
|