diff --git a/src/modules/manager/workflowTemplates/components/AddTaskButton.tsx b/src/modules/manager/workflowTemplates/components/AddTaskButton.tsx index 80cbbb8..0122ebd 100644 --- a/src/modules/manager/workflowTemplates/components/AddTaskButton.tsx +++ b/src/modules/manager/workflowTemplates/components/AddTaskButton.tsx @@ -1,19 +1,18 @@ import React from "react"; -import templateVersionsService, { - TaskMetadata, -} from "../services/WorkflowTemplateService"; +import { TaskMetadata } from "../services/WorkflowTemplateService"; import { useTranslation } from "react-i18next"; import { Namespaces } from "../../../../i18n/i18n"; interface AddTaskButtonProps { - taskType: string; + allowedTasks: TaskMetadata[]; onAdd: (selectedType: TaskMetadata) => void; } -const AddTaskButton: React.FC = ({ taskType, onAdd }) => { +const AddTaskButton: React.FC = ({ + allowedTasks, + onAdd, +}) => { const [open, setOpen] = React.useState(false); - const [items, setItems] = React.useState([]); - const [loading, setLoading] = React.useState(false); const { t: tTaskType } = useTranslation(Namespaces.TaskTypes); const { t } = useTranslation(Namespaces.Common); @@ -21,17 +20,6 @@ const AddTaskButton: React.FC = ({ taskType, onAdd }) => { const toggle = async () => { const next = !open; setOpen(next); - - // Fetch only when opening AND only once - if (next && items.length === 0) { - setLoading(true); - try { - const meta = await templateVersionsService.getTaskMetadata(taskType); - setItems(meta); - } finally { - setLoading(false); - } - } }; return ( @@ -42,21 +30,18 @@ const AddTaskButton: React.FC = ({ taskType, onAdd }) => { {open && (
- {loading &&
{t("Loading")}
} - - {!loading && - items.map((item) => ( - - ))} + {allowedTasks.map((item) => ( + + ))}
)} diff --git a/src/modules/manager/workflowTemplates/components/CapabilityEditors/TaskCoreEditor.tsx b/src/modules/manager/workflowTemplates/components/CapabilityEditors/TaskCoreEditor.tsx index 632f611..dc0f8c6 100644 --- a/src/modules/manager/workflowTemplates/components/CapabilityEditors/TaskCoreEditor.tsx +++ b/src/modules/manager/workflowTemplates/components/CapabilityEditors/TaskCoreEditor.tsx @@ -1,12 +1,18 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { InputType } from "../../../../../components/common/Input"; -import { TaskDefinition } from "../../services/WorkflowTemplateService"; +import { + TaskDefinition, + TaskMetadata, +} from "../../services/WorkflowTemplateService"; import { renderTaskField } from "../taskEditorHelpers"; import { TaskValidationResult } from "../TasksEditor"; +import { Namespaces } from "../../../../../i18n/i18n"; +import { useTranslation } from "react-i18next"; interface TaskCoreEditorProps { task: TaskDefinition; allTasks: TaskDefinition[]; + allowedTasks: TaskMetadata[]; onChange: (updated: TaskDefinition) => void; onValidate: (result: TaskValidationResult) => void; } @@ -14,22 +20,62 @@ interface TaskCoreEditorProps { export const TaskCoreEditor: React.FC = ({ task, allTasks, + allowedTasks, onChange, onValidate, }) => { + const { t: tTaskType } = useTranslation(Namespaces.TaskTypes); const [fieldErrors, setFieldErrors] = useState>({}); const prevErrorsRef = useRef>({}); + const formatNewTaskName = ( + tasks: TaskDefinition>[], + ) => { + const displayName = allowedTasks.find( + (t) => t.taskType === task.type, + )?.displayName; + + return `${tTaskType(displayName!)} ${tasks.length + 1}`; + }; + const runValidation = useCallback(() => { const errors: Record = {}; - if (!task.config.description) { - errors["description"] = "Description cannot be empty"; + //If the task doesn't have a name (can happen when adding a new task), generate a default one. + if (task.config.name === undefined) { + task.config.name = formatNewTaskName(allTasks); + } + + if (!task.config.name || (task.config.name as string).trim() === "") { + errors["name"] = "Name cannot be empty"; + } + + if (task.config.name) { + // Name must be unique across all tasks + const duplicate = allTasks.find( + (t) => + (t.config.guid as string) !== (task.config.guid as string) && // exclude self + (t.config.name as string).trim().toLowerCase() === + (task.config.name as string).trim().toLowerCase(), + ); + + if (duplicate) { + errors["name"] = "Name must be unique."; + } + } + + const descriptionMaxLength = 5000; + if ( + task.config.description && + (task.config.description as string).length >= descriptionMaxLength + ) { + errors["description"] = + `Description can be up to ${descriptionMaxLength} characters long.`; } const isValid = Object.keys(errors).length === 0; return { errors, isValid }; - }, [task.config.description]); + }, [allTasks, task.config.description, task.config.guid, task.config.name]); //Validate when task changes. useEffect(() => { diff --git a/src/modules/manager/workflowTemplates/components/TaskList.tsx b/src/modules/manager/workflowTemplates/components/TaskList.tsx index 44ead31..f789c88 100644 --- a/src/modules/manager/workflowTemplates/components/TaskList.tsx +++ b/src/modules/manager/workflowTemplates/components/TaskList.tsx @@ -13,7 +13,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; interface TaskListProps { tasks: TaskDefinition[]; validTasksList: Record; - taskType: string; + allowedTasks: TaskMetadata[]; onChange: (tasks: TaskDefinition[]) => void; selectedTask: TaskDefinition | null; onSelectTask: (task: TaskDefinition | null) => void; @@ -22,28 +22,16 @@ interface TaskListProps { const TaskList: React.FC = ({ tasks, validTasksList, - taskType, + allowedTasks, onChange, selectedTask, onSelectTask, }) => { - const { t: tTaskType } = useTranslation(Namespaces.TaskTypes); - - const formatNewTaskName = ( - displayName: string, - tasks: TaskDefinition>[], - ) => { - return `${tTaskType(displayName)} ${tasks.length + 1}`; - }; - const handleAddTask = (selectedType: TaskMetadata) => { - const formattedName = formatNewTaskName(selectedType.displayName, tasks); - const newTask: TaskDefinition = { type: selectedType.taskType, config: { - name: formattedName, guid: crypto.randomUUID(), }, }; @@ -56,7 +44,7 @@ const TaskList: React.FC = ({ return (
- + void; onValidate: (taskId: string, isValid: boolean) => void; } @@ -17,6 +18,7 @@ interface TaskEditorProps { export const TaskEditor: React.FC = ({ task, allTasks, + allowedTasks, onChange, onValidate, }) => { @@ -43,6 +45,7 @@ export const TaskEditor: React.FC = ({ <> onCapabilityValidate("core", result)} diff --git a/src/modules/manager/workflowTemplates/components/TasksTab.tsx b/src/modules/manager/workflowTemplates/components/TasksTab.tsx index 0da3ce7..69763c5 100644 --- a/src/modules/manager/workflowTemplates/components/TasksTab.tsx +++ b/src/modules/manager/workflowTemplates/components/TasksTab.tsx @@ -1,7 +1,8 @@ import React, { useEffect, useState } from "react"; -import { +import templateVersionsService, { CreateWorkflowTemplateVersion, TaskDefinition, + TaskMetadata, } from "../services/WorkflowTemplateService"; import TaskList from "./TaskList"; import { TaskEditor } from "./TasksEditor"; @@ -25,6 +26,16 @@ const TasksTab: React.FC = ({ }) => { const tasks = data.tasks; const [selectedTask, setSelectedTask] = useState(null); + const [allowedTasks, setAllowedTasks] = React.useState([]); + + useEffect(() => { + const fetchTaskMetadata = async () => { + const meta = await templateVersionsService.getTaskMetadata("GeneralTask"); + setAllowedTasks(meta); + }; + + fetchTaskMetadata(); + }, []); useEffect(() => { if (tasks.length === 0) { @@ -91,7 +102,7 @@ const TasksTab: React.FC = ({ = ({
{selectedTask && ( {