diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 5c07f309ea..1c8c549713 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -9,11 +9,11 @@ declare module 'vue' { export interface GlobalComponents { AddressGroupComponent: typeof import('./src/components/grouping/AddressGroupComponent.vue')['default'] AutoCompleteInputComponent: typeof import('./src/components/forms/AutoCompleteInputComponent.vue')['default'] + ComboBoxInputComponent: typeof import('./src/components/forms/ComboBoxInputComponent.vue')['default'] ContactGroupComponent: typeof import('./src/components/grouping/ContactGroupComponent.vue')['default'] DataFetcher: typeof import('./src/components/DataFetcher.vue')['default'] DateInputComponent: typeof import('./src/components/forms/DateInputComponent/index.vue')['default'] DateInputPart: typeof import('./src/components/forms/DateInputComponent/DateInputPart.vue')['default'] - DropdownInputComponent: typeof import('./src/components/forms/DropdownInputComponent.vue')['default'] ErrorNotificationGroupingComponent: typeof import('./src/components/grouping/ErrorNotificationGroupingComponent.vue')['default'] FuzzyMatchNotificationGroupingComponent: typeof import('./src/components/grouping/FuzzyMatchNotificationGroupingComponent.vue')['default'] LoadingOverlayComponent: typeof import('./src/components/LoadingOverlayComponent.vue')['default'] diff --git a/frontend/src/components/forms/DropdownInputComponent.vue b/frontend/src/components/forms/ComboBoxInputComponent.vue similarity index 100% rename from frontend/src/components/forms/DropdownInputComponent.vue rename to frontend/src/components/forms/ComboBoxInputComponent.vue diff --git a/frontend/src/components/grouping/AddressGroupComponent.vue b/frontend/src/components/grouping/AddressGroupComponent.vue index 2db5110765..e110db652b 100644 --- a/frontend/src/components/grouping/AddressGroupComponent.vue +++ b/frontend/src/components/grouping/AddressGroupComponent.vue @@ -320,7 +320,7 @@ const section = (index: number, purpose: string) => `section-address-${index} ${ :params="{ method: 'GET' }" #="{ content }" > - `section-address-${index} ${ /> - { @error="validation.phoneNumber = !$event" /> - { /> - :params="{ method: 'GET' }" #="{ content }" > - /> - this.loadUser(), timeDifference); + if (!this.sessionRefreshIntervalId) { + this.sessionRefreshIntervalId = setInterval( + () => this.loadUser(), + timeDifference + ); } } else { this.user = undefined; @@ -125,7 +128,7 @@ class ForestClientUserSession implements SessionProperties { additionalInfo.lastName = payload.family_name; } - if(payload["custom:idp_business_name"]){ + if (payload["custom:idp_business_name"]) { additionalInfo.businessName = payload["custom:idp_business_name"]; } @@ -134,12 +137,18 @@ class ForestClientUserSession implements SessionProperties { (additionalInfo.firstName === "" && additionalInfo.lastName === "") ) { const name = payload["custom:idp_display_name"]; - const nameParts: string[] = name.includes(",") ? name.split(",") : name.split(" "); + const nameParts: string[] = name.includes(",") + ? name.split(",") + : name.split(" "); if (provider === "idir" && nameParts.length >= 2) { // For IDIR, split by comma and then by space for the first name as the value will be Lastname, Firsname MIN:XX additionalInfo.lastName = nameParts[0].trim(); - additionalInfo.firstName = nameParts[1].trim().split(" ").slice(0, -1).join(" ") + additionalInfo.firstName = nameParts[1] + .trim() + .split(" ") + .slice(0, -1) + .join(" "); } else if (nameParts.length >= 2) { // For others, assume space separates the first and last names additionalInfo.firstName = nameParts[0].trim(); diff --git a/frontend/src/helpers/validators/GlobalValidators.ts b/frontend/src/helpers/validators/GlobalValidators.ts index 723dce5326..f3d9c966b8 100644 --- a/frontend/src/helpers/validators/GlobalValidators.ts +++ b/frontend/src/helpers/validators/GlobalValidators.ts @@ -39,7 +39,8 @@ const errorBus = useEventBus( export const isNotEmpty = (message: string = "This field is required") => (value: string): string => { - if (value && (typeof value === "string") && value.trim().length > 0) return ""; + if (value && typeof value === "string" && value.trim().length > 0) + return ""; return message; }; @@ -191,7 +192,7 @@ export const isExactSize = (message: string = "This field must have the defined size") => (size: number) => { return (value: string): string => { - if (isNotEmpty(message)(value) === "" && value.length == size) + if (isNotEmpty(message)(value) === "" && value.length == size) return ""; return message; }; @@ -278,7 +279,10 @@ export const isNoSpecialCharacters = }; export const isIdCharacters = - (field: string = "field", message: string = `The ${field} can only contain: A-Z or 0-9`) => + ( + field: string = "field", + message: string = `The ${field} can only contain: A-Z or 0-9` + ) => (value: string): string => { if (idCharacters.test(value)) return ""; return message; @@ -287,7 +291,7 @@ export const isIdCharacters = export const hasOnlyNamingCharacters = ( field: string = "field", - message: string = `The ${field} can only contain: A-Z, a-z, 0-9, space, apostrophe or hyphen`, + message: string = `The ${field} can only contain: A-Z, a-z, 0-9, space, apostrophe or hyphen` ) => (value: string): string => { if (nameRegex.test(value)) return ""; @@ -297,7 +301,7 @@ export const hasOnlyNamingCharacters = export const isAscii = ( field: string = "field", - message: string = `The ${field} can only contain: A-Z, a-z, 0-9, space or common symbols`, + message: string = `The ${field} can only contain: A-Z, a-z, 0-9, space or common symbols` ) => (value: string): string => { if (ascii.test(value)) return ""; @@ -307,7 +311,7 @@ export const isAscii = export const isAsciiLineBreak = ( field: string = "field", - message: string = `The ${field} can only contain: A-Z, a-z, 0-9, space, line break or common symbols`, + message: string = `The ${field} can only contain: A-Z, a-z, 0-9, space, line break or common symbols` ) => (value: string): string => { if (asciiLineBreak.test(value)) return ""; @@ -433,7 +437,7 @@ export const isDateInThePast = (message: string) => (value: string) => { export const isRegex = ( regex: RegExp, - message: string = `This field must conform to the following regular expression: ${regex}`, + message: string = `This field must conform to the following regular expression: ${regex}` ) => (value: string): string => { if (regex.test(value)) return ""; @@ -447,7 +451,10 @@ export const isRegex = * @returns A function that accepts a validator and applies it to the portion of the string obtained by applying the selector. */ export const validateSelection = - (selector: (value: string) => string, selectorErrorMessage = "Value could not be validated") => + ( + selector: (value: string) => string, + selectorErrorMessage = "Value could not be validated" + ) => (validator: (value: string) => string) => (value: string): string => { try { @@ -466,17 +473,30 @@ export const validateSelection = */ export const optional = (validator: (value: string) => string) => - (value: string): string => { - return value === "" || value === null || value === undefined ? "" : validator(value); + (value: string): string => { + return value === "" || value === null || value === undefined + ? "" + : validator(value); }; -export const isMinSizeMsg = (fieldName: string, minSize: number): ((value: string) => string) => - isMinSize(`The ${fieldName} must contain at least ${minSize} characters`)(minSize); - -export const isMaxSizeMsg = (fieldName: string, maxSize: number): ((value: string) => string) => +export const isMinSizeMsg = ( + fieldName: string, + minSize: number +): ((value: string) => string) => + isMinSize(`The ${fieldName} must contain at least ${minSize} characters`)( + minSize + ); + +export const isMaxSizeMsg = ( + fieldName: string, + maxSize: number +): ((value: string) => string) => isMaxSize(`The ${fieldName} has a ${maxSize} character limit`)(maxSize); -export const isExactSizMsg = (fieldName: string, size: number): ((value: string) => string) => +export const isExactSizMsg = ( + fieldName: string, + size: number +): ((value: string) => string) => isExactSize(`The ${fieldName} must contain ${size} characters`)(size); /** @@ -515,31 +535,38 @@ export const getFieldValue = (path: string, value: any): string | string[] => { return temporaryValue; }; - /** * Parses the aggregator condition and returns an object containing the parsed values. * @param conditional - The aggregator condition to parse. * @returns An object with the parsed values: value1, operator, and value2. * @throws {Error} If the condition format is invalid. */ -const parseAggregatorCondition = (conditinal: string) : { value1: string, operator: string, value2: string } =>{ - - if (conditinal.includes("&&") || conditinal.includes("||")) { - const regex = /(.+?)\s*(&&|\|\|)\s*(.+?)$/; - const match = conditinal.replace("(","").replace(")","").match(regex); - if (match) { - return { - value1: match[1], - operator: match[2], - value2: match[3] - }; - } else { - throw new Error("Invalid condition format it should be just string or string && string or string || string -> " + conditinal); - } - } +const parseAggregatorCondition = ( + conditinal: string +): { value1: string; operator: string; value2: string } => { + if (conditinal.includes("&&") || conditinal.includes("||")) { + const regex = /(.+?)\s*(&&|\|\|)\s*(.+?)$/; + const match = conditinal.replace("(", "").replace(")", "").match(regex); + if (match) { + return { + value1: match[1], + operator: match[2], + value2: match[3], + }; + } else { + throw new Error( + "Invalid condition format it should be just string or string && string or string || string -> " + + conditinal + ); + } + } - return { value1: conditinal.replace("(","").replace(")",""), operator: "&&", value2: "true" }; -} + return { + value1: conditinal.replace("(", "").replace(")", ""), + operator: "&&", + value2: "true", + }; +}; /** * Parses an equality condition and returns an object with the parsed values. @@ -547,22 +574,26 @@ const parseAggregatorCondition = (conditinal: string) : { value1: string, operat * @returns An object with the parsed values: value1, operator, and value2. * @throws {Error} If the condition format is invalid. */ -const parseEqualityCondition = (condition: string): { value1: string, operator: string, value2: string } => { - if(condition.includes("===") || condition.includes("!==")) { - const regex = /(.+?)\s*(===|!==)\s*(?:"(.*?)"|\$(\..+?))$/; - const match = condition.match(regex); - if (match) { - return { - value1: match[1].replace("$.",""), - operator: match[2], - value2: match[3] - }; - } else { - throw new Error("Invalid condition format, it should be just a string or string === string or string !== string"); - } +const parseEqualityCondition = ( + condition: string +): { value1: string; operator: string; value2: string } => { + if (condition.includes("===") || condition.includes("!==")) { + const regex = /(.+?)\s*(===|!==)\s*(?:"(.*?)"|\$(\..+?))$/; + const match = condition.match(regex); + if (match) { + return { + value1: match[1].replace("$.", ""), + operator: match[2], + value2: match[3], + }; + } else { + throw new Error( + "Invalid condition format, it should be just a string or string === string or string !== string" + ); + } } return { value1: condition, operator: "===", value2: "true" }; -} +}; /** * Evaluates a logical condition based on the provided values and operator. @@ -572,19 +603,27 @@ const parseEqualityCondition = (condition: string): { value1: string, operator: * @returns The result of the logical condition evaluation. * @throws {Error} If an invalid operator is provided. */ -const evaluateLogicalCondition = ({ value1, operator, value2 }: { value1: string, operator: string, value2: string }): boolean => { - const boolValue1 = value1 === 'true'; - const boolValue2 = value2 === 'true'; +const evaluateLogicalCondition = ({ + value1, + operator, + value2, +}: { + value1: string; + operator: string; + value2: string; +}): boolean => { + const boolValue1 = value1 === "true"; + const boolValue2 = value2 === "true"; switch (operator) { - case '&&': - return boolValue1 && boolValue2; - case '||': - return boolValue1 || boolValue2; - default: - throw new Error('Invalid operator'); + case "&&": + return boolValue1 && boolValue2; + case "||": + return boolValue1 || boolValue2; + default: + throw new Error("Invalid operator"); } -} +}; /** * Evaluates an entry against an item based on the provided values and operator. @@ -592,30 +631,37 @@ const evaluateLogicalCondition = ({ value1, operator, value2 }: { value1: string * @param entry - The entry containing the values and operator to evaluate. * @returns A boolean indicating whether the evaluation is true or false. */ -const evaluateEntry = (item: any, entry: { value1: string, operator: string, value2: string }): boolean => { - - if(entry.value1 === 'true' && entry.value2 === 'true') - return true; +const evaluateEntry = ( + item: any, + entry: { value1: string; operator: string; value2: string } +): boolean => { + if (entry.value1 === "true" && entry.value2 === "true") return true; const value1Result = getFieldValue(entry.value1, item); - const compareValues = (val1: any, val2: string, operator: string): boolean => { - switch (operator) { - case '===': - return val1 === val2; - case '!==': - return val1 !== val2; - default: - throw new Error(`Unsupported operator: ${operator}`); - } + const compareValues = ( + val1: any, + val2: string, + operator: string + ): boolean => { + switch (operator) { + case "===": + return val1 === val2; + case "!==": + return val1 !== val2; + default: + throw new Error(`Unsupported operator: ${operator}`); + } }; if (Array.isArray(value1Result)) { - return value1Result.some(individualValue => compareValues(individualValue, entry.value2, entry.operator)); + return value1Result.some((individualValue) => + compareValues(individualValue, entry.value2, entry.operator) + ); } else { - return compareValues(value1Result, entry.value2, entry.operator); + return compareValues(value1Result, entry.value2, entry.operator); } -} +}; /** * Evaluates a condition for a given item. @@ -624,16 +670,25 @@ const evaluateEntry = (item: any, entry: { value1: string, operator: string, val * @returns A boolean indicating whether the condition is true or false. */ const evaluateCondition = (item: any, condition: string): boolean => { - - if (condition === 'true') { - return true; + if (condition === "true") { + return true; } - + const conditionParsed = parseAggregatorCondition(condition); - const condition1 = evaluateEntry(item, parseEqualityCondition(conditionParsed.value1)); - const condition2 = evaluateEntry(item, parseEqualityCondition(conditionParsed.value2)); - return evaluateLogicalCondition({ value1: `${condition1}`, operator: conditionParsed.operator, value2: `${condition2}`}); -} + const condition1 = evaluateEntry( + item, + parseEqualityCondition(conditionParsed.value1) + ); + const condition2 = evaluateEntry( + item, + parseEqualityCondition(conditionParsed.value2) + ); + return evaluateLogicalCondition({ + value1: `${condition1}`, + operator: conditionParsed.operator, + value2: `${condition2}`, + }); +}; // We declare here a collection of all validations for every field in the form export const formFieldValidations: Record< @@ -655,7 +710,7 @@ export const validate = ( keys: string[], target: any, notify: boolean = false, - getValidations = defaultGetValidations, + getValidations = defaultGetValidations ): boolean => { // For every received key we get the validations and run them return keys.every((key) => { @@ -670,7 +725,7 @@ export const validate = ( const fieldValue = getFieldValue(fieldKey, target); const fieldEvaluation = evaluateCondition( target, - fieldCondition.replace(targetGlobalRegex, ""), + fieldCondition.replace(targetGlobalRegex, "") ); // We skip if we evaluate and the result is false @@ -683,7 +738,7 @@ export const validate = ( ( validation: (value: string) => string, notify: boolean, - eventOptions: Omit, + eventOptions: Omit ) => (value: string) => { const validationResponse = validation(value); @@ -696,7 +751,10 @@ export const validate = ( // For every validator we run it and check if the result is empty return validations .map((validation) => - validateNotifyFactory(validation, notify, { fieldName: key, fieldId: fieldKey }), + validateNotifyFactory(validation, notify, { + fieldName: key, + fieldId: fieldKey, + }) ) .every((validateNotify: (value: string) => boolean) => { // If the field value is an array we run the validation for every item in the array @@ -729,18 +787,21 @@ export const runValidation = ( const [fieldKey, fieldCondition] = key.includes("(") ? key.replace(")", "").split("(") : [key, "true"]; - + // We then load the field value const fieldValue = getFieldValue(fieldKey, target); - const fieldEvaluation = evaluateCondition(target, fieldCondition.replace(targetGlobalRegex,"")); + const fieldEvaluation = evaluateCondition( + target, + fieldCondition.replace(targetGlobalRegex, "") + ); // We skip if we evaluate and the result is false // Meaning we should not validate this field using this set of validations - if(!fieldEvaluation){ + if (!fieldEvaluation) { return true; } - const validateValue = () : string | (string | string[])[] => { + const validateValue = (): string | (string | string[])[] => { if (Array.isArray(fieldValue)) { return fieldValue.map((item: any) => { // And sometimes we can end up with another array inside, that's life @@ -751,73 +812,92 @@ export const runValidation = ( // If it is not an array here, just validate it return validation(item); }); - }else{ + } else { return validation(fieldValue); } - } + }; const validationResponse = validateValue(); - if (notify) { // Note: also notifies when valid - errorMsg will be empty. - const validationResponseMessages : string[] = []; - (Array.isArray(validationResponse) ? validationResponse.flat() : [validationResponse]) - .forEach((validationResponseMessage) => validationResponseMessages.push(validationResponseMessage)) - - getFirstErrorMessage(validationResponseMessages,exhaustive).forEach((validationResponseMessage) => { - notificationBus.emit({ fieldName: key, fieldId: fieldKey, errorMsg: validationResponseMessage }, fieldValue); - }) + const validationResponseMessages: string[] = []; + (Array.isArray(validationResponse) + ? validationResponse.flat() + : [validationResponse] + ).forEach((validationResponseMessage) => + validationResponseMessages.push(validationResponseMessage) + ); - + getFirstErrorMessage(validationResponseMessages, exhaustive).forEach( + (validationResponseMessage) => { + notificationBus.emit( + { + fieldName: key, + fieldId: fieldKey, + errorMsg: validationResponseMessage, + }, + fieldValue + ); + } + ); } // If the validation response is not empty we return false return isValidResponse(validationResponse); }; -const isValidResponse = (validationResponse: string | (string | string[])[]): boolean => { - if (typeof validationResponse === 'string') { - // Case 1: If it's a string and empty, return true - return validationResponse.trim() === ''; +const isValidResponse = ( + validationResponse: string | (string | string[])[] +): boolean => { + if (typeof validationResponse === "string") { + // Case 1: If it's a string and empty, return true + return validationResponse.trim() === ""; } else if (Array.isArray(validationResponse)) { - // Case 2: If it's an empty array, return true - if (validationResponse.length === 0) { - return true; - } + // Case 2: If it's an empty array, return true + if (validationResponse.length === 0) { + return true; + } - // Check if it's an array of empty arrays - if (validationResponse.every(val => Array.isArray(val) && val.length === 0)) { - return true; - } + // Check if it's an array of empty arrays + if ( + validationResponse.every((val) => Array.isArray(val) && val.length === 0) + ) { + return true; + } - // Check if it's an array of empty strings - if (validationResponse.every(val => typeof val === 'string' && val.trim() === '')) { - return true; - } + // Check if it's an array of empty strings + if ( + validationResponse.every( + (val) => typeof val === "string" && val.trim() === "" + ) + ) { + return true; + } - // Otherwise, it's not one of the valid cases, so return false - return false; + // Otherwise, it's not one of the valid cases, so return false + return false; } else { - // If it's neither a string nor an array, it's an invalid response - return false; + // If it's neither a string nor an array, it's an invalid response + return false; } -} - -const getFirstErrorMessage = (errors: string[],includeAll: boolean): string[] => { +}; - if(includeAll) - return errors; +const getFirstErrorMessage = ( + errors: string[], + includeAll: boolean +): string[] => { + if (includeAll) return errors; const result: string[] = []; for (const error of errors) { - result.push(error); - if (error.trim() !== '') { - break; - } + result.push(error); + if (error.trim() !== "") { + break; + } } return result; -} +}; export const isNullOrUndefinedOrBlank = ( input: string | null | undefined diff --git a/frontend/src/pages/FormBCSCPage.vue b/frontend/src/pages/FormBCSCPage.vue index b50f76f236..15e0c418d9 100644 --- a/frontend/src/pages/FormBCSCPage.vue +++ b/frontend/src/pages/FormBCSCPage.vue @@ -542,7 +542,7 @@ watch(submissionLimitError, () => { >check this map.

- { {{ progressData[0].title}} - { >check this map.

- {
- { @empty="validation.identificationType = !$event" /> - { +describe("ComboBoxInputComponent", () => { const validations = [ (value: any) => (value === "A" ? "A is not supported" : ""), ]; @@ -17,7 +17,7 @@ describe("DropdownInputComponent", () => { }; it("should render", () => { - const wrapper = mount(DropdownInputComponent, { + const wrapper = mount(ComboBoxInputComponent, { props: { id: "test", label: "test", @@ -43,7 +43,7 @@ describe("DropdownInputComponent", () => { }); it("should emit event when changing selection", async () => { - const wrapper = mount(DropdownInputComponent, { + const wrapper = mount(ComboBoxInputComponent, { props: { id: "test", label: "test", @@ -72,7 +72,7 @@ describe("DropdownInputComponent", () => { }); it("should emit empty then emit not empty", async () => { - const wrapper = mount(DropdownInputComponent, { + const wrapper = mount(ComboBoxInputComponent, { props: { id: "test", label: "test", @@ -98,7 +98,7 @@ describe("DropdownInputComponent", () => { }); it("should validate and emit error if required", async () => { - const wrapper = mount(DropdownInputComponent, { + const wrapper = mount(ComboBoxInputComponent, { props: { id: "test", label: "test", @@ -132,7 +132,7 @@ describe("DropdownInputComponent", () => { }); it("should not emit error if the change on selected value was not made by the user", async () => { - const wrapper = mount(DropdownInputComponent, { + const wrapper = mount(ComboBoxInputComponent, { props: { id: "test", label: "test", @@ -161,7 +161,7 @@ describe("DropdownInputComponent", () => { }); it("should emit error with empty payload (meaning it is valid) even if the change on selected value was not made by the user", async () => { - const wrapper = mount(DropdownInputComponent, { + const wrapper = mount(ComboBoxInputComponent, { props: { id: "test", label: "test", @@ -195,7 +195,7 @@ describe("DropdownInputComponent", () => { it.each([[""], [undefined], [null]])( "should clear the selected value when initialValue changes to a falsy value (%s)", async (value) => { - const wrapper = mount(DropdownInputComponent, { + const wrapper = mount(ComboBoxInputComponent, { props: { id: "test", label: "test", @@ -216,7 +216,7 @@ describe("DropdownInputComponent", () => { ); it("should validate and emit no error if required", async () => { - const wrapper = mount(DropdownInputComponent, { + const wrapper = mount(ComboBoxInputComponent, { props: { id: "test", label: "test", @@ -248,7 +248,7 @@ describe("DropdownInputComponent", () => { }); it("should reset selected to initial value when list change", async () => { - const wrapper = mount(DropdownInputComponent, { + const wrapper = mount(ComboBoxInputComponent, { props: { id: "test", label: "test",