Improvements to the validation engine
This commit is contained in:
parent
4b3f65ec20
commit
226d402578
@ -89,6 +89,8 @@ export const AssigneesOfITaskAssigneeEditor: React.FC<CapabilityEditorProps> = (
|
|||||||
onChange(clone);
|
onChange(clone);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const guid = task.config.guid as string;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
Select assigness (you can select either a role or a contact)
|
Select assigness (you can select either a role or a contact)
|
||||||
@ -113,7 +115,7 @@ export const AssigneesOfITaskAssigneeEditor: React.FC<CapabilityEditorProps> = (
|
|||||||
name="role"
|
name="role"
|
||||||
label="Role"
|
label="Role"
|
||||||
value={assignee.role}
|
value={assignee.role}
|
||||||
error={fieldErrors?.[`assignees[${index}].role`]}
|
error={fieldErrors?.[`${guid}.assignees[${index}].role`]}
|
||||||
onChange={(name: string, val: GeneralIdRef | null) =>
|
onChange={(name: string, val: GeneralIdRef | null) =>
|
||||||
updateAssignee(index, { ...assignee, role: val })
|
updateAssignee(index, { ...assignee, role: val })
|
||||||
}
|
}
|
||||||
@ -125,7 +127,7 @@ export const AssigneesOfITaskAssigneeEditor: React.FC<CapabilityEditorProps> = (
|
|||||||
name="contact"
|
name="contact"
|
||||||
label="Contact"
|
label="Contact"
|
||||||
value={assignee.contact}
|
value={assignee.contact}
|
||||||
error={fieldErrors?.[`assignees[${index}].contact`]}
|
error={fieldErrors?.[`${guid}.assignees[${index}].contact`]}
|
||||||
onChange={(name: string, val: GeneralIdRef | null) =>
|
onChange={(name: string, val: GeneralIdRef | null) =>
|
||||||
updateAssignee(index, { ...assignee, contact: val })
|
updateAssignee(index, { ...assignee, contact: val })
|
||||||
}
|
}
|
||||||
@ -136,7 +138,7 @@ export const AssigneesOfITaskAssigneeEditor: React.FC<CapabilityEditorProps> = (
|
|||||||
name="raci"
|
name="raci"
|
||||||
label="RACI"
|
label="RACI"
|
||||||
value={assignee.raci}
|
value={assignee.raci}
|
||||||
error={fieldErrors?.[`assignees[${index}].raci`]}
|
error={fieldErrors?.[`${guid}.assignees[${index}].raci`]}
|
||||||
onChange={(name: string, val: string) =>
|
onChange={(name: string, val: string) =>
|
||||||
updateAssignee(index, { ...assignee, raci: val })
|
updateAssignee(index, { ...assignee, raci: val })
|
||||||
}
|
}
|
||||||
@ -167,22 +169,30 @@ const runValidation = (
|
|||||||
tasks: TaskDefinition[],
|
tasks: TaskDefinition[],
|
||||||
): Record<string, string> => {
|
): Record<string, string> => {
|
||||||
const errors: Record<string, string> = {};
|
const errors: Record<string, 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["assignees"] = "At least one assignee is required.";
|
errors[`${guid}.assignees`] = "At least one assignee is required.";
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
assignees.forEach((a, i) => {
|
assignees.forEach((a, i) => {
|
||||||
if (!a.role && !a.contact) {
|
const noContactSelected = !a.contact || a.contact?.id === BigInt(0);
|
||||||
//errors[`assignees[${i}]`] = "Either role or contact must be selected.";
|
|
||||||
errors[`assignees`] = "Either role or contact must be selected.";
|
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) {
|
if (!a.raci) {
|
||||||
//errors[`assignees[${i}].raci`] = "RACI is required.";
|
errors[`${guid}.assignees[${i}].raci`] = "RACI is required.";
|
||||||
errors[`assignees`] = "RACI is required.";
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { SelectableList } from "../../../../components/common/SelectableList";
|
|||||||
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons";
|
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { sortTasksTopologically } from "./workflowGraphUtils";
|
import { sortTasksTopologically } from "./workflowGraphUtils";
|
||||||
import { useCapabilityDefaults } from "./useCapabilityDefaults";
|
import { useCapabilityDefaults, validateTask } from "./useCapabilityDefaults";
|
||||||
|
|
||||||
interface TaskListProps {
|
interface TaskListProps {
|
||||||
tasks: TaskDefinition[];
|
tasks: TaskDefinition[];
|
||||||
@ -17,6 +17,7 @@ interface TaskListProps {
|
|||||||
onChange: (tasks: TaskDefinition[]) => void;
|
onChange: (tasks: TaskDefinition[]) => void;
|
||||||
selectedTask: TaskDefinition | null;
|
selectedTask: TaskDefinition | null;
|
||||||
onSelectTask: (task: TaskDefinition | null) => void;
|
onSelectTask: (task: TaskDefinition | null) => void;
|
||||||
|
onValidate: (taskId: string, isValid: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TaskList: React.FC<TaskListProps> = ({
|
const TaskList: React.FC<TaskListProps> = ({
|
||||||
@ -26,6 +27,7 @@ const TaskList: React.FC<TaskListProps> = ({
|
|||||||
onChange,
|
onChange,
|
||||||
selectedTask,
|
selectedTask,
|
||||||
onSelectTask,
|
onSelectTask,
|
||||||
|
onValidate,
|
||||||
}) => {
|
}) => {
|
||||||
const runDefaults = useCapabilityDefaults(tasksMetadata);
|
const runDefaults = useCapabilityDefaults(tasksMetadata);
|
||||||
|
|
||||||
@ -43,7 +45,12 @@ const TaskList: React.FC<TaskListProps> = ({
|
|||||||
runDefaults(capability, newTask, tasks);
|
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);
|
onSelectTask(newTask);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import {
|
|||||||
TaskMetadata,
|
TaskMetadata,
|
||||||
} from "../services/WorkflowTemplateService";
|
} from "../services/WorkflowTemplateService";
|
||||||
import { capabilityEditorRegistry } from "./capabilityEditorRegistry";
|
import { capabilityEditorRegistry } from "./capabilityEditorRegistry";
|
||||||
|
import { validateTask } from "./useCapabilityDefaults";
|
||||||
|
|
||||||
interface TaskEditorProps {
|
interface TaskEditorProps {
|
||||||
task: TaskDefinition;
|
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) => {
|
const handleTaskChange = (updatedTask: TaskDefinition) => {
|
||||||
// Update the task list
|
// Update the task list
|
||||||
const updatedTasks = tasks.map((t) =>
|
const updatedTasks = tasks.map((t) =>
|
||||||
@ -49,7 +32,7 @@ export const TaskEditor: React.FC<TaskEditorProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Run validation
|
// Run validation
|
||||||
const errors = validateTask(updatedTask, updatedTasks);
|
const errors = validateTask(updatedTask, updatedTasks, tasksMetadata);
|
||||||
setFieldErrors(errors);
|
setFieldErrors(errors);
|
||||||
|
|
||||||
// Bubble validity up
|
// Bubble validity up
|
||||||
|
|||||||
@ -75,6 +75,7 @@ const TasksTab: React.FC<TasksTabProps> = ({
|
|||||||
onChange={handleTasksChange}
|
onChange={handleTasksChange}
|
||||||
selectedTask={selectedTask}
|
selectedTask={selectedTask}
|
||||||
onSelectTask={setSelectedTask}
|
onSelectTask={setSelectedTask}
|
||||||
|
onValidate={onValidate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{selectedTask && (
|
{selectedTask && (
|
||||||
|
|||||||
@ -35,9 +35,32 @@ export interface capabilityEditorRegistryEntry {
|
|||||||
ValidationRunner?: (
|
ValidationRunner?: (
|
||||||
task: TaskDefinition,
|
task: TaskDefinition,
|
||||||
tasks: TaskDefinition[],
|
tasks: TaskDefinition[],
|
||||||
|
tasksMetadata: TaskMetadata[],
|
||||||
) => Record<string, string>;
|
) => 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[]) {
|
export function useCapabilityDefaults(taskMetadata: TaskMetadata[]) {
|
||||||
const { t: tTaskType } = useTranslation(Namespaces.TaskTypes);
|
const { t: tTaskType } = useTranslation(Namespaces.TaskTypes);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user