Skip to content

Commit

Permalink
ORV2-2338 Bug Fix (#1641)
Browse files Browse the repository at this point in the history
  • Loading branch information
zgong-gov authored Oct 18, 2024
1 parent ab0fb51 commit cc90fa6
Show file tree
Hide file tree
Showing 50 changed files with 999 additions and 720 deletions.
77 changes: 77 additions & 0 deletions frontend/src/common/helpers/equality.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Nullable } from "../types/common";

/**
* Check if two nullable values are different.
* @param val1 First nullable value to be compared
* @param val2 Second nullable value to be compared
* @returns true when only one of the values are empty, or both are non-empty and different, false otherwise
*/
export const areValuesDifferent = <T>(
val1?: Nullable<T>,
val2?: Nullable<T>,
): boolean => {
if (!val1 && !val2) return false; // Both empty implicitly means that values are the same

if ((val1 && !val2) || (!val1 && val2) || (val1 && val2 && val1 !== val2)) {
return true; // Only one empty, or both are non-empty but different means that values are different
}

return false; // Values are considered equal otherwise
};

/**
* Determine whether or not two arrays, each with only unique primitive values, have the same values.
* @param arr1 First array consisting of only non-duplicate primitive values
* @param arr2 Second array consisting of only non-duplicate primitive values
* @returns Whether or not the two arrays have the same values
*/
export const doUniqueArraysHaveSameItems = <T extends (number | string)>(
arr1: T[],
arr2: T[],
) => {
const set1 = new Set(arr1);
const set2 = new Set(arr2);

for (const val of set1) {
if (!set2.has(val)) return false;
}

for (const val of set2) {
if (!set1.has(val)) return false;
}

return true;
};

/**
* Determine whether or not two arrays, each with objects of a certain type identifiable by keys,
* have the same objects.
* @param arr1 First array consisting of identifiable objects
* @param arr2 Second array consisting of identifiable objects
* @param key Function that returns the identifier of an object of the given type
* @param equalFn Function that compares equality of two objects of the given type
* @returns Whether or not the two arrays have the same objects
*/
export const doUniqueArraysHaveSameObjects = <T, K extends (number | string)>(
arr1: T[],
arr2: T[],
key: (item: T) => K,
equalFn: (item1: T, item2: T) => boolean,
) => {
const map1 = new Map<K, T>(arr1.map(item => [key(item), item]));
const map2 = new Map<K, T>(arr2.map(item => [key(item), item]));

for (const [key, item] of map1) {
const itemInOtherMapWithSameKey = map2.get(key);
if (!itemInOtherMapWithSameKey || !equalFn(item, itemInOtherMapWithSameKey))
return false;
}

for (const [key, item] of map2) {
const itemInOtherMapWithSameKey = map1.get(key);
if (!itemInOtherMapWithSameKey || !equalFn(item, itemInOtherMapWithSameKey))
return false;
}

return true;
};
43 changes: 0 additions & 43 deletions frontend/src/common/helpers/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,25 +122,6 @@ export const getDefaultRequiredVal = <T>(
return defaultVals.find((val) => val != null) ?? fallbackDefault;
};

/**
* Check if two nullable values are different.
* @param val1 First nullable value to be compared
* @param val2 Second nullable value to be compared
* @returns boolean value indicating if values are different.
*/
export const areValuesDifferent = <T>(
val1?: Nullable<T>,
val2?: Nullable<T>,
): boolean => {
if (!val1 && !val2) return false; // both empty === equal

if ((val1 && !val2) || (!val1 && val2) || (val1 && val2 && val1 !== val2)) {
return true; // one empty, or both non-empty but different === different
}

return false; // values are equal otherwise
};

/**
* Returns the file name for a file from API response.
* @param headers The collection of headers in an API response.
Expand Down Expand Up @@ -274,27 +255,3 @@ export const setRedirectInSession = (redirectUri: string) => {
}
}
};

/**
* Determine whether or not two arrays have the same items.
* @param arr1 First array
* @param arr2 Second array
* @returns Whether or not the two arrays contain the same items
*/
export const areArraysEqual = <T extends (number | string)>(
arr1: T[],
arr2: T[],
) => {
const set1 = new Set(arr1);
const set2 = new Set(arr2);

for (const val of set1) {
if (!set2.has(val)) return false;
}

for (const val of set2) {
if (!set1.has(val)) return false;
}

return true;
};
33 changes: 33 additions & 0 deletions frontend/src/common/hooks/useMemoizedArray.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useEffect, useState } from "react";
import { doUniqueArraysHaveSameObjects } from "../helpers/equality";

/**
* Hook that memoizes an array of objects.
* The memoized array only changes when the items in the array change.
* eg. If items === [{a: 1}, {a: 2}], and later [{a: 2}, {a: 1}] is passed in,
* the hook returns the same items [{a: 1}, {a: 2}].
* @param items Array of objects
* @param key Function that returns identifier for each object
* @param equalFn Function that determines whether or not two objects are equal
* @returns Memoized array of objects
*/
export const useMemoizedArray = <T, K extends (number | string)>(
items: T[],
key: (item: T) => K,
equalFn: (item1: T, item2: T) => boolean,
) => {
const [arrayItems, setArrayItems] = useState<T[]>(items);

useEffect(() => {
if (!doUniqueArraysHaveSameObjects(
arrayItems,
items,
key,
equalFn,
)) {
setArrayItems(items);
}
}, [items]);

return arrayItems;
};
26 changes: 26 additions & 0 deletions frontend/src/common/hooks/useMemoizedObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useEffect, useState } from "react";

/**
* Hook that memoizes an object.
* The memoized object only changes when its contents change.
* eg. If obj === {a: 1, b: 2}, and later {b: 2, a: 1} is passed in,
* the hook returns the same obj {a: 1, b: 2}.
* @param obj An object
* @param equalFn Function that determines whether or not two objects are equal
* @returns Memoized object
*/
export const useMemoizedObject = <T>(
obj: T,
equalFn: (obj1: T, obj2: T) => boolean,
) => {
const [memoizedObj, setMemoizedObj] = useState<T>(obj);

useEffect(() => {
if (!equalFn(memoizedObj, obj)) {
setMemoizedObj(obj);
}
}, [obj]);

return memoizedObj;
};

18 changes: 18 additions & 0 deletions frontend/src/features/manageVehicles/helpers/vehicleSubtypes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import {
BaseVehicle,
PowerUnit,
Trailer,
VEHICLE_TYPES,
} from "../types/Vehicle";

/**
* Determine whether or not a vehicle subtype ic considered to be LCV.
* @param subtype Vehicle subtype
Expand All @@ -12,3 +19,14 @@ export const EMPTY_VEHICLE_SUBTYPE = {
type: "",
description: "",
};

export const selectedVehicleSubtype = (vehicle: BaseVehicle) => {
switch (vehicle.vehicleType) {
case VEHICLE_TYPES.POWER_UNIT:
return (vehicle as PowerUnit).powerUnitTypeCode;
case VEHICLE_TYPES.TRAILER:
return (vehicle as Trailer).trailerTypeCode;
default:
return "";
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { createContext } from "react";
import { Dayjs } from "dayjs";

import { PermitVehicleDetails } from "../types/PermitVehicleDetails";
import { LOADetail } from "../../settings/types/SpecialAuthorization";
import { LOADetail } from "../../settings/types/LOADetail";
import { ApplicationFormData } from "../types/application";
import { getDefaultValues } from "../helpers/getDefaultApplicationFormData";
import { DEFAULT_PERMIT_TYPE } from "../types/PermitType";
import { PermitCondition } from "../types/PermitCondition";
import { PowerUnit, Trailer, VehicleSubType } from "../../manageVehicles/types/Vehicle";
import { Nullable } from "../../../common/types/common";
import { CompanyProfile } from "../../manageProfile/types/manageProfile.d";
import { PermitLOA } from "../types/PermitLOA";
import {
PAST_START_DATE_STATUSES,
PastStartDateStatus,
Expand Down Expand Up @@ -49,7 +50,7 @@ interface ApplicationFormContextType {
onToggleSaveVehicle: (saveVehicle: boolean) => void;
onSetVehicle: (vehicleDetails: PermitVehicleDetails) => void;
onClearVehicle: (saveVehicle: boolean) => void;
onUpdateLOAs: (updatedLOAs: LOADetail[]) => void;
onUpdateLOAs: (updatedLOAs: PermitLOA[]) => void;
}

export const ApplicationFormContext = createContext<ApplicationFormContextType>({
Expand Down
84 changes: 84 additions & 0 deletions frontend/src/features/permits/helpers/conditions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isVehicleSubtypeLCV } from "../../manageVehicles/helpers/vehicleSubtypes";
import { LCV_CONDITION } from "../constants/constants";
import { MANDATORY_TROS_CONDITIONS, TROS_CONDITIONS } from "../constants/tros";
import { MANDATORY_TROW_CONDITIONS, TROW_CONDITIONS } from "../constants/trow";
Expand Down Expand Up @@ -83,3 +84,86 @@ export const getDefaultConditions = (
})),
);
};

/**
* Get updated permit conditions based on LCV designation and selected vehicle subtype.
* @param isLcvDesignated Whether or not the LCV designation is to be used
* @param prevSelectedConditions Previously selected permit conditions
* @param vehicleSubtype Selected vehicle subtype
* @returns Updated permit conditions
*/
export const getUpdatedConditionsForLCV = (
isLcvDesignated: boolean,
prevSelectedConditions: PermitCondition[],
vehicleSubtype: string,
) => {
if (!isLcvDesignated) {
// If LCV not designated, remove LCV condition
return prevSelectedConditions.filter(
({ condition }: PermitCondition) => condition !== LCV_CONDITION.condition,
);
}

// If LCV is designated, and vehicle subtype isn't LCV but conditions have LCV,
// then remove that LCV condition
if (
!isVehicleSubtypeLCV(vehicleSubtype)
&& prevSelectedConditions.some(({ condition }) => condition === LCV_CONDITION.condition)
) {
return prevSelectedConditions.filter(
({ condition }: PermitCondition) => condition !== LCV_CONDITION.condition,
);
}

// If LCV is designated, and vehicle subtype is LCV but conditions don't have LCV,
// then add that LCV condition
if (
isVehicleSubtypeLCV(vehicleSubtype)
&& !prevSelectedConditions.some(({ condition }) => condition === LCV_CONDITION.condition)
) {
return sortConditions([...prevSelectedConditions, LCV_CONDITION]);
}

// In other cases, the conditions are valid
return prevSelectedConditions;
};

/**
* Get permit condition selection state, including all selected, unselected, and disabled conditions.
* @param permitType Permit type
* @param isLcvDesignated Whether or not the LCV designation is to be used
* @param vehicleSubtype Selected vehicle subtype
* @param prevSelectedConditions Previously selected permit conditions
* @returns Permit condition selection state
*/
export const getPermitConditionSelectionState = (
permitType: PermitType,
isLcvDesignated: boolean,
vehicleSubtype: string,
prevSelectedConditions: PermitCondition[],
): PermitCondition[] => {
const defaultConditionsForPermitType = getDefaultConditions(
permitType,
isLcvDesignated && isVehicleSubtypeLCV(vehicleSubtype),
);

const updatedConditionsInForm = getUpdatedConditionsForLCV(
isLcvDesignated,
prevSelectedConditions,
vehicleSubtype,
);

return defaultConditionsForPermitType.map((defaultCondition) => {
// Select all conditions that were previously selected
const existingCondition = updatedConditionsInForm.find(
(c) => c.condition === defaultCondition.condition,
);

return {
...defaultCondition,
checked: existingCondition
? existingCondition.checked
: defaultCondition.checked,
};
});
};
8 changes: 4 additions & 4 deletions frontend/src/features/permits/helpers/dateSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { Dayjs } from "dayjs";

import { BASE_DAYS_IN_YEAR, TERM_DURATION_INTERVAL_DAYS } from "../constants/constants";
import { PERMIT_TYPES, PermitType } from "../types/PermitType";
import { getExpiryDate } from "./permitState";
import { getMostRecentExpiryFromLOAs } from "./permitLOA";
import { PermitLOA } from "../types/PermitLOA";
import {
MAX_TROS_DURATION,
MIN_TROS_DURATION,
Expand All @@ -15,9 +18,6 @@ import {
TROW_DURATION_INTERVAL_DAYS,
TROW_DURATION_OPTIONS,
} from "../constants/trow";
import { getExpiryDate } from "./permitState";
import { LOADetail } from "../../settings/types/SpecialAuthorization";
import { getMostRecentExpiryFromLOAs } from "./permitLOA";

/**
* Get list of selectable duration options for a given permit type.
Expand Down Expand Up @@ -94,7 +94,7 @@ export const getAvailableDurationOptions = (
value: number;
label: string;
}[],
selectedLOAs: LOADetail[],
selectedLOAs: PermitLOA[],
startDate: Dayjs,
) => {
const mostRecentLOAExpiry = getMostRecentExpiryFromLOAs(selectedLOAs);
Expand Down
Loading

0 comments on commit cc90fa6

Please sign in to comment.