webui/src/modules/manager/workflowTemplates/components/TasksTab.tsx

295 lines
8.0 KiB
TypeScript

import React, { useEffect, useState, useCallback } from "react";
import templateVersionsService, {
CreateWorkflowTemplateVersion,
TaskDefinition,
TaskMetadata,
} from "../services/WorkflowTemplateService";
import TaskList from "./TaskList";
import { TaskEditor } from "./TasksEditor";
interface TasksTabProps {
data: CreateWorkflowTemplateVersion;
errors: Record<string, string>;
isEditMode: boolean;
onTasksChange: (name: string, value: TaskDefinition[]) => void;
onValidate: (taskId: string, isValid: boolean) => void;
taskValidation: Record<string, boolean>;
}
const TasksTab: React.FC<TasksTabProps> = ({
data,
errors,
isEditMode,
onTasksChange,
onValidate,
taskValidation,
}) => {
const tasks = data.tasks;
const [selectedTask, setSelectedTask] = useState<TaskDefinition | null>(null);
const [pendingSelectGuid, setPendingSelectGuid] = useState<string | null>(
null,
);
useEffect(() => {
// If we have a pending task GUID to select, find it and select it
if (pendingSelectGuid) {
const { task: foundTask } = findTaskAndSiblings(
pendingSelectGuid,
tasks,
null,
);
if (foundTask) {
setSelectedTask(foundTask);
setPendingSelectGuid(null);
}
return;
}
// Don't override user selection
if (selectedTask) return;
if (tasks.length === 0) {
setSelectedTask(null);
return;
}
// Find first invalid task
const firstInvalid = tasks.find(
(t) => taskValidation[t.config.guid as string] === false,
);
if (firstInvalid) {
setSelectedTask(firstInvalid);
return;
}
// Otherwise select first task
setSelectedTask(tasks[0]);
}, [tasks, taskValidation, selectedTask, pendingSelectGuid]);
const handleTasksChange = React.useCallback(
(newTasks: TaskDefinition[]) => {
// Update the parent form state
onTasksChange("tasks", newTasks);
},
[onTasksChange],
);
const findTaskAndSiblings = (
targetGuid: string,
source: TaskDefinition[],
parent: TaskDefinition | null = null,
): {
task: TaskDefinition | null;
siblings: TaskDefinition[];
parent: TaskDefinition | null;
} => {
for (const task of source) {
if (task.config.guid === targetGuid) {
return { task, siblings: source, parent };
}
const childTasks = (task.config.tasks as TaskDefinition[]) ?? [];
if (childTasks.length === 0) continue;
const result = findTaskAndSiblings(targetGuid, childTasks, task);
if (result.task) {
return result;
}
}
return { task: null, siblings: [], parent: null };
};
const handleTaskEditorChange = React.useCallback(
(updatedTask: TaskDefinition) => {
const { siblings } = findTaskAndSiblings(
updatedTask.config.guid as string,
tasks,
null,
);
if (siblings.length === 0) {
return;
}
const updateTaskRecursive = (
source: TaskDefinition[],
): TaskDefinition[] => {
return source.map((t) => {
if (t.config.guid === updatedTask.config.guid) {
return updatedTask;
}
const childTasks = (t.config.tasks as TaskDefinition[]) ?? [];
if (childTasks.length === 0) {
return t;
}
const updatedChildren = updateTaskRecursive(childTasks);
if (updatedChildren === childTasks) {
return t;
}
return {
...t,
config: {
...t.config,
tasks: updatedChildren,
},
};
});
};
const newTasks = updateTaskRecursive(tasks);
handleTasksChange(newTasks);
// Use the updated object from the array, not the raw updatedTask
const { task: updatedFromArray } = findTaskAndSiblings(
updatedTask.config.guid as string,
newTasks,
);
if (updatedFromArray) {
setSelectedTask(updatedFromArray);
}
},
[tasks, handleTasksChange],
);
const handleTaskDelete = React.useCallback(
(taskId: string) => {
const removeTaskRecursive = (
source: TaskDefinition[],
): { tasks: TaskDefinition[]; removed: boolean } => {
let removed = false;
const updated = source.flatMap((task) => {
if (task.config.guid === taskId) {
removed = true;
return [];
}
const childTasks = (task.config.tasks as TaskDefinition[]) ?? [];
if (childTasks.length === 0) {
return [task];
}
const childResult = removeTaskRecursive(childTasks);
if (!childResult.removed) {
return [task];
}
removed = true;
return [
{
...task,
config: {
...task.config,
tasks: childResult.tasks,
},
},
];
});
return { tasks: updated, removed };
};
const removePredecessorRecursive = (
source: TaskDefinition[],
): TaskDefinition[] => {
return source.map((task) => {
const childTasks = (task.config.tasks as TaskDefinition[]) ?? [];
const updatedChildren =
childTasks.length > 0
? removePredecessorRecursive(childTasks)
: childTasks;
const predecessors = task.config.predecessors as string[] | undefined;
const updatedPredecessors = predecessors
? predecessors.filter((d) => d !== taskId)
: predecessors;
const childrenChanged = updatedChildren !== childTasks;
const predecessorsChanged = updatedPredecessors !== predecessors;
if (!childrenChanged && !predecessorsChanged) {
return task;
}
const updatedConfig = { ...task.config } as Record<string, unknown>;
if (childrenChanged) {
updatedConfig.tasks = updatedChildren;
}
if (predecessorsChanged) {
updatedConfig.predecessors = updatedPredecessors;
}
return {
...task,
config: updatedConfig,
};
});
};
const deleteResult = removeTaskRecursive(tasks);
if (!deleteResult.removed) {
return;
}
const newTasks = removePredecessorRecursive(deleteResult.tasks);
onValidate(taskId, true); // Clear validation state for deleted task
setSelectedTask(null);
handleTasksChange(newTasks);
},
[tasks, handleTasksChange],
);
return (
<div className="two-column-grid no-scroll workflow-tasks-grid">
<div className="fit-content-width workflow-tasks-list-host">
<TaskList
tasks={tasks}
validTasksList={taskValidation}
onChange={handleTasksChange}
selectedTask={selectedTask}
onSelectTask={setSelectedTask}
onValidate={onValidate}
/>
</div>
{selectedTask && (
<div>
<TaskEditor
task={selectedTask}
tasks={tasks}
siblingTasks={
findTaskAndSiblings(
selectedTask.config.guid as string,
tasks,
null,
).siblings
}
parentTask={
findTaskAndSiblings(
selectedTask.config.guid as string,
tasks,
null,
).parent
}
onChange={handleTaskEditorChange}
onValidate={onValidate}
onDelete={handleTaskDelete}
onTaskAdded={(taskGuid: string) => {
setPendingSelectGuid(taskGuid);
}}
/>
</div>
)}
</div>
);
};
export default TasksTab;