Corrected the place where the task name is generated, it's now part of the capability editor rather than bespoke code.

This commit is contained in:
Colin Dawson 2026-02-14 14:31:22 +00:00
parent f30120d448
commit 6f49add5f7
5 changed files with 88 additions and 54 deletions

View File

@ -1,19 +1,18 @@
import React from "react"; import React from "react";
import templateVersionsService, { import { TaskMetadata } from "../services/WorkflowTemplateService";
TaskMetadata,
} from "../services/WorkflowTemplateService";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Namespaces } from "../../../../i18n/i18n"; import { Namespaces } from "../../../../i18n/i18n";
interface AddTaskButtonProps { interface AddTaskButtonProps {
taskType: string; allowedTasks: TaskMetadata[];
onAdd: (selectedType: TaskMetadata) => void; onAdd: (selectedType: TaskMetadata) => void;
} }
const AddTaskButton: React.FC<AddTaskButtonProps> = ({ taskType, onAdd }) => { const AddTaskButton: React.FC<AddTaskButtonProps> = ({
allowedTasks,
onAdd,
}) => {
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [items, setItems] = React.useState<TaskMetadata[]>([]);
const [loading, setLoading] = React.useState(false);
const { t: tTaskType } = useTranslation(Namespaces.TaskTypes); const { t: tTaskType } = useTranslation(Namespaces.TaskTypes);
const { t } = useTranslation(Namespaces.Common); const { t } = useTranslation(Namespaces.Common);
@ -21,17 +20,6 @@ const AddTaskButton: React.FC<AddTaskButtonProps> = ({ taskType, onAdd }) => {
const toggle = async () => { const toggle = async () => {
const next = !open; const next = !open;
setOpen(next); 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 ( return (
@ -42,21 +30,18 @@ const AddTaskButton: React.FC<AddTaskButtonProps> = ({ taskType, onAdd }) => {
{open && ( {open && (
<div className="dropdown-menu show"> <div className="dropdown-menu show">
{loading && <div className="dropdown-item">{t("Loading")}</div>} {allowedTasks.map((item) => (
<button
{!loading && key={item.taskType}
items.map((item) => ( className="dropdown-item"
<button onClick={() => {
key={item.taskType} onAdd(item);
className="dropdown-item" setOpen(false);
onClick={() => { }}
onAdd(item); >
setOpen(false); {tTaskType(item.displayName)}
}} </button>
> ))}
{tTaskType(item.displayName)}
</button>
))}
</div> </div>
)} )}
</div> </div>

View File

@ -1,12 +1,18 @@
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { InputType } from "../../../../../components/common/Input"; import { InputType } from "../../../../../components/common/Input";
import { TaskDefinition } from "../../services/WorkflowTemplateService"; import {
TaskDefinition,
TaskMetadata,
} from "../../services/WorkflowTemplateService";
import { renderTaskField } from "../taskEditorHelpers"; import { renderTaskField } from "../taskEditorHelpers";
import { TaskValidationResult } from "../TasksEditor"; import { TaskValidationResult } from "../TasksEditor";
import { Namespaces } from "../../../../../i18n/i18n";
import { useTranslation } from "react-i18next";
interface TaskCoreEditorProps { interface TaskCoreEditorProps {
task: TaskDefinition; task: TaskDefinition;
allTasks: TaskDefinition[]; allTasks: TaskDefinition[];
allowedTasks: TaskMetadata[];
onChange: (updated: TaskDefinition) => void; onChange: (updated: TaskDefinition) => void;
onValidate: (result: TaskValidationResult) => void; onValidate: (result: TaskValidationResult) => void;
} }
@ -14,22 +20,62 @@ interface TaskCoreEditorProps {
export const TaskCoreEditor: React.FC<TaskCoreEditorProps> = ({ export const TaskCoreEditor: React.FC<TaskCoreEditorProps> = ({
task, task,
allTasks, allTasks,
allowedTasks,
onChange, onChange,
onValidate, onValidate,
}) => { }) => {
const { t: tTaskType } = useTranslation(Namespaces.TaskTypes);
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({}); const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
const prevErrorsRef = useRef<Record<string, string>>({}); const prevErrorsRef = useRef<Record<string, string>>({});
const formatNewTaskName = (
tasks: TaskDefinition<Record<string, unknown>>[],
) => {
const displayName = allowedTasks.find(
(t) => t.taskType === task.type,
)?.displayName;
return `${tTaskType(displayName!)} ${tasks.length + 1}`;
};
const runValidation = useCallback(() => { const runValidation = useCallback(() => {
const errors: Record<string, string> = {}; const errors: Record<string, string> = {};
if (!task.config.description) { //If the task doesn't have a name (can happen when adding a new task), generate a default one.
errors["description"] = "Description cannot be empty"; 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; const isValid = Object.keys(errors).length === 0;
return { errors, isValid }; return { errors, isValid };
}, [task.config.description]); }, [allTasks, task.config.description, task.config.guid, task.config.name]);
//Validate when task changes. //Validate when task changes.
useEffect(() => { useEffect(() => {

View File

@ -13,7 +13,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
interface TaskListProps { interface TaskListProps {
tasks: TaskDefinition[]; tasks: TaskDefinition[];
validTasksList: Record<string, boolean>; validTasksList: Record<string, boolean>;
taskType: string; allowedTasks: TaskMetadata[];
onChange: (tasks: TaskDefinition[]) => void; onChange: (tasks: TaskDefinition[]) => void;
selectedTask: TaskDefinition | null; selectedTask: TaskDefinition | null;
onSelectTask: (task: TaskDefinition | null) => void; onSelectTask: (task: TaskDefinition | null) => void;
@ -22,28 +22,16 @@ interface TaskListProps {
const TaskList: React.FC<TaskListProps> = ({ const TaskList: React.FC<TaskListProps> = ({
tasks, tasks,
validTasksList, validTasksList,
taskType, allowedTasks,
onChange, onChange,
selectedTask, selectedTask,
onSelectTask, onSelectTask,
}) => { }) => {
const { t: tTaskType } = useTranslation(Namespaces.TaskTypes);
const formatNewTaskName = (
displayName: string,
tasks: TaskDefinition<Record<string, unknown>>[],
) => {
return `${tTaskType(displayName)} ${tasks.length + 1}`;
};
const handleAddTask = (selectedType: TaskMetadata) => { const handleAddTask = (selectedType: TaskMetadata) => {
const formattedName = formatNewTaskName(selectedType.displayName, tasks);
const newTask: TaskDefinition = { const newTask: TaskDefinition = {
type: selectedType.taskType, type: selectedType.taskType,
config: { config: {
name: formattedName,
guid: crypto.randomUUID(), guid: crypto.randomUUID(),
}, },
}; };
@ -56,7 +44,7 @@ const TaskList: React.FC<TaskListProps> = ({
return ( return (
<div> <div>
<AddTaskButton taskType={taskType} onAdd={handleAddTask} /> <AddTaskButton allowedTasks={allowedTasks} onAdd={handleAddTask} />
<SelectableList <SelectableList
items={tasks} items={tasks}

View File

@ -10,6 +10,7 @@ export interface TaskValidationResult {
interface TaskEditorProps { interface TaskEditorProps {
task: TaskDefinition; task: TaskDefinition;
allTasks: TaskDefinition[]; allTasks: TaskDefinition[];
allowedTasks: TaskMetadata[];
onChange: (updatedTask: TaskDefinition) => void; onChange: (updatedTask: TaskDefinition) => void;
onValidate: (taskId: string, isValid: boolean) => void; onValidate: (taskId: string, isValid: boolean) => void;
} }
@ -17,6 +18,7 @@ interface TaskEditorProps {
export const TaskEditor: React.FC<TaskEditorProps> = ({ export const TaskEditor: React.FC<TaskEditorProps> = ({
task, task,
allTasks, allTasks,
allowedTasks,
onChange, onChange,
onValidate, onValidate,
}) => { }) => {
@ -43,6 +45,7 @@ export const TaskEditor: React.FC<TaskEditorProps> = ({
<> <>
<TaskCoreEditor <TaskCoreEditor
task={task} task={task}
allowedTasks={allowedTasks}
allTasks={allTasks} allTasks={allTasks}
onChange={onChange} onChange={onChange}
onValidate={(result) => onCapabilityValidate("core", result)} onValidate={(result) => onCapabilityValidate("core", result)}

View File

@ -1,7 +1,8 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { import templateVersionsService, {
CreateWorkflowTemplateVersion, CreateWorkflowTemplateVersion,
TaskDefinition, TaskDefinition,
TaskMetadata,
} from "../services/WorkflowTemplateService"; } from "../services/WorkflowTemplateService";
import TaskList from "./TaskList"; import TaskList from "./TaskList";
import { TaskEditor } from "./TasksEditor"; import { TaskEditor } from "./TasksEditor";
@ -25,6 +26,16 @@ const TasksTab: React.FC<TasksTabProps> = ({
}) => { }) => {
const tasks = data.tasks; const tasks = data.tasks;
const [selectedTask, setSelectedTask] = useState<TaskDefinition | null>(null); const [selectedTask, setSelectedTask] = useState<TaskDefinition | null>(null);
const [allowedTasks, setAllowedTasks] = React.useState<TaskMetadata[]>([]);
useEffect(() => {
const fetchTaskMetadata = async () => {
const meta = await templateVersionsService.getTaskMetadata("GeneralTask");
setAllowedTasks(meta);
};
fetchTaskMetadata();
}, []);
useEffect(() => { useEffect(() => {
if (tasks.length === 0) { if (tasks.length === 0) {
@ -91,7 +102,7 @@ const TasksTab: React.FC<TasksTabProps> = ({
<TaskList <TaskList
tasks={tasks} tasks={tasks}
validTasksList={taskValidation} validTasksList={taskValidation}
taskType="GeneralTask" allowedTasks={allowedTasks}
onChange={handleTasksChange} onChange={handleTasksChange}
selectedTask={selectedTask} selectedTask={selectedTask}
onSelectTask={setSelectedTask} onSelectTask={setSelectedTask}
@ -99,6 +110,7 @@ const TasksTab: React.FC<TasksTabProps> = ({
</div> </div>
{selectedTask && ( {selectedTask && (
<TaskEditor <TaskEditor
allowedTasks={allowedTasks}
task={selectedTask} task={selectedTask}
allTasks={tasks} allTasks={tasks}
onChange={(updatedTask) => { onChange={(updatedTask) => {