Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix form multi steps is working incorrectly #57

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions samples/ManagementSite/Controllers/ReactController.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using EPiServer.Cms.Shell;
using EPiServer.Web;
using EPiServer.Web.Routing;
using Microsoft.AspNetCore.Mvc;
Expand Down Expand Up @@ -34,6 +35,8 @@ public async Task<IActionResult> GetFormInPageByUrl(string url)
var contentHeadless = await _contentRepositoryInteApi.GetAsync(new ContentReference(key), new GetContentOptions());

pageModel.Title = contentHeadless.DisplayName;
pageModel.PageUrl = UrlResolver.Current.GetUrl(content.ContentLink);

if (contentHeadless.Properties.ContainsKey("MainContentArea"))
{
pageModel.Childrens.AddRange(contentHeadless.Properties["MainContentArea"] as IList<IContentComponent>);
Expand All @@ -46,5 +49,6 @@ public async Task<IActionResult> GetFormInPageByUrl(string url)
public class PageModel
{
public string Title { get; set; }
public string PageUrl { get; set; }
public List<IContentComponent> Childrens { get; set; } = new List<IContentComponent>();
}
1 change: 1 addition & 0 deletions samples/sample-react-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ function App() {
baseUrl={process.env.REACT_APP_HEADLESS_FORM_BASE_URL ?? "/"}
identityInfo={identityInfo}
history={history}
currentPageUrl={pageData.pageUrl}
/>
))}
</div>
Expand Down
22 changes: 14 additions & 8 deletions src/@episerver/forms-react/src/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,35 @@ interface FormProps {
/**
* The form key that identifies the form
*/
formKey: string,
formKey: string;
/**
* The code of the form language
*/
language?: string,
language?: string;
/**
* The base url of Headless Form API
*/
baseUrl: string,
baseUrl: string;
/**
* Access token for form submit
*/
identityInfo?: IdentityInfo

history? : any
identityInfo?: IdentityInfo;
/**
* The instance of useHistory() received from react-router-dom
*/
history?: any;
/**
* The public url of current page
*/
currentPageUrl?: string;
}

export const Form = ({formKey, language, baseUrl, identityInfo, history}: FormProps) => {
export const Form = ({formKey, language, baseUrl, identityInfo, history, currentPageUrl}: FormProps) => {
const {data: formData } = useFormLoader({ formKey, language, baseUrl } as UseFormLoaderProps)

return (
<>
{formData && <FormContainerBlock form={formData} key={formData.key} identityInfo={identityInfo} baseUrl={baseUrl} history={history}/>}
{formData && <FormContainerBlock form={formData} key={formData.key} identityInfo={identityInfo} baseUrl={baseUrl} history={history} currentPageUrl={currentPageUrl}/>}
</>
);
}
89 changes: 47 additions & 42 deletions src/@episerver/forms-react/src/components/FormBody.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,55 @@
import React, { useEffect, useRef } from "react";
import React, { useEffect, useMemo, useRef } from "react";
import { useForms } from "../context/store";
import { FormContainer, FormSubmitter, IdentityInfo, equals, isInArray, isNull, isNullOrEmpty, FormSubmitModel, FormSubmitResult, SubmitButton, FormValidationResult, FormCache, FormConstants, ProblemDetail } from "@episerver/forms-sdk";
import { FormContainer, FormSubmitter, IdentityInfo, equals, isInArray, isNull, isNullOrEmpty, FormSubmitModel, FormSubmitResult, SubmitButton, FormValidationResult, FormCache, FormConstants, ProblemDetail, StepDependCondition } from "@episerver/forms-sdk";
import { RenderElementInStep } from "./RenderElementInStep";
import { DispatchFunctions } from "../context/dispatchFunctions";
import { FormStepNavigation } from "./FormStepNavigation";
import { StepHelper } from "@episerver/forms-sdk/dist/form-step/stepHelper";

interface FormBodyProps {
identityInfo?: IdentityInfo;
baseUrl: string;
history?: any
history?: any;
currentPageUrl?: string;
}

export const FormBody = (props: FormBodyProps) => {
const formContext = useForms();
const form = formContext?.formContainer ?? {} as FormContainer;
const inactiveElements = formContext?.dependencyInactiveElements ?? [];
const formSubmitter = new FormSubmitter(formContext?.formContainer ?? {} as FormContainer, props.baseUrl);
const dispatchFunctions = new DispatchFunctions();
const stepDependCondition = new StepDependCondition(form, inactiveElements);
const stepHelper = new StepHelper(form);
const currentPageUrl = props.currentPageUrl ?? window.location.href;

const formTitleId = `${form.key}_label`;
const statusMessage = useRef<string>("");
const statusDisplay = useRef<string>("hide");

const formCache = new FormCache();
const localFormCache = new FormCache(window.localStorage);
const currentStepIndex = formContext?.currentStepIndex ?? 0;

//TODO: these variables should be get from api or sdk
const validateFail = useRef<boolean>(false),
isFormFinalized = useRef<boolean>(false),
isProgressiveSubmit = useRef<boolean>(false),
isSuccess = useRef<boolean>(false),
submittable = true,
submissionWarning = useRef<boolean>(false),
message = useRef<string>(""),
isReadOnlyMode = false,
readOnlyModeMessage = "",
currentStepIndex = formContext?.currentStepIndex ?? 0,
submissionStorageKey = FormConstants.FormSubmissionId + form.key,
isStepValidToDisplay = true;

isStepValidToDisplay = stepDependCondition.isStepValidToDisplay(currentStepIndex, currentPageUrl),
isMalFormSteps = stepHelper.isMalFormSteps();

if((isFormFinalized.current || isProgressiveSubmit.current) && isSuccess.current)
{
statusDisplay.current = "Form__Success__Message";
statusMessage.current = form.properties.submitSuccessMessage ?? message.current;
}
else if ((submissionWarning.current || (!submittable && !isSuccess.current))
else if ((submissionWarning.current || !isSuccess.current)
&& !isNullOrEmpty(message.current)) {
statusDisplay.current = "Form__Warning__Message";
statusMessage.current = message.current;
Expand All @@ -55,21 +61,6 @@ 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 showError = (error: string) => {
submissionWarning.current = !isNullOrEmpty(error);
message.current = error;
Expand All @@ -91,38 +82,36 @@ export const FormBody = (props: FormBodyProps) => {
}
//filter submissions by active elements and current step
let formSubmissions = (formContext?.formSubmissions ?? [])
.filter(fs => !isInArray(fs.elementKey, formContext?.dependencyInactiveElements ?? []) && isInCurrentStep(fs.elementKey));
.filter(fs => !isInArray(fs.elementKey, formContext?.dependencyInactiveElements ?? []) && stepHelper.isInCurrentStep(fs.elementKey, currentStepIndex));

//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 = getFirstInvalidElement(formValidationResults);
let invalid = stepHelper.getFirstInvalidElement(formValidationResults, currentStepIndex);
if(!isNullOrEmpty(invalid)){
dispatchFunctions.updateFocusOn(invalid);
return;
}

let isLastStep = formContext?.currentStepIndex === form.steps.length - 1;
let isLastStep = currentStepIndex === form.steps.length - 1;
let model: FormSubmitModel = {
formKey: form.key,
locale: form.locale,
isFinalized: submitButton?.properties?.finalizeForm || isLastStep,
partialSubmissionKey: localFormCache.get(submissionStorageKey) ?? formContext?.submissionKey ?? "",
hostedPageUrl: window.location.pathname,
hostedPageUrl: currentPageUrl,
submissionData: formSubmissions,
accessToken: formContext?.identityInfo?.accessToken,
currentStepIndex: currentStepIndex
}

//submit data to API
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("<br>");
}
else {
if(!response.success) {
//ignore validation message
showError(response.messages.filter(m => isNullOrEmpty(m.identifier)).map(m => m.message).join("<br>"));
}
Expand All @@ -142,27 +131,33 @@ export const FormBody = (props: FormBodyProps) => {
dispatchFunctions.updateAllValidation(formValidationResults);

//set focus on the 1st invalid element of current step
dispatchFunctions.updateFocusOn(getFirstInvalidElement(formValidationResults));
dispatchFunctions.updateFocusOn(stepHelper.getFirstInvalidElement(formValidationResults, currentStepIndex));
}

validateFail.current = response.validationFail;
isSuccess.current = response.success;
isFormFinalized.current = isLastStep && response.success;
dispatchFunctions.updateSubmissionKey(response.submissionKey);
localFormCache.set(submissionStorageKey, response.submissionKey)
localFormCache.set(submissionStorageKey, response.submissionKey);

if (isFormFinalized.current) {
formCache.remove(submissionStorageKey)
localFormCache.remove(submissionStorageKey)
formCache.remove(FormConstants.FormCurrentStep + form.key)
formCache.remove(FormConstants.FormCurrentStep + form.key);
localFormCache.remove(submissionStorageKey);
message.current = response.messages.map(m => m.message).join("<br>");
}
}).catch((e: ProblemDetail) => {
if(e.status === 401) {
//clear access token to ask login again
dispatchFunctions.updateIdentity({} as IdentityInfo);
formCache.remove(FormConstants.FormAccessToken);
showError(e.detail);
switch(e.status){
case 401:
//clear access token to ask login again
dispatchFunctions.updateIdentity({} as IdentityInfo);
formCache.remove(FormConstants.FormAccessToken);
break;
case 400:
//validate fail
break;
}

showError(e.detail);
}).finally(()=>{
dispatchFunctions.updateIsSubmitting(false);
});
Expand All @@ -178,6 +173,13 @@ export const FormBody = (props: FormBodyProps) => {
}
}, [props.identityInfo?.accessToken]);

//reset when change page
useEffect(()=>{
isSuccess.current = false;
},[currentStepIndex]);

isMalFormSteps && showError("Improperly formed FormStep configuration. Some steps are attached to pages, while some steps are not attached, or attached to content with no public URL.");

return (
<form method="post"
noValidate={true}
Expand Down Expand Up @@ -217,7 +219,7 @@ export const FormBody = (props: FormBodyProps) => {
<div className="Form__MainBody">
{/* render element */}
{form.steps.map((e, i) => {
let stepDisplaying = (currentStepIndex === i && !isFormFinalized.current && isStepValidToDisplay) ? "" : "hide";
let stepDisplaying = (currentStepIndex === i && !isFormFinalized.current && isStepValidToDisplay && !isMalFormSteps) ? "" : "hide";
return (
<section key={e.formStep.key} id={e.formStep.key} className={`Form__Element__Step ${stepDisplaying}`}>
<RenderElementInStep elements={e.elements} stepIndex={i} />
Expand All @@ -230,6 +232,9 @@ export const FormBody = (props: FormBodyProps) => {
isFormFinalized={isFormFinalized.current}
history = {props.history}
handleSubmit = {handleSubmit}
isMalFormSteps = {isMalFormSteps}
isStepValidToDisplay = {isStepValidToDisplay}
isSuccess = {isSuccess.current}
/>
</div>
</form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@ export interface FormContainerProps {
identityInfo?: IdentityInfo;
baseUrl: string;
history?: any;
currentPageUrl?: string;
}

export function FormContainerBlock(props: FormContainerProps){
const stepBuilder = new StepBuilder(props.form);
const form = stepBuilder.buildForm();
const state = initFormState(form);
const state = initFormState(form, props.currentPageUrl);

{/* finally return the form */}
return (
<FormProvider initialState={state}>
<FormBody identityInfo={props.identityInfo} baseUrl={props.baseUrl} history={props.history}/>
<FormBody identityInfo={props.identityInfo} baseUrl={props.baseUrl} history={props.history} currentPageUrl={props.currentPageUrl}/>
</FormProvider>
)
}
44 changes: 27 additions & 17 deletions src/@episerver/forms-react/src/components/FormStepNavigation.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,67 @@
import React, { useRef } from "react";
import React, { useEffect, useRef } from "react";
import { useForms } from "../context/store";
import { FormCache, FormConstants, FormContainer, FormStep, StepDependCondition, SubmitButtonType, isNull } from "@episerver/forms-sdk";
import { FormCache, FormConstants, FormContainer, StepDependCondition, SubmitButtonType, isNull, isNullOrEmpty } from "@episerver/forms-sdk";
import { DispatchFunctions } from "../context/dispatchFunctions";

interface FormStepNavigationProps {
isFormFinalized: boolean
history?: any
handleSubmit: (e: any) => void
isFormFinalized: boolean;
history?: any;
handleSubmit: (e: any) => void;
isMalFormSteps: boolean;
isStepValidToDisplay: boolean;
isSuccess: boolean;
}

export const FormStepNavigation = (props: FormStepNavigationProps) => {
const formContext = useForms();
const formCache = new FormCache();
const form = formContext?.formContainer ?? {} as FormContainer;
const depend = new StepDependCondition(form, formContext?.dependencyInactiveElements ?? []);
const { isFormFinalized, history, handleSubmit } = props;
const { isFormFinalized, history, handleSubmit, isMalFormSteps, isStepValidToDisplay, isSuccess } = props;
const dispatchFuncs = new DispatchFunctions();
const stepLocalizations = useRef<Record<string, string>>(form.steps?.filter(s => !isNull(s.formStep.localizations))[0]?.formStep.localizations).current;
const isNextStep = useRef<boolean>(false);

const submittable = true
const stepCount = form.steps.length;

const currentStepIndex = formContext?.currentStepIndex ?? 0
const currentStepIndex = formContext?.currentStepIndex ?? 0;
const currentDisplayStepIndex = currentStepIndex + 1;
const prevButtonDisableState = (currentStepIndex == 0) || !submittable;
const nextButtonDisableState = (currentStepIndex == stepCount - 1) || !submittable;
const progressWidth = (100 * currentDisplayStepIndex / stepCount) + "%";

const isShowStepNavigation = stepCount > 1 && currentStepIndex > -1 && currentStepIndex < stepCount && !isFormFinalized;
const isShowStepNavigation = stepCount > 1 && currentStepIndex > -1 && currentStepIndex < stepCount && !isFormFinalized && !isMalFormSteps && isStepValidToDisplay;

const handlePrevStep = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault()
goToStep(depend.findPreviousStep(currentStepIndex) ?? 0)
}

const handleNextStep = (event: React.MouseEvent<HTMLButtonElement>) => {
const handleNextStep = async (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault()
handleSubmit(event)
goToStep(depend.findNextStep(currentStepIndex) ?? 0)
handleSubmit(event);
isNextStep.current = true;
}

const goToStep = (stepIndex: number) => {
var step = form.steps[stepIndex].formStep as FormStep
var attachedContentLink = form.steps[stepIndex]?.formStep?.properties?.attachedContentLink;

formCache.set<number>(FormConstants.FormCurrentStep + form.key, stepIndex)
dispatchFuncs.updateCurrentStepIndex(stepIndex)

if (!isNull(step) && !isNull(step.properties.attachedContentLink)) {
let url = new URL(step.properties.attachedContentLink)
formCache.set<number>(FormConstants.FormCurrentStep + form.key, stepIndex);
dispatchFuncs.updateCurrentStepIndex(stepIndex);
if (!isNullOrEmpty(attachedContentLink)) {
let url = new URL(attachedContentLink);
history && history.push(url.pathname);
}
}

useEffect(()=>{
if(isSuccess && isNextStep.current) {
goToStep(depend.findNextStep(currentStepIndex) ?? 0);
isNextStep.current = false;
}
},[isSuccess]);

return (
<>
{isShowStepNavigation &&
Expand Down
3 changes: 2 additions & 1 deletion src/@episerver/forms-sdk/src/form-step/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./stepBuilder";
export * from "./stepDependCondition";
export * from "./stepDependCondition";
export * from "./stepHelper";
Loading