Improvements to the validation engine

This commit is contained in:
Colin Dawson 2026-02-16 20:35:56 +00:00
parent 4b3f65ec20
commit 226d402578
5 changed files with 54 additions and 30 deletions

View File

@ -89,6 +89,8 @@ export const AssigneesOfITaskAssigneeEditor: React.FC<CapabilityEditorProps> = (
onChange(clone);
}
const guid = task.config.guid as string;
return (
<div>
Select assigness (you can select either a role or a contact)
@ -113,7 +115,7 @@ export const AssigneesOfITaskAssigneeEditor: React.FC<CapabilityEditorProps> = (
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<CapabilityEditorProps> = (
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<CapabilityEditorProps> = (
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<string, string> => {
const errors: Record<string, string> = {};
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.";
}
});

View File

@ -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<TaskListProps> = ({
@ -26,6 +27,7 @@ const TaskList: React.FC<TaskListProps> = ({
onChange,
selectedTask,
onSelectTask,
onValidate,
}) => {
const runDefaults = useCapabilityDefaults(tasksMetadata);
@ -43,7 +45,12 @@ const TaskList: React.FC<TaskListProps> = ({
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);
};

View File

@ -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<TaskEditorProps> = ({
{},
);
const validateTask = (task: TaskDefinition, tasks: TaskDefinition[]) => {
const taskMeta = tasksMetadata.find((t) => t.taskType === task.type);
const errors: Record<string, string> = {};
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<TaskEditorProps> = ({
);
// Run validation
const errors = validateTask(updatedTask, updatedTasks);
const errors = validateTask(updatedTask, updatedTasks, tasksMetadata);
setFieldErrors(errors);
// Bubble validity up

View File

@ -75,6 +75,7 @@ const TasksTab: React.FC<TasksTabProps> = ({
onChange={handleTasksChange}
selectedTask={selectedTask}
onSelectTask={setSelectedTask}
onValidate={onValidate}
/>
</div>
{selectedTask && (

View File

@ -35,9 +35,32 @@ export interface capabilityEditorRegistryEntry {
ValidationRunner?: (
task: TaskDefinition,
tasks: TaskDefinition[],
tasksMetadata: TaskMetadata[],
) => Record<string, string>;
}
export function validateTask(
task: TaskDefinition,
tasks: TaskDefinition[],
tasksMetadata: TaskMetadata[],
): Record<string, string> {
const taskMeta = tasksMetadata.find((t) => t.taskType === task.type);
const errors: Record<string, string> = {};
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);