finalized workflows page

This commit is contained in:
Ida Dittrich 2025-08-21 18:45:29 +02:00
parent 41a3b8f40e
commit 43951c280a
6 changed files with 580 additions and 363 deletions

View file

@ -44,13 +44,13 @@
.workflowId { .workflowId {
font-family: 'Courier New', monospace; font-family: 'Courier New', monospace;
font-size: 0.9em; font-size: 0.9em;
color: var(--color-text-secondary, #666); color: var(--color-gray);
cursor: help; cursor: help;
} }
.workflowName { .workflowName {
font-weight: 500; font-weight: 500;
color: var(--color-text-primary, #333); color: var(--color-text);
} }
.statusBadge { .statusBadge {
@ -65,21 +65,18 @@
} }
.status-running { .status-running {
background-color: #d4edda; background-color: var(--color-gray);
color: #155724; color: white;
border: 1px solid #c3e6cb;
} }
.status-completed { .status-completed {
background-color: #d1ecf1; background-color: var(--color-secondary);
color: #0c5460; color: white;
border: 1px solid #bee5eb;
} }
.status-failed { .status-failed {
background-color: #f8d7da; background-color: var(--color-red);
color: #721c24; color: white;
border: 1px solid #f5c6cb;
} }
.status-stopped { .status-stopped {
@ -95,11 +92,10 @@
} }
.roundNumber { .roundNumber {
font-weight: 600; font-weight: 400;
color: var(--color-primary, #007bff); color: var(--color-text);
background-color: var(--color-primary-light, #e3f2fd); background-color: var(--color-bg);
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
border-radius: 8px;
font-size: 0.9em; font-size: 0.9em;
min-width: 24px; min-width: 24px;
text-align: center; text-align: center;
@ -108,8 +104,8 @@
.messageCount { .messageCount {
font-weight: 500; font-weight: 500;
color: var(--color-text-secondary, #666); color: var(--color-text);
background-color: var(--color-bg-secondary, #f8f9fa); background-color: var(--color-bg);
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
border-radius: 8px; border-radius: 8px;
font-size: 0.9em; font-size: 0.9em;
@ -139,7 +135,7 @@
/* Dark mode support */ /* Dark mode support */
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.workflowId { .workflowId {
color: #adb5bd; color: var(--color-gray);
} }
.workflowName { .workflowName {
@ -147,43 +143,33 @@
} }
.status-running { .status-running {
background-color: #155724; background-color: var(--color-gray);
color: #d4edda; color: white;
} }
.status-completed { .status-completed {
background-color: #0c5460; background-color: var(--color-secondary);
color: #d1ecf1; color: white;
} }
.status-failed { .status-failed {
background-color: #721c24; background-color: var(--color-red);
color: #f8d7da; color: white;
} }
.status-stopped { .status-stopped {
background-color: #495057; background-color: var(--color-primary);
color: #f5f5f5; color: white;
} }
.status-pending {
background-color: #856404;
color: #fff3cd;
}
.roundNumber { .roundNumber {
background-color: #1e3a8a; background-color: var(--color-bg);
color: #bfdbfe; color: var(--color-text);
} }
.messageCount { .messageCount {
background-color: #374151; background-color: var(--color-gray);
color: #d1d5db; color: white;
}
.errorState {
background-color: #2d1b1e;
border-color: #5c2b33;
color: #f5c6cb;
} }
} }

View file

@ -1,318 +1,22 @@
import { useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { IoIosTrash, IoIosPlay } from 'react-icons/io';
import { MdModeEdit } from 'react-icons/md';
import { FormGenerator, ColumnConfig } from '../FormGenerator/FormGenerator'; import { FormGenerator } from '../FormGenerator/FormGenerator';
import { useWorkflows, useWorkflowOperations, Workflow } from '../../hooks/useWorkflows';
import { useLanguage } from '../../contexts/LanguageContext';
import { Popup, EditForm } from '../Popup'; import { Popup, EditForm } from '../Popup';
import type { EditFieldConfig } from '../Popup/EditForm'; import { useWorkflowsLogic } from './workflowsLogic';
import { WorkflowsTableProps } from './workflowsTypes';
import styles from './WorkflowsTable.module.css'; import styles from './WorkflowsTable.module.css';
interface WorkflowsTableProps {
className?: string;
}
function WorkflowsTable({ className = '' }: WorkflowsTableProps) { function WorkflowsTable({ className = '' }: WorkflowsTableProps) {
const { workflows, loading, error, refetch } = useWorkflows(); const logic = useWorkflowsLogic();
const navigate = useNavigate();
// Debug: Log workflow data to see the actual structure
console.log('Workflows data:', workflows);
const {
deleteWorkflow,
updateWorkflow,
deletingWorkflows
} = useWorkflowOperations();
const { t } = useLanguage();
// Edit modal state
const [editModalOpen, setEditModalOpen] = useState(false);
const [editingWorkflow, setEditingWorkflow] = useState<Workflow | null>(null);
// Configure edit fields for workflow name editing
const editWorkflowFields: EditFieldConfig[] = useMemo(() => [
{
key: 'name',
label: t('workflows.field.name', 'Workflow Name'),
type: 'string',
editable: true,
required: true,
validator: (value: string) => {
if (!value || value.trim() === '') {
return t('workflows.validation.nameRequired', 'Workflow name cannot be empty');
}
if (value.length > 100) {
return t('workflows.validation.nameTooLong', 'Workflow name cannot exceed 100 characters');
}
return null;
}
}
], [t]);
// Configure columns for the workflows table
const columns: ColumnConfig[] = useMemo(() => [
{
key: 'id',
label: t('workflows.column.id'),
type: 'string',
width: 180,
minWidth: 150,
maxWidth: 250,
sortable: true,
filterable: true,
searchable: true,
formatter: (value: string) => (
<span className={styles.workflowId} title={value}>
{value.length > 8 ? `${value.substring(0, 8)}...` : value}
</span>
)
},
{
key: 'name',
label: t('workflows.column.name'),
type: 'string',
width: 200,
minWidth: 150,
maxWidth: 300,
sortable: true,
filterable: true,
searchable: true,
formatter: (value: string | undefined) => (
<span className={styles.workflowName}>
{value || t('workflows.unnamed')}
</span>
)
},
{
key: 'status',
label: t('workflows.column.status'),
type: 'enum',
width: 120,
minWidth: 100,
maxWidth: 150,
sortable: true,
filterable: true,
filterOptions: ['running', 'completed', 'failed', 'stopped', 'pending'],
formatter: (value: string) => (
<span className={`${styles.statusBadge} ${styles[`status-${value}`]}`}>
{t(`workflows.status.${value}`, value)}
</span>
)
},
{
key: 'currentRound',
label: t('workflows.column.round'),
type: 'number',
width: 80,
minWidth: 60,
maxWidth: 100,
sortable: true,
filterable: true,
formatter: (value: number | undefined) => (
<span className={styles.roundNumber}>
{value || 1}
</span>
)
},
{
key: 'startedAt',
label: t('workflows.column.started'),
type: 'date',
width: 140,
minWidth: 120,
maxWidth: 180,
sortable: true,
filterable: true,
formatter: (value: string | undefined) => {
if (!value) return '-';
try {
const date = new Date(value);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} catch {
return value;
}
}
},
{
key: 'lastActivity',
label: t('workflows.column.lastActivity'),
type: 'date',
width: 140,
minWidth: 120,
maxWidth: 180,
sortable: true,
filterable: true,
formatter: (value: string | undefined) => {
if (!value) return '-';
try {
const date = new Date(value);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} catch {
return value;
}
}
},
{
key: 'messages',
label: t('workflows.column.messages'),
type: 'number',
width: 100,
minWidth: 80,
maxWidth: 120,
sortable: true,
filterable: false,
formatter: (value: any[] | undefined) => (
<span className={styles.messageCount}>
{value?.length || 0}
</span>
)
}
], [t]);
// Handle workflow actions
const handleDeleteWorkflow = async (workflow: Workflow) => {
const workflowName = workflow.name || workflow.id;
if (window.confirm(t('workflows.delete.confirm').replace('{name}', workflowName))) {
const success = await deleteWorkflow(workflow.id);
if (success) {
refetch(); // Refresh the workflows list
}
}
};
// Handle single workflow deletion for bulk delete
const handleDeleteSingle = async (workflow: Workflow) => {
const workflowName = workflow.name || workflow.id;
if (window.confirm(t('workflows.delete.confirm', 'Are you sure you want to delete "{name}"?').replace('{name}', workflowName))) {
const success = await deleteWorkflow(workflow.id);
if (success) {
refetch(); // Refresh the workflows list
} else {
console.error('Delete failed for workflow:', workflow.id);
}
}
};
// Handle multiple workflow deletion
const handleDeleteMultiple = async (workflowsToDelete: Workflow[]) => {
const workflowCount = workflowsToDelete.length;
if (window.confirm(t('workflows.delete.confirmMultiple', 'Are you sure you want to delete {count} workflows?').replace('{count}', workflowCount.toString()))) {
// Start all delete operations simultaneously
const deletePromises = workflowsToDelete.map(async (workflow) => {
try {
const success = await deleteWorkflow(workflow.id);
return { workflowId: workflow.id, success };
} catch (error) {
console.error('Failed to delete workflow:', workflow.id, error);
return { workflowId: workflow.id, success: false };
}
});
// Wait for all deletions to complete
const results = await Promise.all(deletePromises);
// Check if any deletions failed
const failedDeletions = results.filter(result => !result.success);
if (failedDeletions.length > 0) {
console.error('Some workflow deletions failed:', failedDeletions);
}
// Refresh the workflow list regardless of individual failures
refetch();
}
};
// Handle edit workflow
const handleEditWorkflow = (workflow: Workflow) => {
setEditingWorkflow(workflow);
setEditModalOpen(true);
};
// Handle save workflow
const handleSaveWorkflow = async (updatedWorkflow: Workflow) => {
if (!editingWorkflow) return;
try {
// Call API to update workflow name
const result = await updateWorkflow(editingWorkflow.id, {
name: updatedWorkflow.name
});
if (result.success) {
// Close modal
setEditModalOpen(false);
setEditingWorkflow(null);
// Refresh workflow list
await refetch();
} else {
console.error('Failed to update workflow:', result.error);
// TODO: Show error message to user
}
} catch (error) {
console.error('Failed to update workflow:', error);
// TODO: Show error message to user
}
};
// Handle cancel edit
const handleCancelEdit = () => {
setEditModalOpen(false);
setEditingWorkflow(null);
};
// Handle play workflow - navigate to dashboard with workflow ID
const handlePlayWorkflow = (workflow: Workflow) => {
// Navigate to dashboard with workflow ID as URL parameter
navigate(`/dashboard?workflowId=${workflow.id}`);
};
// Configure action buttons
const actions = useMemo(() => [
{
label: t('workflows.action.play'),
icon: (_row: Workflow) => {
return <IoIosPlay />;
},
onClick: (row: Workflow) => {
handlePlayWorkflow(row);
}
},
{
label: t('workflows.action.edit'),
icon: (_row: Workflow) => {
return <MdModeEdit />;
},
onClick: (row: Workflow) => {
handleEditWorkflow(row);
}
},
{
label: t('workflows.action.delete'),
icon: (_row: Workflow) => {
return <IoIosTrash />;
},
onClick: (row: Workflow) => {
if (!deletingWorkflows.has(row.id)) {
handleDeleteWorkflow(row);
}
}
},
], [t, deletingWorkflows, handleDeleteWorkflow, handleEditWorkflow, handlePlayWorkflow]);
// Show error state // Show error state
if (error) { if (logic.error) {
return ( return (
<div className={`${styles.workflowsTable} ${className}`}> <div className={`${styles.workflowsTable} ${className}`}>
<div className={styles.errorState}> <div className={styles.errorState}>
<p>{t('workflows.error.loading')} {error}</p> <p>Error loading workflows: {logic.error}</p>
<button onClick={refetch} className={styles.retryButton}> <button onClick={logic.refetch} className={styles.retryButton}>
{t('workflows.button.retry')} Retry
</button> </button>
</div> </div>
</div> </div>
@ -322,20 +26,20 @@ function WorkflowsTable({ className = '' }: WorkflowsTableProps) {
return ( return (
<div className={`${styles.workflowsTable} ${className}`}> <div className={`${styles.workflowsTable} ${className}`}>
<FormGenerator <FormGenerator
data={workflows} data={logic.workflows}
columns={columns} columns={logic.columns}
searchable={true} searchable={true}
filterable={true} filterable={true}
sortable={true} sortable={true}
resizable={true} resizable={true}
pagination={true} pagination={true}
pageSize={10} pageSize={10}
loading={loading} loading={logic.loading}
actions={actions} actions={logic.actions}
onDelete={handleDeleteSingle} onDelete={logic.handleDeleteSingle}
onDeleteMultiple={handleDeleteMultiple} onDeleteMultiple={logic.handleDeleteMultiple}
className={styles.workflowsFormGenerator} className={styles.workflowsFormGenerator}
onRowClick={(workflow: Workflow) => { onRowClick={(workflow: any) => {
// TODO: Navigate to workflow detail view // TODO: Navigate to workflow detail view
console.log('Clicked workflow:', workflow); console.log('Clicked workflow:', workflow);
}} }}
@ -343,19 +47,19 @@ function WorkflowsTable({ className = '' }: WorkflowsTableProps) {
{/* Edit Workflow Modal */} {/* Edit Workflow Modal */}
<Popup <Popup
isOpen={editModalOpen} isOpen={logic.editModalOpen}
title={t('workflows.edit.title', 'Edit Workflow')} title="Edit Workflow"
onClose={handleCancelEdit} onClose={logic.handleCancelEdit}
size="small" size="small"
> >
{editingWorkflow && ( {logic.editingWorkflow && (
<EditForm <EditForm
data={editingWorkflow} data={logic.editingWorkflow}
fields={editWorkflowFields} fields={logic.editWorkflowFields}
onSave={handleSaveWorkflow} onSave={logic.handleSaveWorkflow}
onCancel={handleCancelEdit} onCancel={logic.handleCancelEdit}
saveButtonText={t('common.save', 'Save')} saveButtonText="Save"
cancelButtonText={t('common.cancel', 'Cancel')} cancelButtonText="Cancel"
/> />
)} )}
</Popup> </Popup>

View file

@ -1 +1,3 @@
export { default as WorkflowsTable } from './WorkflowsTable'; export { default as WorkflowsTable } from './WorkflowsTable';
export { useWorkflowsLogic } from './workflowsLogic';
export * from './workflowsTypes';

View file

@ -0,0 +1,454 @@
import { useState, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { IoIosTrash, IoIosPlay } from 'react-icons/io';
import { MdModeEdit } from 'react-icons/md';
import { useWorkflows, useWorkflowOperations, Workflow } from '../../hooks/useWorkflows';
import { useApiRequest } from '../../hooks/useApi';
import { useLanguage } from '../../contexts/LanguageContext';
import type { EditFieldConfig } from '../Popup/EditForm';
import type {
WorkflowsLogicReturn,
WorkflowMessageCounts,
WorkflowActionConfig,
WorkflowColumnConfig
} from './workflowsTypes';
import styles from './WorkflowsTable.module.css';
export function useWorkflowsLogic(): WorkflowsLogicReturn {
const { workflows, loading, error, refetch } = useWorkflows();
const navigate = useNavigate();
const { t } = useLanguage();
// State to track message counts for each workflow
const [workflowMessageCounts, setWorkflowMessageCounts] = useState<WorkflowMessageCounts>({});
const { request } = useApiRequest();
// Debug: Log workflow data to see the actual structure
console.log('Workflows data:', workflows);
if (workflows && workflows.length > 0) {
const firstWorkflow = workflows[0];
console.log('First workflow object:', firstWorkflow);
console.log('First workflow keys:', Object.keys(firstWorkflow));
console.log('First workflow stats:', firstWorkflow.stats);
if (firstWorkflow.stats) {
console.log('Stats keys:', Object.keys(firstWorkflow.stats));
console.log('Stats object:', firstWorkflow.stats);
}
console.log('First workflow messages array:', firstWorkflow.messages, 'length:', firstWorkflow.messages?.length);
}
const {
deleteWorkflow,
updateWorkflow,
deletingWorkflows
} = useWorkflowOperations();
// Edit modal state
const [editModalOpen, setEditModalOpen] = useState(false);
const [editingWorkflow, setEditingWorkflow] = useState<Workflow | null>(null);
// Function to fetch message count for a single workflow
const fetchMessageCount = async (workflowId: string) => {
try {
console.log(`Fetching messages for workflow: ${workflowId}`);
const messages = await request({
url: `/api/workflows/${workflowId}/messages`,
method: 'get'
});
console.log(`Messages for ${workflowId}:`, messages, 'length:', messages?.length);
const count = Array.isArray(messages) ? messages.length : 0;
console.log(`Setting message count for ${workflowId}:`, count);
setWorkflowMessageCounts(prev => ({
...prev,
[workflowId]: count
}));
} catch (error) {
console.error(`Failed to fetch message count for workflow ${workflowId}:`, error);
// Set count to 0 for failed requests
setWorkflowMessageCounts(prev => ({
...prev,
[workflowId]: 0
}));
}
};
// Effect to fetch message counts when workflows change
useEffect(() => {
if (workflows && workflows.length > 0) {
workflows.forEach(workflow => {
// Only fetch if we don't already have the count
if (!(workflow.id in workflowMessageCounts)) {
fetchMessageCount(workflow.id);
}
});
}
}, [workflows]); // Don't include workflowMessageCounts to avoid infinite loop
// Configure edit fields for workflow name editing
const editWorkflowFields: EditFieldConfig[] = useMemo(() => [
{
key: 'name',
label: t('workflows.field.name', 'Workflow Name'),
type: 'string',
editable: true,
required: true,
validator: (value: string) => {
if (!value || value.trim() === '') {
return t('workflows.validation.nameRequired', 'Workflow name cannot be empty');
}
if (value.length > 100) {
return t('workflows.validation.nameTooLong', 'Workflow name cannot exceed 100 characters');
}
return null;
}
}
], [t]);
// Configure columns for the workflows table
const columns: WorkflowColumnConfig[] = useMemo(() => [
{
key: 'id',
label: t('workflows.column.id'),
type: 'string',
width: 180,
minWidth: 150,
maxWidth: 250,
sortable: true,
filterable: true,
searchable: true,
formatter: (value: string) => (
<span className={styles.workflowId} title={value}>
{value.length > 8 ? `${value.substring(0, 8)}...` : value}
</span>
)
},
{
key: 'name',
label: t('workflows.column.name'),
type: 'string',
width: 200,
minWidth: 150,
maxWidth: 300,
sortable: true,
filterable: true,
searchable: true,
formatter: (value: string | undefined) => (
<span className={styles.workflowName}>
{value || t('workflows.unnamed')}
</span>
)
},
{
key: 'status',
label: t('workflows.column.status'),
type: 'enum',
width: 120,
minWidth: 100,
maxWidth: 150,
sortable: true,
filterable: true,
filterOptions: ['running', 'completed', 'failed', 'stopped', 'pending'],
formatter: (value: string) => (
<span className={`${styles.statusBadge} ${styles[`status-${value}`]}`}>
{t(`workflows.status.${value}`, value)}
</span>
)
},
{
key: 'currentRound',
label: t('workflows.column.round'),
type: 'number',
width: 80,
minWidth: 60,
maxWidth: 100,
sortable: true,
filterable: true,
formatter: (value: number | undefined) => (
<span className={styles.roundNumber}>
{value || 1}
</span>
)
},
{
key: 'startedAt',
label: t('workflows.column.started'),
type: 'date',
width: 140,
minWidth: 120,
maxWidth: 180,
sortable: true,
filterable: true,
formatter: (value: string | number | undefined) => {
if (!value) return '-';
try {
let date: Date;
// Handle Unix timestamp (as string or number)
if (typeof value === 'string' && /^\d+$/.test(value)) {
// Unix timestamp as string
date = new Date(parseInt(value) * 1000);
} else if (typeof value === 'number') {
// Unix timestamp as number
date = new Date(value * 1000);
} else {
// ISO string or other date format
date = new Date(value);
}
// Check if date is valid
if (isNaN(date.getTime())) {
console.warn('Invalid startedAt date:', value);
return '-';
}
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} catch (error) {
console.warn('Error parsing startedAt date:', value, error);
return '-';
}
}
},
{
key: 'lastActivity',
label: t('workflows.column.lastActivity'),
type: 'date',
width: 140,
minWidth: 120,
maxWidth: 180,
sortable: true,
filterable: true,
formatter: (value: string | number | undefined) => {
if (!value) return '-';
try {
let date: Date;
// Handle Unix timestamp (as string or number)
if (typeof value === 'string' && /^\d+$/.test(value)) {
// Unix timestamp as string
date = new Date(parseInt(value) * 1000);
} else if (typeof value === 'number') {
// Unix timestamp as number
date = new Date(value * 1000);
} else {
// ISO string or other date format
date = new Date(value);
}
// Check if date is valid
if (isNaN(date.getTime())) {
console.warn('Invalid lastActivity date:', value);
return '-';
}
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} catch (error) {
console.warn('Error parsing lastActivity date:', value, error);
return '-';
}
}
},
{
key: 'messages',
label: t('workflows.column.messages'),
type: 'number',
width: 100,
minWidth: 80,
maxWidth: 120,
sortable: true,
filterable: false,
formatter: (_value: any, row: any) => {
// Get message count from our fetched data, just like in Dashboard component
const workflowId = row?.id;
const messageCount = workflowId ? workflowMessageCounts[workflowId] : undefined;
console.log(`Messages formatter for ${workflowId}:`, {
workflowId,
messageCount,
hasInCache: workflowId in workflowMessageCounts,
allCounts: workflowMessageCounts
});
// Show the count if available, otherwise show loading indicator or dash
let displayValue;
if (messageCount !== undefined) {
displayValue = messageCount;
} else if (workflowId && workflows.some(w => w.id === workflowId)) {
// We're still loading this count
displayValue = '...';
} else {
displayValue = '-';
}
return (
<span className={styles.messageCount}>
{displayValue}
</span>
);
}
}
], [t, workflowMessageCounts, workflows]);
// Handle workflow actions
const handleDeleteWorkflow = async (workflow: Workflow) => {
const workflowName = workflow.name || workflow.id;
if (window.confirm(t('workflows.delete.confirm').replace('{name}', workflowName))) {
const success = await deleteWorkflow(workflow.id);
if (success) {
refetch(); // Refresh the workflows list
}
}
};
// Handle single workflow deletion for bulk delete
const handleDeleteSingle = async (workflow: Workflow) => {
const workflowName = workflow.name || workflow.id;
if (window.confirm(t('workflows.delete.confirm', 'Are you sure you want to delete "{name}"?').replace('{name}', workflowName))) {
const success = await deleteWorkflow(workflow.id);
if (success) {
refetch(); // Refresh the workflows list
} else {
console.error('Delete failed for workflow:', workflow.id);
}
}
};
// Handle multiple workflow deletion
const handleDeleteMultiple = async (workflowsToDelete: Workflow[]) => {
const workflowCount = workflowsToDelete.length;
if (window.confirm(t('workflows.delete.confirmMultiple', 'Are you sure you want to delete {count} workflows?').replace('{count}', workflowCount.toString()))) {
// Start all delete operations simultaneously
const deletePromises = workflowsToDelete.map(async (workflow) => {
try {
const success = await deleteWorkflow(workflow.id);
return { workflowId: workflow.id, success };
} catch (error) {
console.error('Failed to delete workflow:', workflow.id, error);
return { workflowId: workflow.id, success: false };
}
});
// Wait for all deletions to complete
const results = await Promise.all(deletePromises);
// Check if any deletions failed
const failedDeletions = results.filter(result => !result.success);
if (failedDeletions.length > 0) {
console.error('Some workflow deletions failed:', failedDeletions);
}
// Refresh the workflow list regardless of individual failures
refetch();
}
};
// Handle edit workflow
const handleEditWorkflow = (workflow: Workflow) => {
setEditingWorkflow(workflow);
setEditModalOpen(true);
};
// Handle save workflow
const handleSaveWorkflow = async (updatedWorkflow: Workflow) => {
if (!editingWorkflow) return;
try {
// Call API to update workflow name
const result = await updateWorkflow(editingWorkflow.id, {
name: updatedWorkflow.name
});
if (result.success) {
// Close modal
setEditModalOpen(false);
setEditingWorkflow(null);
// Refresh workflow list
await refetch();
// Notify other components that workflows have been updated
window.dispatchEvent(new CustomEvent('workflowUpdated', {
detail: { workflowId: editingWorkflow.id, newName: updatedWorkflow.name }
}));
} else {
console.error('Failed to update workflow:', result.error);
// TODO: Show error message to user
}
} catch (error) {
console.error('Failed to update workflow:', error);
// TODO: Show error message to user
}
};
// Handle cancel edit
const handleCancelEdit = () => {
setEditModalOpen(false);
setEditingWorkflow(null);
};
// Handle play workflow - navigate to dashboard with workflow ID
const handlePlayWorkflow = (workflow: Workflow) => {
// Navigate to dashboard with workflow ID as URL parameter
navigate(`/dashboard?workflowId=${workflow.id}`);
};
// Configure action buttons
const actions: WorkflowActionConfig[] = useMemo(() => [
{
label: t('workflows.action.play'),
icon: (_row: Workflow) => {
return <IoIosPlay />;
},
onClick: (row: Workflow) => {
handlePlayWorkflow(row);
}
},
{
label: t('workflows.action.edit'),
icon: (_row: Workflow) => {
return <MdModeEdit />;
},
onClick: (row: Workflow) => {
handleEditWorkflow(row);
}
},
{
label: t('workflows.action.delete'),
icon: (_row: Workflow) => {
return <IoIosTrash />;
},
onClick: (row: Workflow) => {
if (!deletingWorkflows.has(row.id)) {
handleDeleteWorkflow(row);
}
}
},
], [t, deletingWorkflows, handleDeleteWorkflow, handleEditWorkflow, handlePlayWorkflow]);
return {
// Data
workflows,
loading,
error,
workflowMessageCounts,
editModalOpen,
editingWorkflow,
editWorkflowFields,
// Actions
handleDeleteSingle,
handleDeleteMultiple,
handleEditWorkflow,
handleSaveWorkflow,
handleCancelEdit,
handlePlayWorkflow,
// Refetch function
refetch,
// Additional data for rendering
columns: columns as any,
actions: actions as any
};
}

View file

@ -0,0 +1,59 @@
import React from 'react';
import { Workflow } from '../../hooks/useWorkflows';
export interface WorkflowsTableProps {
className?: string;
}
export interface WorkflowMessageCounts {
[workflowId: string]: number;
}
export interface WorkflowEditState {
editModalOpen: boolean;
editingWorkflow: Workflow | null;
}
export interface WorkflowsLogicReturn {
// Data
workflows: Workflow[];
loading: boolean;
error: string | null;
workflowMessageCounts: WorkflowMessageCounts;
editModalOpen: boolean;
editingWorkflow: Workflow | null;
editWorkflowFields: any[];
columns: any[];
actions: any[];
// Actions
handleDeleteSingle: (workflow: Workflow) => Promise<void>;
handleDeleteMultiple: (workflows: Workflow[]) => Promise<void>;
handleEditWorkflow: (workflow: Workflow) => void;
handleSaveWorkflow: (updatedWorkflow: Workflow) => Promise<void>;
handleCancelEdit: () => void;
handlePlayWorkflow: (workflow: Workflow) => void;
// Refetch function
refetch: () => Promise<void>;
}
export interface WorkflowActionConfig {
label: string;
icon: (row: Workflow) => React.ReactElement;
onClick: (row: Workflow) => void;
}
export interface WorkflowColumnConfig {
key: string;
label: string;
type: string;
width: number;
minWidth: number;
maxWidth: number;
sortable: boolean;
filterable: boolean;
searchable?: boolean;
filterOptions?: string[];
formatter: (value: any, row?: any) => React.ReactElement | string;
}

View file

@ -19,7 +19,7 @@ function Dashboard() {
// Get workflow ID from URL parameters // Get workflow ID from URL parameters
const workflowIdFromUrl = searchParams.get('workflowId'); const workflowIdFromUrl = searchParams.get('workflowId');
const [workflowState, workflowActions] = useWorkflowManager(workflowIdFromUrl); const [workflowState, workflowActions] = useWorkflowManager(workflowIdFromUrl);
const { workflows, loading: workflowsLoading, error: workflowsError } = useWorkflows(); const { workflows, loading: workflowsLoading, error: workflowsError, refetch: refetchWorkflows } = useWorkflows();
const handleWorkflowSelect = useCallback((workflowId: string) => { const handleWorkflowSelect = useCallback((workflowId: string) => {
workflowActions.loadWorkflow(workflowId); workflowActions.loadWorkflow(workflowId);
@ -44,6 +44,18 @@ function Dashboard() {
return () => document.removeEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside);
}, []); }, []);
// Listen for workflow updates from other components (like WorkflowsTable)
useEffect(() => {
const handleWorkflowUpdated = (event: CustomEvent) => {
console.log('Dashboard received workflow update event:', event.detail);
// Refetch workflows to update the dropdown
refetchWorkflows();
};
window.addEventListener('workflowUpdated', handleWorkflowUpdated as EventListener);
return () => window.removeEventListener('workflowUpdated', handleWorkflowUpdated as EventListener);
}, [refetchWorkflows]);
const getWorkflowDisplayName = (workflow: Workflow) => const getWorkflowDisplayName = (workflow: Workflow) =>
workflow.name || `${workflow.id.substring(0, 8)}...`; workflow.name || `${workflow.id.substring(0, 8)}...`;