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 = ( const runValidation = (
task: TaskDefinition, task: TaskDefinition,
tasks: TaskDefinition[], tasks: TaskDefinition[],
): Record<string, string> => { ): Promise<Record<string, string>> => {
const errors: Record<string, string> = {}; const errors: Record<string, string> = {};
const guid = task.config.guid as string; const guid = task.config.guid as string;
const assignees = task.config.assignees as ITaskAssignee[] | undefined; const assignees = task.config.assignees as ITaskAssignee[] | undefined;
if (!assignees || assignees.length === 0) { if (!assignees || assignees.length === 0) {
errors[`${guid}.assignees`] = "At least one assignee is required."; errors[`${guid}.assignees`] = "At least one assignee is required.";
return errors; return Promise.resolve(errors);
} }
assignees.forEach((a, i) => { assignees.forEach((a, i) => {

View File

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

View File

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

View File

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

View File

@ -31,7 +31,7 @@ export const StageOfGeneralTaskEditor: React.FC<CapabilityEditorProps> = (
const runDefaults = useCapabilityDefaults(tasksMetadata); const runDefaults = useCapabilityDefaults(tasksMetadata);
const handleAddTask = (selectedType: TaskMetadata) => { const handleAddTask = async (selectedType: TaskMetadata) => {
const newTask: TaskDefinition = { const newTask: TaskDefinition = {
type: selectedType.taskType, type: selectedType.taskType,
@ -48,14 +48,12 @@ export const StageOfGeneralTaskEditor: React.FC<CapabilityEditorProps> = (
}); });
const updatedTasks = [...childTasks, newTask]; const updatedTasks = [...childTasks, newTask];
const errors = validateTask(newTask, updatedTasks, tasksMetadata); const errors = await validateTask(newTask, updatedTasks, tasksMetadata);
const isValid = Object.keys(errors).length === 0; const isValid = Object.keys(errors).length === 0;
task.config.tasks = updatedTasks; task.config.tasks = updatedTasks;
onValidate({ errors: errors, isValid: isValid } as TaskValidationResult);
onChange(task); onChange(task);
}; };
@ -66,12 +64,28 @@ export const StageOfGeneralTaskEditor: React.FC<CapabilityEditorProps> = (
); );
}; };
const runValidation = ( const runValidation = async (
task: TaskDefinition, task: TaskDefinition,
tasks: TaskDefinition[], tasks: TaskDefinition[],
): Record<string, string> => { ): Promise<Record<string, string>> => {
const errors: 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; return errors;
}; };

View File

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

View File

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

View File

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

View File

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