diff --git a/src/modules/manager/workflowTemplates/components/CapabilityEditors/TaskCoreEditor.tsx b/src/modules/manager/workflowTemplates/components/CapabilityEditors/TaskCoreEditor.tsx index dc0f8c6..8c284da 100644 --- a/src/modules/manager/workflowTemplates/components/CapabilityEditors/TaskCoreEditor.tsx +++ b/src/modules/manager/workflowTemplates/components/CapabilityEditors/TaskCoreEditor.tsx @@ -28,46 +28,76 @@ export const TaskCoreEditor: React.FC = ({ const [fieldErrors, setFieldErrors] = useState>({}); const prevErrorsRef = useRef>({}); - const formatNewTaskName = ( - tasks: TaskDefinition>[], - ) => { - const displayName = allowedTasks.find( - (t) => t.taskType === task.type, - )?.displayName; + // Generate a unique default name + const nameExists = (tasks: TaskDefinition[], candidate: string): boolean => { + const target = candidate.trim().toLowerCase(); - return `${tTaskType(displayName!)} ${tasks.length + 1}`; + return tasks.some( + (t) => (t.config.name as string)?.trim().toLowerCase() === target, + ); }; + const formatNewTaskName = useCallback( + (tasks: TaskDefinition[]) => { + const displayName = allowedTasks.find( + (t) => t.taskType === task.type, + )?.displayName; + + if (!displayName) return "New Task"; + + const base = `${tTaskType(displayName)} `; + let index = 1; + + while (nameExists(tasks, `${base}${index}`)) { + index++; + } + + return `${base}${index}`; + }, + [allowedTasks, task.type, tTaskType], + ); + const runValidation = useCallback(() => { const errors: Record = {}; - //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 the task doesn't have a name, generate a default one via onChange + if (!task.config.name) { + const newName = formatNewTaskName(allTasks); + + onChange({ + ...task, + config: { + ...task.config, + name: newName, + }, + }); + + // Stop here — next render will validate again + return { errors: {}, isValid: true }; } - if (!task.config.name || (task.config.name as string).trim() === "") { + // Name required + if (!(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(), - ); + // Name must be unique + const duplicate = allTasks.find( + (t) => + t.config.guid !== task.config.guid && + (t.config.name as string).trim().toLowerCase() === + (task.config.name as string).trim().toLowerCase(), + ); - if (duplicate) { - errors["name"] = "Name must be unique."; - } + if (duplicate) { + errors["name"] = "Name must be unique."; } + // Description max length const descriptionMaxLength = 5000; if ( - task.config.description && - (task.config.description as string).length >= descriptionMaxLength + (task.config.description && (task.config.description as string).length) ?? + 0 > descriptionMaxLength ) { errors["description"] = `Description can be up to ${descriptionMaxLength} characters long.`; @@ -75,9 +105,9 @@ export const TaskCoreEditor: React.FC = ({ const isValid = Object.keys(errors).length === 0; return { errors, isValid }; - }, [allTasks, task.config.description, task.config.guid, task.config.name]); + }, [task, allTasks, formatNewTaskName, onChange]); - //Validate when task changes. + // Validate when task changes (new task selected / created) useEffect(() => { const { errors, isValid } = runValidation(); @@ -85,9 +115,9 @@ export const TaskCoreEditor: React.FC = ({ prevErrorsRef.current = errors; onValidate({ isValid, errors }); - }, [onValidate, runValidation, task.config.guid]); + }, [task.config.guid, runValidation, onValidate]); - //Validate when fields change + // Validate when fields change useEffect(() => { const { errors, isValid } = runValidation(); @@ -101,7 +131,7 @@ export const TaskCoreEditor: React.FC = ({ onValidate({ isValid, errors }); prevErrorsRef.current = errors; } - }, [task.config.description, task.config.name, onValidate, runValidation]); + }, [task.config.name, task.config.description, runValidation, onValidate]); return (
diff --git a/src/modules/manager/workflowTemplates/components/TaskList.tsx b/src/modules/manager/workflowTemplates/components/TaskList.tsx index f789c88..ef97b87 100644 --- a/src/modules/manager/workflowTemplates/components/TaskList.tsx +++ b/src/modules/manager/workflowTemplates/components/TaskList.tsx @@ -4,8 +4,6 @@ import { TaskMetadata, } from "../services/WorkflowTemplateService"; import AddTaskButton from "./AddTaskButton"; -import { Namespaces } from "../../../../i18n/i18n"; -import { useTranslation } from "react-i18next"; import { SelectableList } from "../../../../components/common/SelectableList"; import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; diff --git a/src/modules/manager/workflowTemplates/components/TasksEditor.tsx b/src/modules/manager/workflowTemplates/components/TasksEditor.tsx index 3dce665..0b89fb6 100644 --- a/src/modules/manager/workflowTemplates/components/TasksEditor.tsx +++ b/src/modules/manager/workflowTemplates/components/TasksEditor.tsx @@ -22,6 +22,7 @@ export const TaskEditor: React.FC = ({ onChange, onValidate, }) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [validationMap, setValidationMap] = useState< Record >({});