From 226d40257806d686b03b12f189c33a5126e10daf Mon Sep 17 00:00:00 2001 From: Colin Dawson Date: Mon, 16 Feb 2026 20:35:56 +0000 Subject: [PATCH] Improvements to the validation engine --- .../AssigneesOfITaskAssigneeEditor.tsx | 28 +++++++++++++------ .../workflowTemplates/components/TaskList.tsx | 11 ++++++-- .../components/TasksEditor.tsx | 21 ++------------ .../workflowTemplates/components/TasksTab.tsx | 1 + .../components/useCapabilityDefaults.tsx | 23 +++++++++++++++ 5 files changed, 54 insertions(+), 30 deletions(-) diff --git a/src/modules/manager/workflowTemplates/components/CapabilityEditors/AssigneesOfITaskAssigneeEditor.tsx b/src/modules/manager/workflowTemplates/components/CapabilityEditors/AssigneesOfITaskAssigneeEditor.tsx index b0aabc9..b2ad3f1 100644 --- a/src/modules/manager/workflowTemplates/components/CapabilityEditors/AssigneesOfITaskAssigneeEditor.tsx +++ b/src/modules/manager/workflowTemplates/components/CapabilityEditors/AssigneesOfITaskAssigneeEditor.tsx @@ -89,6 +89,8 @@ export const AssigneesOfITaskAssigneeEditor: React.FC = ( onChange(clone); } + const guid = task.config.guid as string; + return (
Select assigness (you can select either a role or a contact) @@ -113,7 +115,7 @@ export const AssigneesOfITaskAssigneeEditor: React.FC = ( name="role" label="Role" value={assignee.role} - error={fieldErrors?.[`assignees[${index}].role`]} + error={fieldErrors?.[`${guid}.assignees[${index}].role`]} onChange={(name: string, val: GeneralIdRef | null) => updateAssignee(index, { ...assignee, role: val }) } @@ -125,7 +127,7 @@ export const AssigneesOfITaskAssigneeEditor: React.FC = ( name="contact" label="Contact" value={assignee.contact} - error={fieldErrors?.[`assignees[${index}].contact`]} + error={fieldErrors?.[`${guid}.assignees[${index}].contact`]} onChange={(name: string, val: GeneralIdRef | null) => updateAssignee(index, { ...assignee, contact: val }) } @@ -136,7 +138,7 @@ export const AssigneesOfITaskAssigneeEditor: React.FC = ( name="raci" label="RACI" value={assignee.raci} - error={fieldErrors?.[`assignees[${index}].raci`]} + error={fieldErrors?.[`${guid}.assignees[${index}].raci`]} onChange={(name: string, val: string) => updateAssignee(index, { ...assignee, raci: val }) } @@ -167,22 +169,30 @@ const runValidation = ( tasks: TaskDefinition[], ): Record => { const errors: Record = {}; + const guid = task.config.guid as string; const assignees = task.config.assignees as ITaskAssignee[] | undefined; if (!assignees || assignees.length === 0) { - errors["assignees"] = "At least one assignee is required."; + errors[`${guid}.assignees`] = "At least one assignee is required."; return errors; } assignees.forEach((a, i) => { - if (!a.role && !a.contact) { - //errors[`assignees[${i}]`] = "Either role or contact must be selected."; - errors[`assignees`] = "Either role or contact must be selected."; + const noContactSelected = !a.contact || a.contact?.id === BigInt(0); + + if (noContactSelected) { + errors[`${guid}.assignees[${i}].contact`] = "A contact must be selected."; + } + + if (!a.role && noContactSelected) { + errors[`${guid}.assignees[${i}].role`] = + "Either role or contact must be selected."; + errors[`${guid}.assignees[${i}].contact`] = + "Either role or contact must be selected."; } if (!a.raci) { - //errors[`assignees[${i}].raci`] = "RACI is required."; - errors[`assignees`] = "RACI is required."; + errors[`${guid}.assignees[${i}].raci`] = "RACI is required."; } }); diff --git a/src/modules/manager/workflowTemplates/components/TaskList.tsx b/src/modules/manager/workflowTemplates/components/TaskList.tsx index 15985b3..7acdae5 100644 --- a/src/modules/manager/workflowTemplates/components/TaskList.tsx +++ b/src/modules/manager/workflowTemplates/components/TaskList.tsx @@ -8,7 +8,7 @@ import { SelectableList } from "../../../../components/common/SelectableList"; import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { sortTasksTopologically } from "./workflowGraphUtils"; -import { useCapabilityDefaults } from "./useCapabilityDefaults"; +import { useCapabilityDefaults, validateTask } from "./useCapabilityDefaults"; interface TaskListProps { tasks: TaskDefinition[]; @@ -17,6 +17,7 @@ interface TaskListProps { onChange: (tasks: TaskDefinition[]) => void; selectedTask: TaskDefinition | null; onSelectTask: (task: TaskDefinition | null) => void; + onValidate: (taskId: string, isValid: boolean) => void; } const TaskList: React.FC = ({ @@ -26,6 +27,7 @@ const TaskList: React.FC = ({ onChange, selectedTask, onSelectTask, + onValidate, }) => { const runDefaults = useCapabilityDefaults(tasksMetadata); @@ -43,7 +45,12 @@ const TaskList: React.FC = ({ runDefaults(capability, newTask, tasks); }); - onChange([...tasks, newTask]); + const updatedTasks = [...tasks, newTask]; + const errors = validateTask(newTask, updatedTasks, tasksMetadata); + + onValidate(newTask.config.guid as string, Object.keys(errors).length === 0); + + onChange(updatedTasks); onSelectTask(newTask); }; diff --git a/src/modules/manager/workflowTemplates/components/TasksEditor.tsx b/src/modules/manager/workflowTemplates/components/TasksEditor.tsx index 70efad6..9021a65 100644 --- a/src/modules/manager/workflowTemplates/components/TasksEditor.tsx +++ b/src/modules/manager/workflowTemplates/components/TasksEditor.tsx @@ -4,6 +4,7 @@ import { TaskMetadata, } from "../services/WorkflowTemplateService"; import { capabilityEditorRegistry } from "./capabilityEditorRegistry"; +import { validateTask } from "./useCapabilityDefaults"; interface TaskEditorProps { task: TaskDefinition; @@ -24,24 +25,6 @@ export const TaskEditor: React.FC = ({ {}, ); - const validateTask = (task: TaskDefinition, tasks: TaskDefinition[]) => { - const taskMeta = tasksMetadata.find((t) => t.taskType === task.type); - - const errors: Record = {}; - - for (const capability of taskMeta?.capabilities ?? []) { - const entry = capabilityEditorRegistry[capability]; - - if (!entry?.ValidationRunner) { - continue; - } - - const validationErrors = entry.ValidationRunner(task, tasks); - Object.assign(errors, validationErrors); - } - return errors; - }; - const handleTaskChange = (updatedTask: TaskDefinition) => { // Update the task list const updatedTasks = tasks.map((t) => @@ -49,7 +32,7 @@ export const TaskEditor: React.FC = ({ ); // Run validation - const errors = validateTask(updatedTask, updatedTasks); + const errors = validateTask(updatedTask, updatedTasks, tasksMetadata); setFieldErrors(errors); // Bubble validity up diff --git a/src/modules/manager/workflowTemplates/components/TasksTab.tsx b/src/modules/manager/workflowTemplates/components/TasksTab.tsx index a5467d1..9f4c6d8 100644 --- a/src/modules/manager/workflowTemplates/components/TasksTab.tsx +++ b/src/modules/manager/workflowTemplates/components/TasksTab.tsx @@ -75,6 +75,7 @@ const TasksTab: React.FC = ({ onChange={handleTasksChange} selectedTask={selectedTask} onSelectTask={setSelectedTask} + onValidate={onValidate} />
{selectedTask && ( diff --git a/src/modules/manager/workflowTemplates/components/useCapabilityDefaults.tsx b/src/modules/manager/workflowTemplates/components/useCapabilityDefaults.tsx index d3e2ae6..2eeb6eb 100644 --- a/src/modules/manager/workflowTemplates/components/useCapabilityDefaults.tsx +++ b/src/modules/manager/workflowTemplates/components/useCapabilityDefaults.tsx @@ -35,9 +35,32 @@ export interface capabilityEditorRegistryEntry { ValidationRunner?: ( task: TaskDefinition, tasks: TaskDefinition[], + tasksMetadata: TaskMetadata[], ) => Record; } +export function validateTask( + task: TaskDefinition, + tasks: TaskDefinition[], + tasksMetadata: TaskMetadata[], +): Record { + const taskMeta = tasksMetadata.find((t) => t.taskType === task.type); + + const errors: Record = {}; + + for (const capability of taskMeta?.capabilities ?? []) { + const entry = capabilityEditorRegistry[capability]; + + if (!entry?.ValidationRunner) { + continue; + } + + const validationErrors = entry.ValidationRunner(task, tasks); + Object.assign(errors, validationErrors); + } + return errors; +} + export function useCapabilityDefaults(taskMetadata: TaskMetadata[]) { const { t: tTaskType } = useTranslation(Namespaces.TaskTypes);