295 lines
8.0 KiB
TypeScript
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;
|