Skip to content

Commit

Permalink
Do not allow child be in the team hierarchy with itself as parent
Browse files Browse the repository at this point in the history
  • Loading branch information
anagperal committed Nov 7, 2024
1 parent 3dd5b41 commit adc486e
Show file tree
Hide file tree
Showing 11 changed files with 154 additions and 31 deletions.
5 changes: 4 additions & 1 deletion src/domain/entities/ValidationError.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Maybe } from "../../utils/ts-utils";

export type ValidationErrorKey = "field_is_required" | "field_is_required_na";
export type ValidationErrorKey =
| "field_is_required"
| "field_is_required_na"
| "cannot_create_cyclycal_dependency";

export type ValidationError = {
property: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,71 @@
import { Maybe } from "../../../utils/ts-utils";
import { TEAM_ROLE_FIELD_ID } from "../../../webapp/pages/form-page/incident-management-team-member-assignment/mapIncidentManagementTeamMemberToInitialFormState";
import { Struct } from "../generic/Struct";
import { ValidationError } from "../ValidationError";
import { TeamMember } from "./TeamMember";

interface IncidentManagementTeamAttrs {
lastUpdated: Maybe<Date>;
teamHierarchy: TeamMember[];
}

export class IncidentManagementTeam extends Struct<IncidentManagementTeamAttrs>() {}
export class IncidentManagementTeam extends Struct<IncidentManagementTeamAttrs>() {
static validateNotCyclicalDependency(
teamMember: string | undefined,
reportsToUsername: string | undefined,
currentIncidentManagementTeamHierarchy: TeamMember[],
property: string
): ValidationError[] {
const descendantsUsernamesByParentUsername = getAllDescendantsUsernamesByParentUsername(
currentIncidentManagementTeamHierarchy
);

const descendantsFromTeamMember =
descendantsUsernamesByParentUsername.get(teamMember ?? "") ?? [];

const isCyclicalDependency = descendantsFromTeamMember.includes(reportsToUsername ?? "");

return property === TEAM_ROLE_FIELD_ID || !isCyclicalDependency
? []
: [
{
property: property,
value: "",
errors: ["cannot_create_cyclycal_dependency"],
},
];
}
}

function getAllDescendantsUsernamesByParentUsername(
teamMembers: TeamMember[]
): Map<string, string[]> {
const initialMap = teamMembers.reduce((acc, member) => {
const entries = (member.teamRoles ?? []).map(role => ({
parentUsername: role.reportsToUsername ?? "PARENT_ROOT",
username: member.username,
}));

return entries.reduce((mapAcc, { parentUsername, username }) => {
return mapAcc.set(parentUsername, [...(mapAcc.get(parentUsername) ?? []), username]);
}, acc);
}, new Map<string, string[]>());

const getDescendantUsernames = (
parent: string,
processedParents = new Set<string>()
): string[] => {
if (processedParents.has(parent)) return [];
processedParents.add(parent);

return (initialMap.get(parent) ?? []).flatMap(username => [
username,
...getDescendantUsernames(username, processedParents),
]);
};

return Array.from(initialMap.keys()).reduce((descendantsMap, parent) => {
descendantsMap.set(parent, getDescendantUsernames(parent));
return descendantsMap;
}, new Map<string, string[]>());
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export function getIncidentManagementTeamWithOptions(
labels: {
errors: {
field_is_required: "This field is required",
cannot_create_cyclycal_dependency:
"Cannot depend on itself in the team hierarchy",
},
},
rules: [
Expand All @@ -62,6 +64,14 @@ export function getIncidentManagementTeamWithOptions(
],
sectionsWithFieldsToDisableOption: [SECTION_IDS.reportsTo],
},
{
type: "disableFieldOptionWithSameFieldValue",
fieldId: incidentManagementTeamBuilderCodesWithoutRoles.reportsToUsername,
fieldIdsToDisableOption: [
incidentManagementTeamBuilderCodesWithoutRoles.teamMemberAssigned,
],
sectionsWithFieldsToDisableOption: [SECTION_IDS.teamMemberAssigned],
},
],
};
return Future.success(incidentManagementTeamMemberFormData);
Expand Down
14 changes: 10 additions & 4 deletions src/webapp/components/form/FormFieldsState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type FormFieldStateBase<T> = {
maxWidth?: string;
value: T;
type: FieldType;
updateAllStateWithValidationErrors?: boolean;
};

export type FormTextFieldState = FormFieldStateBase<string> & {
Expand Down Expand Up @@ -151,15 +152,20 @@ export function updateFields(
fieldValidationErrors?: ValidationError[]
): FormFieldState[] {
return formFields.map(field => {
const errors =
fieldValidationErrors?.find(error => error.property === field.id)?.errors || [];
if (field.id === updatedField.id) {
return {
...updatedField,
errors:
fieldValidationErrors?.find(error => error.property === updatedField.id)
?.errors || [],
errors: errors,
};
} else {
return field;
return updatedField.updateAllStateWithValidationErrors
? {
...field,
errors: errors,
}
: field;
}
});
}
Expand Down
22 changes: 19 additions & 3 deletions src/webapp/components/form/FormSectionsState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ function hasSectionAFieldWithNotApplicable(sectionsState: FormSectionState) {
return sectionsState.fields.some(field => field.hasNotApplicable);
}

export function getFieldValueByIdFromSections(
sectionsState: FormSectionState[],
fieldId: string
): FormFieldState["value"] | undefined {
const section = sectionsState.find(section =>
section.fields.some(field => field.id === fieldId)
);
return section?.fields.find(field => field.id === fieldId)?.value;
}

// UPDATES:

export function applyEffectNotApplicableFieldUpdatedInSections(
Expand Down Expand Up @@ -84,8 +94,11 @@ export function updateSections(
fieldValidationErrors?: ValidationError[]
): FormSectionState[] {
return formSectionsState.map(section => {
const hasToUpdateSection =
isFieldInSection(section, updatedField) ||
updatedField.updateAllStateWithValidationErrors;
if (section.subsections?.length) {
const maybeUpdatedSection = isFieldInSection(section, updatedField)
const maybeUpdatedSection = hasToUpdateSection
? updateSectionState(section, updatedField, fieldValidationErrors)
: section;
return {
Expand All @@ -97,7 +110,7 @@ export function updateSections(
),
};
} else {
return isFieldInSection(section, updatedField)
return hasToUpdateSection
? updateSectionState(section, updatedField, fieldValidationErrors)
: section;
}
Expand All @@ -109,7 +122,10 @@ function updateSectionState(
updatedField: FormFieldState,
fieldValidationErrors?: ValidationError[]
): FormSectionState {
if (isFieldInSection(formSectionState, updatedField)) {
if (
isFieldInSection(formSectionState, updatedField) ||
updatedField.updateAllStateWithValidationErrors
) {
return {
...formSectionState,
fields: updateFields(formSectionState.fields, updatedField, fieldValidationErrors),
Expand Down
4 changes: 2 additions & 2 deletions src/webapp/components/form/FormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ export function isValidForm(formSections: FormSectionState[]): boolean {

return allFields.every(field => {
const validationErrors = validateField(field, allFields);

return !validationErrors || validationErrors.errors.length === 0;
const hasErrorsInFields = allFields.some(f => f.errors.length > 0);
return !hasErrorsInFields && (!validationErrors || validationErrors.errors.length === 0);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,19 @@ export function mapIncidentManagementTeamMemberToInitialFormState(
multiple: false,
options:
teamRoleToAssing?.roleId === INCIDENT_MANAGER_ROLE
? incidentManagerOptions
: teamMemberOptions,
? incidentManagerOptions.map(user => ({
...user,
disabled: user.value === teamRoleToAssing?.reportsToUsername,
}))
: teamMemberOptions.map(user => ({
...user,
disabled: user.value === teamRoleToAssing?.reportsToUsername,
})),
value: incidentManagementTeamMember?.username || "",
required: true,
showIsRequired: true,
disabled: false,
updateAllStateWithValidationErrors: true,
},
],
},
Expand All @@ -118,6 +125,7 @@ export function mapIncidentManagementTeamMemberToInitialFormState(
required: teamRoleToAssing?.roleId !== INCIDENT_MANAGER_ROLE,
showIsRequired: teamRoleToAssing?.roleId !== INCIDENT_MANAGER_ROLE,
disabled: teamRoleToAssing?.roleId === INCIDENT_MANAGER_ROLE,
updateAllStateWithValidationErrors: true,
},
],
},
Expand Down
2 changes: 1 addition & 1 deletion src/webapp/pages/form-page/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Id } from "../../../domain/entities/Ref";
import { FormState } from "../../components/form/FormState";
import { RouteName, useRoutes } from "../../hooks/useRoutes";
import { mapFormStateToEntityData } from "./mapFormStateToEntityData";
import { updateAndValidateFormState } from "./disease-outbreak-event/utils/updateDiseaseOutbreakEventFormState";
import { updateAndValidateFormState } from "./utils/updateDiseaseOutbreakEventFormState";
import { FormFieldState } from "../../components/form/FormFieldsState";
import { FormType } from "./FormPage";
import { ConfigurableForm, FormLables } from "../../../domain/entities/ConfigurableForm";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Rule } from "../../../../../domain/entities/Rule";
import { FormFieldState } from "../../../../components/form/FormFieldsState";
import { Rule } from "../../../../domain/entities/Rule";
import { FormFieldState } from "../../../components/form/FormFieldsState";
import {
disableFieldOptionWithSameFieldValueInSection,
disableFieldsByFieldValueInSection,
toggleSectionVisibilityByFieldValue,
} from "../../../../components/form/FormSectionsState";
import { FormState } from "../../../../components/form/FormState";
} from "../../../components/form/FormSectionsState";
import { FormState } from "../../../components/form/FormState";

export function applyRulesInFormState(
currentFormState: FormState,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { ConfigurableForm } from "../../../../../domain/entities/ConfigurableForm";
import { DiseaseOutbreakEvent } from "../../../../../domain/entities/disease-outbreak-event/DiseaseOutbreakEvent";
import { ValidationError } from "../../../../../domain/entities/ValidationError";
import { FormFieldState } from "../../../../components/form/FormFieldsState";
import { incidentManagementTeamBuilderCodesWithoutRoles } from "../../../../data/repositories/consts/IncidentManagementTeamBuilderConstants";
import { ConfigurableForm } from "../../../../domain/entities/ConfigurableForm";
import { DiseaseOutbreakEvent } from "../../../../domain/entities/disease-outbreak-event/DiseaseOutbreakEvent";
import { IncidentManagementTeam } from "../../../../domain/entities/incident-management-team/IncidentManagementTeam";
import { ValidationError } from "../../../../domain/entities/ValidationError";
import { FormFieldState } from "../../../components/form/FormFieldsState";
import { getFieldValueByIdFromSections } from "../../../components/form/FormSectionsState";
import {
FormState,
isValidForm,
updateFormStateAndApplySideEffects,
updateFormStateWithFieldErrors,
validateForm,
} from "../../../../components/form/FormState";
} from "../../../components/form/FormState";
import { applyRulesInFormState } from "./applyRulesInFormState";

export function updateAndValidateFormState(
Expand Down Expand Up @@ -67,8 +70,24 @@ function validateFormState(
break;
case "incident-response-action":
break;
case "incident-management-team-member-assignment":
case "incident-management-team-member-assignment": {
const reportsToUsername = getFieldValueByIdFromSections(
updatedForm.sections,
incidentManagementTeamBuilderCodesWithoutRoles.reportsToUsername
) as string | undefined;
const teamMemberAssigned = getFieldValueByIdFromSections(
updatedForm.sections,
incidentManagementTeamBuilderCodesWithoutRoles.teamMemberAssigned
) as string | undefined;

return IncidentManagementTeam.validateNotCyclicalDependency(
teamMemberAssigned,
reportsToUsername,
configurableForm?.currentIncidentManagementTeam?.teamHierarchy || [],
updatedField.id
);
break;
}
}

return [...formValidationErrors, ...entityValidationErrors];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,12 @@ function checkIfParentsAndAllChildrenSelected(
}))
.filter(teamMember => teamMember.teamRoles?.length);

const allTeamRoleChildrenIdsByParentId = getAllTeamRoleChildrenIdsByParentTeamRoleId(
const allTeamRoleChildrenIdsByParentUsername = getAllTeamRoleChildrenIdsByParentUsername(
incidentManagementTeamHierarchy
);

return selectedItemsInTeamHierarchy.every(teamMember => {
const teamRoleChildrenIds = allTeamRoleChildrenIdsByParentId.get(teamMember.username);
const teamRoleChildrenIds = allTeamRoleChildrenIdsByParentUsername.get(teamMember.username);
return (
!teamRoleChildrenIds ||
teamRoleChildrenIds.every(childId => {
Expand All @@ -225,14 +225,14 @@ function checkIfParentsAndAllChildrenSelected(
});
}

function getAllTeamRoleChildrenIdsByParentTeamRoleId(teamMembers: TeamMember[]): Map<string, Id[]> {
function getAllTeamRoleChildrenIdsByParentUsername(teamMembers: TeamMember[]): Map<string, Id[]> {
return teamMembers.reduce((acc, teamMember) => {
return (
teamMember.teamRoles?.reduce((innerAcc, teamRole) => {
const parentTeamRoleId = teamRole.reportsToUsername;
if (parentTeamRoleId) {
const existingChildren = innerAcc.get(parentTeamRoleId) || [];
innerAcc.set(parentTeamRoleId, [...existingChildren, teamRole.id]);
const parentUsername = teamRole.reportsToUsername;
if (parentUsername) {
const existingChildren = innerAcc.get(parentUsername) || [];
innerAcc.set(parentUsername, [...existingChildren, teamRole.id]);
}
return innerAcc;
}, acc) || acc
Expand Down

0 comments on commit adc486e

Please sign in to comment.