Unverified Commit 41c1f9f4 authored by Zhou (Link)  Fang's avatar Zhou (Link) Fang Committed by GitHub
Browse files

Added validation logic for using steppers (#309)

* Added validation logic for using steppers

* Added a function to check if a form is empty

* Added logic to handle go back using stepper

* Fixed missing id when roll back form values in some occasions

* Added customized dialog component

* Applied new logic to back button

* Fixed reset wg data issue in some occasions

* Updated code format based on feedback

* Updated the logic based on feedback

* Made current step index into context

* Updated the wording in modal window

* Fixed reset/roll back issue on company info step
parent efc3c447
......@@ -26,6 +26,7 @@ const App = () => {
pathName: '/sign-in',
});
const [needLoadingSignIn, setNeedLoadingSignIn] = useState(true);
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const membershipContextValue = {
currentUser,
......@@ -36,6 +37,8 @@ const App = () => {
setFurthestPage,
needLoadingSignIn,
setNeedLoadingSignIn,
currentStepIndex,
setCurrentStepIndex
};
return (
......
......@@ -23,6 +23,7 @@ export const api_prefix = () => {
export const API_PREFIX_FORM = api_prefix() + '/form';
export const API_FORM_PARAM = '?sort=dateCreated&order=desc';
export const SIGN_IN = 'Sign In';
export const COMPANY_INFORMATION = 'Company Information';
export const MEMBERSHIP_LEVEL = 'Membership Level';
export const WORKING_GROUPS = 'Working Groups';
......@@ -31,7 +32,7 @@ export const REVIEW = 'Review';
export const HAS_TOKEN_EXPIRED = 'HAS_TOKEN_EXPIRED';
export const LOGIN_EXPIRED_MSG = 'Your session has expired, please sign in again.';
export const MAX_LENGTH_HELPER_TEXT = 'The value exceeds max length 255 characters'
export const MAX_LENGTH_HELPER_TEXT = 'The value exceeds max length 255 characters';
export const PATH_NAME_ARRAY = [
'/company-info',
......@@ -65,6 +66,7 @@ export const MEMBERSHIP_LEVELS = [
];
export const PAGE_STEP = [
{ label: SIGN_IN, pathName: '/sign-in' },
{ label: COMPANY_INFORMATION, pathName: '/company-info' },
{ label: MEMBERSHIP_LEVEL, pathName: '/membership-level' },
{ label: WORKING_GROUPS, pathName: '/working-groups' },
......
......@@ -18,6 +18,8 @@ const MembershipContext = React.createContext({
setFurthestPage: () => {},
needLoadingSignIn: '',
setNeedLoadingSignIn: () => {},
currentStepIndex: 0,
setCurrentStepIndex: (stepIndex) => {},
});
export default MembershipContext;
......@@ -102,9 +102,9 @@ export function mapPurchasingAndVAT(existingPurchasingAndVATData) {
// Step1: purchasing process and VAT Info
id: existingPurchasingAndVATData?.id || '',
isRegistered: !!existingPurchasingAndVATData?.registration_country,
purchasingProcess: existingPurchasingAndVATData?.purchase_order_required,
vatNumber: existingPurchasingAndVATData?.vat_number,
countryOfRegistration: existingPurchasingAndVATData?.registration_country,
purchasingProcess: existingPurchasingAndVATData?.purchase_order_required || '',
vatNumber: existingPurchasingAndVATData?.vat_number || '',
countryOfRegistration: existingPurchasingAndVATData?.registration_country || '',
};
}
......@@ -339,7 +339,7 @@ export function matchWGFieldsToBackend(eachWorkingGroupData, formId) {
* @param formId - Form Id fetched from the server, sotored in membership context, used for calling APIs
* @param userId - User Id fetched from the server when sign in, sotored in membership context, used for calling APIs
*/
export async function executeSendDataByStep(step, formData, formId, userId, setFieldValueObj) {
export async function executeSendDataByStep(step, formData, formId, userId, setFieldValueObj, updateFormValuesObj) {
switch (step) {
case 1:
callSendData(
......@@ -350,7 +350,8 @@ export async function executeSendDataByStep(step, formData, formId, userId, setF
{
fieldName: setFieldValueObj.fieldName.organization,
method: setFieldValueObj.method,
}
},
updateFormValuesObj
);
callSendData(
formId,
......@@ -360,7 +361,8 @@ export async function executeSendDataByStep(step, formData, formId, userId, setF
{
fieldName: setFieldValueObj.fieldName.member,
method: setFieldValueObj.method,
}
},
updateFormValuesObj
);
callSendData(
formId,
......@@ -370,7 +372,8 @@ export async function executeSendDataByStep(step, formData, formId, userId, setF
{
fieldName: setFieldValueObj.fieldName.marketing,
method: setFieldValueObj.method,
}
},
updateFormValuesObj
);
callSendData(
formId,
......@@ -380,14 +383,10 @@ export async function executeSendDataByStep(step, formData, formId, userId, setF
{
fieldName: setFieldValueObj.fieldName.accounting,
method: setFieldValueObj.method,
}
);
callSendData(
formId,
'',
matchMembershipLevelFieldsToBackend(formData, formId, userId),
''
},
updateFormValuesObj
);
callSendData(formId, '', matchMembershipLevelFieldsToBackend(formData, formId, userId), '');
let isWGRepSameAsCompany = false;
formData.workingGroups.map(
(wg) => (isWGRepSameAsCompany = wg.workingGroupRepresentative?.sameAsCompany || isWGRepSameAsCompany)
......@@ -401,6 +400,7 @@ export async function executeSendDataByStep(step, formData, formId, userId, setF
matchWGFieldsToBackend(item, formId),
'',
setFieldValueObj,
updateFormValuesObj,
index
);
});
......@@ -419,6 +419,7 @@ export async function executeSendDataByStep(step, formData, formId, userId, setF
matchWGFieldsToBackend(item, formId),
step,
setFieldValueObj,
updateFormValuesObj,
index
);
});
......@@ -430,7 +431,8 @@ export async function executeSendDataByStep(step, formData, formId, userId, setF
END_POINT.contacts,
matchContactFieldsToBackend(formData.signingAuthorityRepresentative, CONTACT_TYPE.SIGNING, formId),
step,
setFieldValueObj
setFieldValueObj,
updateFormValuesObj
);
break;
......@@ -457,7 +459,8 @@ function callSendData(
dataBody,
stepNum,
setFieldValueObj,
index
updateFormValuesObj,
index,
) {
const entityId = dataBody.id ? dataBody.id : '';
const method = dataBody.id ? FETCH_METHOD.PUT : FETCH_METHOD.POST;
......@@ -508,6 +511,9 @@ function callSendData(
'organization.address.id',
data[0]?.address?.id
);
updateFormValuesObj.theNewValue.organization.id = data[0]?.id;
updateFormValuesObj.theNewValue.organization.address.id = data[0]?.address?.id;
updateFormValuesObj.setUpdatedFormValues(updateFormValuesObj.theNewValue);
break;
case 'representative.member':
......@@ -515,6 +521,8 @@ function callSendData(
`${setFieldValueObj.fieldName}.id`,
data[0]?.id
);
updateFormValuesObj.theNewValue.representative.member.id = data[0]?.id;
updateFormValuesObj.setUpdatedFormValues(updateFormValuesObj.theNewValue);
break;
case 'representative.marketing':
......@@ -522,6 +530,8 @@ function callSendData(
`${setFieldValueObj.fieldName}.id`,
data[0]?.id
);
updateFormValuesObj.theNewValue.representative.marketing.id = data[0]?.id;
updateFormValuesObj.setUpdatedFormValues(updateFormValuesObj.theNewValue);
break;
case 'representative.accounting':
......@@ -529,6 +539,8 @@ function callSendData(
`${setFieldValueObj.fieldName}.id`,
data[0]?.id
);
updateFormValuesObj.theNewValue.representative.accounting.id = data[0]?.id;
updateFormValuesObj.setUpdatedFormValues(updateFormValuesObj.theNewValue);
break;
case 'workingGroups':
......@@ -540,6 +552,11 @@ function callSendData(
`workingGroups[${index}].workingGroupRepresentative.id`,
data[0]?.contact?.id
);
if (updateFormValuesObj?.theNewValue) {
updateFormValuesObj.theNewValue.workingGroups[index].id = data[0]?.id;
updateFormValuesObj.theNewValue.workingGroups[index].workingGroupRepresentative.id = data[0]?.contact?.id;
updateFormValuesObj.setUpdatedFormValues(updateFormValuesObj.theNewValue);
}
break;
case 'signingAuthorityRepresentative':
......@@ -551,6 +568,8 @@ function callSendData(
`${setFieldValueObj.fieldName}.id`,
data[0]?.id
);
updateFormValuesObj.theNewValue.signingAuthorityRepresentative.id = data[0]?.id;
updateFormValuesObj.setUpdatedFormValues(updateFormValuesObj.theNewValue);
break;
default:
......@@ -693,6 +712,51 @@ export function scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
export function isObjectEmpty(obj) {
for (const key in obj) {
// Do not need to check the value of id or allWorkingGroups, as they are not provided by users
if (key === 'id' || key === 'allWorkingGroups') {
continue;
}
const element = obj[key];
if (typeof element === 'object') {
if (!isObjectEmpty(element)) {
return false;
}
} else if (element !== '' && element !== false) {
return false;
}
}
return true;
}
export function validateGoBack(isEmpty, result, formik, setShouldOpen, navigate, isNotFurthestPage) {
// Save values on current step if it's NOT empty and passes validation
if (!isEmpty && Object.keys(result).length <= 0) {
formik.submitForm();
}
// Open modal window if it's NOT empty and fails to pass validation
// OR open it if it's emtpy and NOT the furthest page
if ((!isEmpty && Object.keys(result).length > 0) || (isEmpty && isNotFurthestPage)) {
formik.setTouched(result);
setShouldOpen(true);
return;
}
navigate();
}
export function checkIsNotFurthestPage(currentIndex, furthestIndex) {
if (currentIndex === 3) {
// For wg/3rd step, it can be empty, and clear and remove operation will update the database when user does so
// So, no need to roll back the data
return false;
}
return currentIndex < furthestIndex;
}
export const logout = () => {
fetch(`${api_prefix()}/logout`)
.then(() => {
......
......@@ -55,7 +55,7 @@ export default function Application() {
goToNextStep(5, '/submitted');
};
const submitCompanyInfo = (isUsingStepper) => {
const submitCompanyInfo = () => {
const values = formikCompanyInfo.values;
// update the organization values
const organization = values.organization;
......@@ -107,27 +107,28 @@ export default function Application() {
method: formikCompanyInfo.setFieldValue,
};
executeSendDataByStep(1, theNewValue, currentFormId, currentUser.name, setFieldValueObj);
const updateFormValuesObj = {
theNewValue,
setUpdatedFormValues,
};
executeSendDataByStep(1, theNewValue, currentFormId, currentUser.name, setFieldValueObj, updateFormValuesObj);
// Only make the API call when signingAuthorityRepresentative has an id
// If not, it means there is nothing in the db, so no need to update.
values.signingAuthorityRepresentative.id &&
executeSendDataByStep(
4,
values,
currentFormId,
currentUser.name,
setFieldValueObj
);
executeSendDataByStep(4, values, currentFormId, currentUser.name, setFieldValueObj, updateFormValuesObj);
// Only need to call goToNextStep when is not using stepper
!isUsingStepper && goToNextStep(1, '/membership-level');
};
const formikCompanyInfo = useFormik({
initialValues: initialValues,
validationSchema: validationSchema[0],
onSubmit: () => submitCompanyInfo(),
onSubmit: () => {
submitCompanyInfo();
goToNextStep(1, '/membership-level');
},
});
const submitMembershipLevel = (isUsingStepper) => {
const submitMembershipLevel = () => {
const values = formikMembershipLevel.values;
// update the membershipLevel values
const membershipLevel = values.membershipLevel;
......@@ -145,21 +146,26 @@ export default function Application() {
];
// set valueToUpdateFormik to CompanyInfo formik to make sure the value is up to date
updateCompanyInfoForm(valueToUpdateFormik);
executeSendDataByStep(2, values, currentFormId, currentUser.name);
!isUsingStepper && goToNextStep(2, '/working-groups');
};
const formikMembershipLevel = useFormik({
initialValues: initialValues,
validationSchema: validationSchema[1],
onSubmit: () => submitMembershipLevel(),
onSubmit: () => {
submitMembershipLevel();
goToNextStep(2, '/working-groups');
},
});
const submitWorkingGroups = (isUsingStepper) => {
const submitWorkingGroups = () => {
const values = formikWorkingGroups.values;
// update the workingGroups values
const workingGroups = values.workingGroups;
setUpdatedFormValues({ ...updatedFormValues, workingGroups });
const theNewValue = {
...updatedFormValues,
workingGroups: values.workingGroups,
skipJoiningWG: values.skipJoiningWG,
};
setUpdatedFormValues(theNewValue);
console.log('updated working groups: ', values);
if (!values.skipJoiningWG) {
......@@ -169,27 +175,30 @@ export default function Application() {
method: formikWorkingGroups.setFieldValue,
};
executeSendDataByStep(3, values, currentFormId, currentUser.name, setFieldValueObj);
!isUsingStepper && goToNextStep(3, '/signing-authority');
} else if (!isUsingStepper) {
// If the user is NOT using stepper and NOT joining any wg, then go to next page directly
goToNextStep(3, '/signing-authority');
executeSendDataByStep(3, values, currentFormId, currentUser.name, setFieldValueObj, {
theNewValue,
setUpdatedFormValues,
});
}
};
const formikWorkingGroups = useFormik({
initialValues: initialValues,
validationSchema: validationSchema[2],
onSubmit: () => submitWorkingGroups(),
onSubmit: () => {
submitWorkingGroups();
goToNextStep(3, '/signing-authority');
},
});
const submitSigningAuthority = (isUsingStepper) => {
const submitSigningAuthority = () => {
const values = formikSigningAuthority.values;
// update the signingAuthorityRepresentative values
const signingAuthorityRepresentative = values.signingAuthorityRepresentative;
setUpdatedFormValues({
const theNewValue = {
...updatedFormValues,
signingAuthorityRepresentative,
});
};
setUpdatedFormValues(theNewValue);
console.log('updated SigningAuthority: ', values);
const valueToUpdateFormik = [
......@@ -207,19 +216,24 @@ export default function Application() {
companyInfo: formikCompanyInfo.setFieldValue,
},
};
executeSendDataByStep(4, values, currentFormId, currentUser.name, setFieldValueObj);
!isUsingStepper && goToNextStep(4, '/review');
executeSendDataByStep(4, values, currentFormId, currentUser.name, setFieldValueObj, {
theNewValue,
setUpdatedFormValues,
});
};
const formikSigningAuthority = useFormik({
initialValues: initialValues,
validationSchema: validationSchema[3],
onSubmit: () => submitSigningAuthority(),
onSubmit: () => {
submitSigningAuthority();
goToNextStep(4, '/review');
},
});
const handleLoginExpired = useCallback(() => {
if (sessionStorage.getItem(HAS_TOKEN_EXPIRED)) {
sessionStorage.setItem(HAS_TOKEN_EXPIRED, '');
// using setTimeout here is to make the pop up message more noticeable
setTimeout(() => {
setIsLoginExpired(true);
......@@ -237,8 +251,6 @@ export default function Application() {
// generate the step options above the form
const renderStepper = () => (
<div className="stepper">
<Step title="Sign In" index={-1} pathName="/sign-in" />
{PAGE_STEP.map((pageStep, index) => {
return (
<Step
......@@ -246,10 +258,23 @@ export default function Application() {
title={pageStep.label}
index={index}
pathName={pageStep.pathName}
submitCompanyInfo={submitCompanyInfo}
submitMembershipLevel={submitMembershipLevel}
submitWorkingGroups={submitWorkingGroups}
submitSigningAuthority={submitSigningAuthority}
updatedFormValues={updatedFormValues}
formikCompanyInfo={{
...formikCompanyInfo,
submitForm: submitCompanyInfo,
}}
formikMembershipLevel={{
...formikMembershipLevel,
submitForm: submitMembershipLevel,
}}
formikWorkingGroups={{
...formikWorkingGroups,
submitForm: submitWorkingGroups,
}}
formikSigningAuthority={{
...formikSigningAuthority,
submitForm: submitSigningAuthority,
}}
/>
);
})}
......@@ -289,6 +314,8 @@ export default function Application() {
fullWorkingGroupList={fullWorkingGroupList}
setFullWorkingGroupList={setFullWorkingGroupList}
setWorkingGroupsUserJoined={setWorkingGroupsUserJoined}
updatedFormValues={updatedFormValues}
setUpdatedFormValues={setUpdatedFormValues}
/>
) : (
// if uses are not allowed to visit this page,
......@@ -301,7 +328,10 @@ export default function Application() {
<Route path="/membership-level">
{renderStepper()}
{furthestPage.index >= 2 ? (
<MembershipLevel formik={formikMembershipLevel} />
<MembershipLevel
formik={{ ...formikMembershipLevel, submitForm: submitMembershipLevel }}
updatedFormValues={updatedFormValues}
/>
) : (
<Redirect to={furthestPage.pathName} />
)}
......@@ -311,11 +341,13 @@ export default function Application() {
{renderStepper()}
{furthestPage.index >= 3 ? (
<WorkingGroupsWrapper
formik={formikWorkingGroups}
formik={{ ...formikWorkingGroups, submitForm: submitWorkingGroups }}
formikOrgValue={formikCompanyInfo.values}
isStartNewForm={isStartNewForm}
fullWorkingGroupList={fullWorkingGroupList}
workingGroupsUserJoined={workingGroupsUserJoined}
updatedFormValues={updatedFormValues}
setUpdatedFormValues={setUpdatedFormValues}
/>
) : (
<Redirect to={furthestPage.pathName} />
......@@ -325,7 +357,11 @@ export default function Application() {
<Route path="/signing-authority">
{renderStepper()}
{furthestPage.index >= 4 ? (
<SigningAuthority formik={formikSigningAuthority} formikOrgValue={formikCompanyInfo.values} />
<SigningAuthority
formik={{ ...formikSigningAuthority, submitForm: submitSigningAuthority }}
formikOrgValue={formikCompanyInfo.values}
updatedFormValues={updatedFormValues}
/>
) : (
<Redirect to={furthestPage.pathName} />
)}
......
......@@ -52,7 +52,6 @@ const useStyles = makeStyles(() => ({
}));
let hasOrgData = false;
let hasMembershipLevelData = false;
const CompanyInformation = ({
formik,
......@@ -61,8 +60,10 @@ const CompanyInformation = ({
fullWorkingGroupList,
setFullWorkingGroupList,
setWorkingGroupsUserJoined,
updatedFormValues,
setUpdatedFormValues,
}) => {
const { currentFormId } = useContext(MembershipContext); // current chosen form id
const { currentFormId, setCurrentStepIndex } = useContext(MembershipContext); // current chosen form id
const [loading, setLoading] = useState(true);
const { setFieldValue } = formik;
const setWGFieldValue = formikWG.setFieldValue;
......@@ -72,6 +73,10 @@ const CompanyInformation = ({
scrollToTop();
}, []);
useEffect(() => {
setCurrentStepIndex(1);
}, [setCurrentStepIndex]);
useEffect(() => {
const detectModeAndFetch = () => {
// Once we have API set up ready, we don't need the
......@@ -105,6 +110,9 @@ const CompanyInformation = ({
fetch(url_prefix_local + `/${currentFormId}/` + END_POINT.contacts + url_suffix_local, {
headers: FETCH_HEADER,
}),
fetch(url_prefix_local + `/${currentFormId}` + url_suffix_local, {
headers: FETCH_HEADER,
}),
];
Promise.all(pool)
.then((res) =>
......@@ -117,7 +125,8 @@ const CompanyInformation = ({
})
)
)
.then(([organizations, contacts]) => {
.then(([organizations, contacts, membershipLevel]) => {
let newFormData = { ...updatedFormValues };
// Matching the field data
if (organizations[0]) {
// the organization data returned is always an
......@@ -130,7 +139,7 @@ const CompanyInformation = ({
// if nested, it will automatically map the
// properties and values
setFieldValue('organization', tempOrg);
hasOrgData = true;
newFormData = { ...newFormData, organization: tempOrg };
}
if (contacts.length) {
......@@ -142,52 +151,25 @@ const CompanyInformation = ({
// to set representative field with the mapped data,
// if nested, it will automatically map the properties and values
setFieldValue('representative', tempContacts.organizationContacts);
setFieldValue('signingAuthorityRepresentative', tempContacts.signingAuthorityRepresentative);
hasOrgData = true;
}
setLoading(false);
})
.catch((err) => console.log(err));
};
const detectModeAndFetchMembershipLevel = () => {
let url_prefix_local;
let url_suffix_local = '';
if (getCurrentMode() === MODE_REACT_ONLY) {
url_prefix_local = 'membership_data';
url_suffix_local = '/form.json';
}
if (getCurrentMode() === MODE_REACT_API) {
url_prefix_local = API_PREFIX_FORM;
newFormData = { ...newFormData, representative: tempContacts.organizationContacts };
}
fetch(url_prefix_local + `/${currentFormId}` + url_suffix_local, {
headers: FETCH_HEADER,
})
.then((res) => {
if (res.ok) return res.json();
requestErrorHandler(res.status);
throw res.status;
})
.then((data) => {
if (data) {
if (membershipLevel) {
// setFieldValue(): Prefill Data --> Call the setFieldValue of
// Formik, to set membershipLevel field with the mapped data
setFieldValue('membershipLevel', data.membership_level);
setFieldValue('membershipLevel', membershipLevel.membership_level);
const tempPurchasingAndVAT = mapPurchasingAndVAT(data);