import type { Formik, FormikProps } from 'formik'; import { Form } from 'formik'; import { merge, without } from 'lodash'; import React from 'react'; import { Modal } from 'react-bootstrap'; import type { WizardPage } from './WizardPage'; import { WizardStepLabel } from './WizardStepLabel'; import { ModalClose } from '../buttons/ModalClose'; import { SubmitButton } from '../buttons/SubmitButton'; import { SpinFormik } from '../../presentation'; import type { IModalComponentProps } from '../../presentation'; import type { TaskMonitor } from '../../task/monitor/TaskMonitor'; import { TaskMonitorWrapper } from '../../task/monitor/TaskMonitorWrapper'; import { Spinner } from '../../widgets/spinners/Spinner'; export interface IWizardPageInjectedProps { formik: FormikProps; /** WizardModal supplies this incrementor fn, which should be used to supply the WizardPage order prop */ nextIdx: () => number; /** The WizardModal Callback API for use by WizardPage component */ wizard: IWizardModalApi; } export interface IWizardModalProps extends IModalComponentProps { formClassName?: string; heading: string; initialValues: T; loading?: boolean; render: (props: IWizardPageInjectedProps) => React.ReactNode; submitButtonLabel: string; taskMonitor: TaskMonitor; validate?(values: T): any; } export interface IWizardModalState { currentPage: WizardPage; initialized: boolean; pages: Array>; } export interface IWizardModalApi { onWizardPageAdded: (wizardPage: WizardPage) => void; onWizardPageRemoved: (wizardPage: WizardPage) => void; /** * The wrapped WizardPage component can call this when its state changes * and WizardModal will force a re-render */ onWizardPageStateChanged: (_page: WizardPage) => void; } export class WizardModal extends React.Component, IWizardModalState> implements IWizardModalApi { private stepsElement = React.createRef(); private formikRef = React.createRef>(); public state: IWizardModalState = { pages: [], initialized: false, currentPage: null }; private static incrementer() { let idx = 0; return () => ++idx; } public get formik() { return this.formikRef.current && (this.formikRef.current.getFormikBag() as FormikProps); } public componentDidMount(): void { this.setState({ initialized: true }); } public onWizardPageAdded = (wizardPage: WizardPage): void => { this.setState((prevState) => { const pages = prevState.pages.concat(wizardPage); const currentPage = prevState.currentPage || pages[0]; return { pages, currentPage }; }, this.revalidate); }; public onWizardPageRemoved = (wizardPage: WizardPage): void => { this.setState((prevState) => { const pages = without(prevState.pages, wizardPage); const currentPage = prevState.currentPage || pages[0]; return { pages, currentPage }; }, this.revalidate); }; private setCurrentPage = (currentPage: WizardPage): void => { if (currentPage && this.stepsElement.current && currentPage.ref.current) { this.stepsElement.current.scrollTop = currentPage.ref.current.offsetTop; } this.setState({ currentPage }); }; private handleStepsScroll = (event: React.UIEvent): void => { const pageTops = this.state.pages.map((page) => page.ref.current.offsetTop); const scrollTop = event.currentTarget.scrollTop; let reversedCurrentPage = pageTops.reverse().findIndex((pageTop) => scrollTop >= pageTop); if (reversedCurrentPage === undefined) { reversedCurrentPage = pageTops.length - 1; } const currentPageIndex = pageTops.length - (reversedCurrentPage + 1); const currentPage = this.state.pages[currentPageIndex]; if (currentPage !== this.state.currentPage) { this.setState({ currentPage }); } }; private validate = (values: T): any => { const validateProp = this.props.validate || (() => ({})); const errorsForPages: object[] = this.state.pages.map((page) => page.validate(values)).concat(validateProp(values)); return errorsForPages.reduce((mergedErrors, errorsForPage) => merge(mergedErrors, errorsForPage), {}); }; /** Rerender everything when a WizardPage requests it */ public onWizardPageStateChanged(_page: WizardPage) { this.forceUpdate(); } public revalidate = () => this.formik && this.formik.validateForm(); public render() { const { formClassName, heading, initialValues, loading, submitButtonLabel, taskMonitor, closeModal, dismissModal, } = this.props; const { currentPage, initialized, pages } = this.state; const spinner = (
); const pageLabels: React.ReactNode[] = pages .sort((a, b) => a.state.order - b.state.order) .map((page: WizardPage) => ( )); const renderPageContents = () => { const formik = this.formik; const nextIdx = WizardModal.incrementer(); return formik ? this.props.render({ formik, nextIdx, wizard: this }) : null; }; const isSubmitting = taskMonitor && taskMonitor.submitting; const anyLoading = pages.some((page) => page.state.status === 'loading'); return ( <> {taskMonitor && } ref={this.formikRef} initialValues={initialValues} onSubmit={closeModal} validate={this.validate} render={(formik) => (
{heading && {heading}} {loading || !initialized ? ( spinner ) : (
    {pageLabels}
{renderPageContents()}
)}
)} /> ); } }