diff --git a/package-lock.json b/package-lock.json index 64c0e0c..be02afb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,10 +13,7 @@ "src/@episerver/forms-sdk", "samples/sample-react-app", "samples/managementsite" - ], - "dependencies": { - "react-router": "^6.21.0" - } + ] }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", @@ -3461,14 +3458,6 @@ } } }, - "node_modules/@remix-run/router": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.14.0.tgz", - "integrity": "sha512-WOHih+ClN7N8oHk9N4JUiMxQJmRVaOxcg8w7F/oHUXzJt920ekASLI/7cYX8XkntDWRhLZtsk6LbGrkgOAvi5A==", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@rollup/plugin-commonjs": { "version": "25.0.4", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.4.tgz", @@ -15711,20 +15700,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-router": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.0.tgz", - "integrity": "sha512-hGZ0HXbwz3zw52pLZV3j3+ec+m/PQ9cTpBvqjFQmy2XVUWGn5MD+31oXHb6dVTxYzmAeaiUBYjkoNz66n3RGCg==", - "dependencies": { - "@remix-run/router": "1.14.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, "node_modules/react-router-dom": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", @@ -19681,9 +19656,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@episerver/forms-sdk": "file:../forms-sdk", - "@types/react-router-dom": "^5.3.3", - "react-router-dom": "^5.3.4" + "@episerver/forms-sdk": "file:../forms-sdk" }, "devDependencies": { "@rollup/plugin-commonjs": "^25.0.4", @@ -22617,10 +22590,8 @@ "@rollup/plugin-typescript": "^11.1.3", "@types/react": "^18.2.24", "@types/react-dom": "^18.2.8", - "@types/react-router-dom": "^5.3.3", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "5.3.4", "rollup": "^3.29.1", "rollup-plugin-dts": "^6.0.2", "rollup-plugin-postcss": "^4.0.2", @@ -24394,11 +24365,6 @@ "source-map": "^0.7.3" } }, - "@remix-run/router": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.14.0.tgz", - "integrity": "sha512-WOHih+ClN7N8oHk9N4JUiMxQJmRVaOxcg8w7F/oHUXzJt920ekASLI/7cYX8XkntDWRhLZtsk6LbGrkgOAvi5A==" - }, "@rollup/plugin-commonjs": { "version": "25.0.4", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.4.tgz", @@ -33076,14 +33042,6 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==" }, - "react-router": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.0.tgz", - "integrity": "sha512-hGZ0HXbwz3zw52pLZV3j3+ec+m/PQ9cTpBvqjFQmy2XVUWGn5MD+31oXHb6dVTxYzmAeaiUBYjkoNz66n3RGCg==", - "requires": { - "@remix-run/router": "1.14.0" - } - }, "react-router-dom": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", diff --git a/samples/ManagementSite/Alloy.ManagementSite.csproj b/samples/ManagementSite/Alloy.ManagementSite.csproj index 13825f6..dffa3ef 100644 --- a/samples/ManagementSite/Alloy.ManagementSite.csproj +++ b/samples/ManagementSite/Alloy.ManagementSite.csproj @@ -19,9 +19,9 @@ - + - + diff --git a/src/@episerver/forms-react/src/components/FormBody.tsx b/src/@episerver/forms-react/src/components/FormBody.tsx index 9f10fd6..8c5740d 100644 --- a/src/@episerver/forms-react/src/components/FormBody.tsx +++ b/src/@episerver/forms-react/src/components/FormBody.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef } from "react"; import { useForms, useFormsDispatch } from "../context/store"; -import { FormContainer, FormSubmitter, IdentityInfo, SubmitButtonType, equals, isInArray, isNull, isNullOrEmpty, FormSubmitModel, FormSubmitResult, SubmitButton } from "@episerver/forms-sdk"; +import { FormContainer, FormSubmitter, IdentityInfo, SubmitButtonType, equals, isInArray, isNull, isNullOrEmpty, FormSubmitModel, FormSubmitResult, SubmitButton, ElementValidationResult, FormValidationResult } from "@episerver/forms-sdk"; import { RenderElementInStep } from "./RenderElementInStep"; import { DispatchFunctions } from "../context/dispatchFunctions"; import { FormStepNavigation } from "./FormStepNavigation"; @@ -15,13 +15,11 @@ export const FormBody = (props: FormBodyProps) => { const formContext = useForms(); const form = formContext?.formContainer ?? {} as FormContainer; const formSubmitter = new FormSubmitter(formContext?.formContainer ?? {} as FormContainer, props.baseUrl); - const dispatch = useFormsDispatch(); - const dispatchFunctions = new DispatchFunctions(dispatch); + const dispatchFunctions = new DispatchFunctions(); const formTitleId = `${form.key}_label`; const statusMessage = useRef(""); const statusDisplay = useRef("hide"); - const stepLocalizations = useRef>(form.steps?.filter(s => !isNull(s.formStep.localizations))[0]?.formStep.localizations); //TODO: these variables should be get from api or sdk const validateFail = useRef(false), @@ -49,6 +47,21 @@ export const FormBody = (props: FormBodyProps) => { const validationCssClass = validateFail.current ? "ValidationFail" : "ValidationSuccess"; + const isInCurrentStep = (elementKey: string): boolean => { + let currentStep = form.steps[currentStepIndex]; + if(currentStep){ + return currentStep.elements.some(e => equals(e.key, elementKey)); + } + return true; + } + + const getFirstInvalidElement = (formValidationResults: FormValidationResult[]): string => { + return formValidationResults.filter(fv => + fv.results.some(r => !r.valid) && + form.steps[currentStepIndex]?.elements?.some(e => equals(e.key, fv.elementKey)) + )[0]?.elementKey; + } + const handleSubmit = (e: any) => { e.preventDefault(); @@ -58,35 +71,31 @@ export const FormBody = (props: FormBodyProps) => { //Find submit button, if found then check property 'finalizeForm' of submit button. Otherwise, button Next/Previous was clicked. let buttonId = e.nativeEvent.submitter.id; - let submitButton = form.formElements.filter(fe => fe.key === buttonId)[0] as SubmitButton; + let submitButton = form.formElements.find(fe => fe.key === buttonId) as SubmitButton; if (!isNull(submitButton)) { //when submitting by SubmitButton, isProgressiveSubmit default is true isProgressiveSubmit.current = true; } - //remove submissions of inactive elements and submissions with undefined value + //filter submissions by active elements and current step let formSubmissions = (formContext?.formSubmissions ?? []) - //only post value of active elements - .filter(fs => !isInArray(fs.elementKey, formContext?.dependencyInactiveElements ?? []) && !isNull(fs.value)); + .filter(fs => !isInArray(fs.elementKey, formContext?.dependencyInactiveElements ?? []) && isInCurrentStep(fs.elementKey)); //validate all submission data before submit let formValidationResults = formSubmitter.doValidate(formSubmissions); dispatchFunctions.updateAllValidation(formValidationResults); //set focus on the 1st invalid element of current step - let invalid = formValidationResults.filter(fv => - fv.results.some(r => !r.valid) && - form.steps[currentStepIndex]?.elements?.some(e => equals(e.key, fv.elementKey)) - )[0]?.elementKey; + let invalid = getFirstInvalidElement(formValidationResults); if(!isNullOrEmpty(invalid)){ dispatchFunctions.updateFocusOn(invalid); - isFormFinalized.current = false; return; } + let isLastStep = formContext?.currentStepIndex === form.steps.length - 1; let model: FormSubmitModel = { formKey: form.key, locale: form.locale, - isFinalized: submitButton?.properties?.finalizeForm || formContext?.currentStepIndex === form.steps.length - 1, + isFinalized: submitButton?.properties?.finalizeForm || isLastStep, partialSubmissionKey: formContext?.submissionKey ?? "", hostedPageUrl: window.location.pathname, submissionData: formSubmissions, @@ -95,6 +104,7 @@ export const FormBody = (props: FormBodyProps) => { dispatchFunctions.updateIsSubmitting(true); formSubmitter.doSubmit(model).then((response: FormSubmitResult)=>{ + //get error or success message if(response.success){ message.current = response.messages.map(m => m.message).join("
"); } @@ -103,9 +113,27 @@ export const FormBody = (props: FormBodyProps) => { //ignore validation message message.current = response.messages.filter(m => isNullOrEmpty(m.identifier)).map(m => m.message).join("
"); } + //update validation message + if(response.validationFail){ + let formValidationResults = formContext?.formValidationResults?.map(fr => { + let errorMessages = response.messages.filter(m => equals(m.identifier, fr.elementKey)); + return errorMessages.length === 0 ? fr : { + ...fr, + results: fr.results.map(er => !errorMessages.some(em => equals(em.section, er.type)) ? er : { + ...er, + valid: false + }) + } as FormValidationResult + }) ?? []; + + dispatchFunctions.updateAllValidation(formValidationResults); + + //set focus on the 1st invalid element of current step + dispatchFunctions.updateFocusOn(getFirstInvalidElement(formValidationResults)); + } validateFail.current = response.validationFail; isSuccess.current = response.success; - isFormFinalized.current = formContext?.currentStepIndex === form.steps.length - 1 && response.success; + isFormFinalized.current = isLastStep && response.success; dispatchFunctions.updateSubmissionKey(response.submissionKey); dispatchFunctions.updateIsSubmitting(false); }); @@ -172,9 +200,7 @@ export const FormBody = (props: FormBodyProps) => { {/* render step navigation*/} diff --git a/src/@episerver/forms-react/src/components/FormStepNavigation.tsx b/src/@episerver/forms-react/src/components/FormStepNavigation.tsx index 1f4bcb0..5b69c41 100644 --- a/src/@episerver/forms-react/src/components/FormStepNavigation.tsx +++ b/src/@episerver/forms-react/src/components/FormStepNavigation.tsx @@ -1,24 +1,22 @@ -import React from "react"; +import React, { useRef } from "react"; import { useForms, useFormsDispatch } from "../context/store"; import { FormCache, FormConstants, FormContainer, FormStep, StepDependCondition, SubmitButtonType, isNull } from "@episerver/forms-sdk"; import { DispatchFunctions } from "../context/dispatchFunctions"; import { useHistory } from "react-router-dom"; interface FormStepNavigationProps { - stepLocalizations: React.MutableRefObject> - form: FormContainer - isFormFinalized: React.MutableRefObject + isFormFinalized: boolean history?: any } export const FormStepNavigation = (props: FormStepNavigationProps) => { - const formContext = useForms() - const formCache = new FormCache() - const dispatch = useFormsDispatch() - const history = props.history - const depend = new StepDependCondition(props.form, formContext?.dependencyInactiveElements ?? []) - const { stepLocalizations, form, isFormFinalized } = props; - const dispatchFuncs = new DispatchFunctions(dispatch); + const formContext = useForms(); + const formCache = new FormCache(); + const form = formContext?.formContainer ?? {} as FormContainer; + const depend = new StepDependCondition(form, formContext?.dependencyInactiveElements ?? []); + const { isFormFinalized, history } = props; + const dispatchFuncs = new DispatchFunctions(); + const stepLocalizations = useRef>(form.steps?.filter(s => !isNull(s.formStep.localizations))[0]?.formStep.localizations); const submittable = true const stepCount = form.steps.length; @@ -29,7 +27,7 @@ export const FormStepNavigation = (props: FormStepNavigationProps) => { const nextButtonDisableState = (currentStepIndex == stepCount - 1) || !submittable; const progressWidth = (100 * currentDisplayStepIndex / stepCount) + "%"; - const isShowStepNavigation = stepCount > 1 && currentStepIndex > -1 && currentStepIndex < stepCount && !isFormFinalized.current; + const isShowStepNavigation = stepCount > 1 && currentStepIndex > -1 && currentStepIndex < stepCount && !isFormFinalized; const handlePrevStep = (event: React.MouseEvent) => { event.preventDefault() @@ -44,7 +42,7 @@ export const FormStepNavigation = (props: FormStepNavigationProps) => { const goToStep = (stepIndex: number) => { var step = form.steps[stepIndex].formStep as FormStep - formCache.set(FormConstants.FormCurrentStep + props.form.key, stepIndex) + formCache.set(FormConstants.FormCurrentStep + form.key, stepIndex) dispatchFuncs.updateCurrentStepIndex(stepIndex) if (!isNull(step) && !isNull(step.properties.attachedContentLink)) { diff --git a/src/@episerver/forms-react/src/context/dispatchFunctions.ts b/src/@episerver/forms-react/src/context/dispatchFunctions.ts index 26f8bf6..a778193 100644 --- a/src/@episerver/forms-react/src/context/dispatchFunctions.ts +++ b/src/@episerver/forms-react/src/context/dispatchFunctions.ts @@ -1,10 +1,11 @@ import { FormContainer, ElementValidationResult, FormValidationResult, initFormState, IdentityInfo } from "@episerver/forms-sdk"; import { ActionType } from "./reducer"; +import { useFormsDispatch } from "./store"; export class DispatchFunctions { readonly _dispatch: any - constructor(dispatch: any){ - this._dispatch = dispatch; + constructor(){ + this._dispatch = useFormsDispatch(); } updateValidation = (elementKey: string, validationResults: ElementValidationResult[]) => { diff --git a/src/@episerver/forms-react/src/hooks/useElement.ts b/src/@episerver/forms-react/src/hooks/useElement.ts index 3b93425..87e9666 100644 --- a/src/@episerver/forms-react/src/hooks/useElement.ts +++ b/src/@episerver/forms-react/src/hooks/useElement.ts @@ -35,13 +35,12 @@ export interface ElementContext { export const useElement = (element: FormElementBase) => { const formContext = useForms(); - const dispatch = useFormsDispatch(); const extraAttr = useRef({}); const formValidation = new FormValidator(element); const formCondition = new FormDependConditions(element) const defaultValue = getDefaultValue(element); const isVisible = useRef(true); - const dispatchFuncs = new DispatchFunctions(dispatch); + const dispatchFuncs = new DispatchFunctions(); const elementRef = useRef(null); //build element state diff --git a/src/@episerver/forms-sdk/src/form-submit/formSubmit.ts b/src/@episerver/forms-sdk/src/form-submit/formSubmit.ts index 967c487..732a99f 100644 --- a/src/@episerver/forms-sdk/src/form-submit/formSubmit.ts +++ b/src/@episerver/forms-sdk/src/form-submit/formSubmit.ts @@ -1,6 +1,6 @@ import { FormStorage } from "../form-storage"; import { FormValidator } from "../form-validator"; -import { equals } from "../helpers"; +import { equals, isNull } from "../helpers"; import { FormConstants, FormContainer, FormSubmission, FormValidationResult } from "../models"; import { ApiConstant } from "../form-loader/apiConstant"; @@ -104,6 +104,11 @@ export class FormSubmitter { model.submissionData.forEach(data => { let ovalue = data.value; let key = `${FormConstants.FormFieldPrefix}${data.elementKey}`; + + if(isNull(ovalue)) { + return; + } + // checking file upload elements, item must be File if any, // for using Object.getPrototypeOf(variable) variable must be object type if(Object.getPrototypeOf(ovalue) === FileList.prototype && (ovalue as FileList).length > 0) { diff --git a/src/@episerver/forms-sdk/src/helpers/initFormState.ts b/src/@episerver/forms-sdk/src/helpers/initFormState.ts index 0338cbd..538d82a 100644 --- a/src/@episerver/forms-sdk/src/helpers/initFormState.ts +++ b/src/@episerver/forms-sdk/src/helpers/initFormState.ts @@ -2,7 +2,7 @@ import { FormCache } from "../form-cache"; import { FormStorage } from "../form-storage"; import { ElementValidationResult, FormConstants, FormContainer, FormState, FormSubmission, FormValidationResult, StepDependencies, ValidatableElementBaseProperties } from "../models"; import { getDefaultValue } from "./elementHelper"; -import { isNull } from "./utils"; +import { equals, isNull } from "./utils"; /** * Function to initialize FormState object @@ -39,9 +39,12 @@ export function initFormState(formContainer: FormContainer): FormState { stepDependencies = stepDependencies.concat({ elementKey: s.formStep.key, isSatisfied: false }); }); - //binding the elements with stored input value between Next/Prev navigation - if (formData.length > 0) { - formSubmissions = formData; + //binding the elements with saved data between Next/Prev navigation + if(formData.length > 0){ + formSubmissions = formSubmissions.map(s => { + let savedData = formData.find(d => equals(d.elementKey, s.elementKey)); + return savedData ? savedData : s; + }); } return {