Skip to content

Commit

Permalink
Fix form multi steps is working incorrectly
Browse files Browse the repository at this point in the history
Fixes: AFORM-3801
  • Loading branch information
hungoptimizely committed Dec 20, 2023
1 parent 419f4f1 commit 6883380
Show file tree
Hide file tree
Showing 14 changed files with 232 additions and 76 deletions.
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}/>}
</>
);
}
61 changes: 29 additions & 32 deletions src/@episerver/forms-react/src/components/FormBody.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,57 @@
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.pathname;

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 = useMemo(()=>{
return stepHelper.getCurrentStepIndex(currentPageUrl);
},[currentPageUrl]);

//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 +63,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,26 +84,26 @@ 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
Expand Down Expand Up @@ -142,18 +135,17 @@ 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)
localFormCache.remove(submissionStorageKey);
}
}).catch((e: ProblemDetail) => {
if(e.status === 401) {
Expand All @@ -177,6 +169,8 @@ export const FormBody = (props: FormBodyProps) => {
}
}, [props.identityInfo?.accessToken]);

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 @@ -216,7 +210,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 @@ -229,6 +223,9 @@ export const FormBody = (props: FormBodyProps) => {
isFormFinalized={isFormFinalized.current}
history = {props.history}
handleSubmit = {handleSubmit}
isMalFormSteps = {isMalFormSteps}
isStepValidToDisplay = {isStepValidToDisplay}
currentStepIndex={currentStepIndex}
/>
</div>
</form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface FormContainerProps {
identityInfo?: IdentityInfo;
baseUrl: string;
history?: any;
currentPageUrl?: string;
}

export function FormContainerBlock(props: FormContainerProps){
Expand All @@ -18,7 +19,7 @@ export function FormContainerBlock(props: FormContainerProps){
{/* 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>
)
}
19 changes: 9 additions & 10 deletions src/@episerver/forms-react/src/components/FormStepNavigation.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,35 @@
import React, { useRef } from "react";
import { useForms } from "../context/store";
import { FormCache, FormConstants, FormContainer, FormStep, StepDependCondition, SubmitButtonType, isNull } from "@episerver/forms-sdk";
import { FormCache, FormContainer, FormStep, StepDependCondition, SubmitButtonType, isNull } 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;
currentStepIndex: number;
}

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, currentStepIndex } = props;
const dispatchFuncs = new DispatchFunctions();
const stepLocalizations = useRef<Record<string, string>>(form.steps?.filter(s => !isNull(s.formStep.localizations))[0]?.formStep.localizations).current;

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

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()
Expand All @@ -43,9 +45,6 @@ export const FormStepNavigation = (props: FormStepNavigationProps) => {
const goToStep = (stepIndex: number) => {
var step = form.steps[stepIndex].formStep as FormStep

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)
history && history.push(url.pathname);
Expand Down
7 changes: 0 additions & 7 deletions src/@episerver/forms-react/src/context/dispatchFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,6 @@ export class DispatchFunctions {
});
}

updateCurrentStepIndex = (currentStepIndex?: number) => {
this._dispatch({
type: ActionType.UpdateCurrentStepIndex,
currentStepIndex
});
}

updateIsSubmitting = (isSubmitting?: boolean) => {
this._dispatch({
type: ActionType.UpdateIsSubmitting,
Expand Down
7 changes: 0 additions & 7 deletions src/@episerver/forms-react/src/context/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export enum ActionType {
UpdateFocusOn = "UpdateFocusOn",
UpdateIdentityInfo = "UpdateIdentityInfo",
UpdateSubmissionKey = "UpdateSubmissionKey",
UpdateCurrentStepIndex = "UpdateCurrentStepIndex",
UpdateIsSubmitting = "UpdateIsSubmitting"
}

Expand Down Expand Up @@ -79,12 +78,6 @@ export function formReducer(formState: FormState, action: any) {
submissionKey: action.submissionKey
} as FormState
}
case ActionType.UpdateCurrentStepIndex: {
return {
...formState,
currentStepIndex: action.currentStepIndex
} as FormState
}
case ActionType.UpdateIsSubmitting: {
return {
...formState,
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

0 comments on commit 6883380

Please sign in to comment.