validation now runs async, to allow for potential api calls whilst validating (the results of these calls should be cached to help with performance)

This commit is contained in:
Colin Dawson 2026-02-25 18:39:24 +00:00
parent 4706b78d88
commit 99dfd14ec9
9 changed files with 43 additions and 25 deletions

View File

@ -171,14 +171,14 @@ export const AssigneesOfITaskAssigneeEditor: React.FC<CapabilityEditorProps> = (
const runValidation = (
task: TaskDefinition,
tasks: TaskDefinition[],
): Record<string, string> => {
): Promise<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[`${guid}.assignees`] = "At least one assignee is required.";
return errors;
return Promise.resolve(errors);
}
assignees.forEach((a, i) => {

View File

@ -69,10 +69,10 @@ export const BudgetEditor: React.FC<CapabilityEditorProps> = (props) => {
const runValidation = (
task: TaskDefinition,
tasks: TaskDefinition[],
): Record<string, string> => {
): Promise<Record<string, string>> => {
const errors: Record<string, string> = {};
return errors;
return Promise.resolve(errors);
};
export function defaultsAssignment(

View File

@ -27,10 +27,10 @@ export const BypassableEditor: React.FC<CapabilityEditorProps> = (props) => {
const runValidation = (
task: TaskDefinition,
tasks: TaskDefinition[],
): Record<string, string> => {
): Promise<Record<string, string>> => {
const errors: Record<string, string> = {};
return errors;
return Promise.resolve(errors);
};
export function defaultsAssignment(

View File

@ -152,7 +152,7 @@ export const outcomeEditor: React.FC<CapabilityEditorProps> = (props) => {
const runValidation = (
task: TaskDefinition,
tasks: TaskDefinition[],
): Record<string, string> => {
): Promise<Record<string, string>> => {
const errors: Record<string, string> = {};
const guid = task.config.guid as string;
const outcomeActions =
@ -160,7 +160,7 @@ const runValidation = (
if (!outcomeActions) {
// No outcome actions is valid, it just means there are no outcomes configured
return errors;
return Promise.resolve(errors);
}
// --- Rule 1: Task must be selected ---
@ -190,7 +190,7 @@ const runValidation = (
}
});
return errors;
return Promise.resolve(errors);
};
export function defaultsAssignment(

View File

@ -31,7 +31,7 @@ export const StageOfGeneralTaskEditor: React.FC<CapabilityEditorProps> = (
const runDefaults = useCapabilityDefaults(tasksMetadata);
const handleAddTask = (selectedType: TaskMetadata) => {
const handleAddTask = async (selectedType: TaskMetadata) => {
const newTask: TaskDefinition = {
type: selectedType.taskType,
@ -48,14 +48,12 @@ export const StageOfGeneralTaskEditor: React.FC<CapabilityEditorProps> = (
});
const updatedTasks = [...childTasks, newTask];
const errors = validateTask(newTask, updatedTasks, tasksMetadata);
const errors = await validateTask(newTask, updatedTasks, tasksMetadata);
const isValid = Object.keys(errors).length === 0;
task.config.tasks = updatedTasks;
onValidate({ errors: errors, isValid: isValid } as TaskValidationResult);
onChange(task);
};
@ -66,12 +64,28 @@ export const StageOfGeneralTaskEditor: React.FC<CapabilityEditorProps> = (
);
};
const runValidation = (
const runValidation = async (
task: TaskDefinition,
tasks: TaskDefinition[],
): Record<string, string> => {
): Promise<Record<string, string>> => {
const errors: Record<string, string> = {};
const meta = await templateVersionsService.getTaskMetadata("GeneralTask");
if (task.config.tasks) {
for (const childTask of task.config.tasks as TaskDefinition[]) {
const childErrors = await validateTask(
childTask,
task.config.tasks as TaskDefinition[],
meta,
);
if (childErrors && Object.keys(childErrors).length > 0) {
errors[`${task.config.guid}.tasks`] =
`One or more child tasks are invalid.`;
}
}
}
return errors;
};

View File

@ -27,10 +27,10 @@ export const TagsEditor: React.FC<CapabilityEditorProps> = (props) => {
const runValidation = (
task: TaskDefinition,
tasks: TaskDefinition[],
): Record<string, string> => {
): Promise<Record<string, string>> => {
const errors: Record<string, string> = {};
return errors;
return Promise.resolve(errors);
};
export function defaultsAssignment(

View File

@ -120,7 +120,7 @@ export const TaskCoreEditor: React.FC<CapabilityEditorProps> = (props) => {
const runValidation = (
task: TaskDefinition,
tasks: TaskDefinition[],
): Record<string, string> => {
): Promise<Record<string, string>> => {
const errors: Record<string, string> = {};
if (!(task.config.name as string)?.trim()) {
@ -156,7 +156,7 @@ const runValidation = (
}
}
return errors;
return Promise.resolve(errors);
};
export function defaultsAssignment(

View File

@ -41,12 +41,16 @@ const TaskEditorComponent: React.FC<TaskEditorProps> = ({
);
const runValidation = useCallback(
(
async (
taskToValidate: TaskDefinition,
tasksList: TaskDefinition[],
tasksMetadataList: TaskMetadata[],
) => {
const errors = validateTask(taskToValidate, tasksList, tasksMetadataList);
const errors = await validateTask(
taskToValidate,
tasksList,
tasksMetadataList,
);
setFieldErrors(errors);
onValidate(
taskToValidate.config.guid as string,

View File

@ -36,14 +36,14 @@ export interface capabilityEditorRegistryEntry {
task: TaskDefinition,
tasks: TaskDefinition[],
tasksMetadata: TaskMetadata[],
) => Record<string, string>;
) => Promise<Record<string, string>>;
}
export function validateTask(
export async function validateTask(
task: TaskDefinition,
tasks: TaskDefinition[],
tasksMetadata: TaskMetadata[],
): Record<string, string> {
): Promise<Record<string, string>> {
const taskMeta = tasksMetadata.find((t) => t.taskType === task.type);
const errors: Record<string, string> = {};
@ -55,7 +55,7 @@ export function validateTask(
continue;
}
const validationErrors = entry.ValidationRunner(task, tasks);
const validationErrors = await entry.ValidationRunner(task, tasks);
Object.assign(errors, validationErrors);
}
return errors;