UNPKG

glass-app-manager

Version:

Informatica's Glass Framework CLI for bootstrapping

232 lines (201 loc) 8.34 kB
// @flow import * as React from "react"; import Button from "../button/Button"; import Roadmap from "../roadmap/Roadmap"; type VisitedSteps = Array<boolean>; type StepDefinition = {| name: string, render: () => React.Node, beforeLeave?: () => Promise<boolean>, beforeEnter?: () => Promise<boolean>, isValid?: () => Promise<boolean>, |}; type Steps = Array<StepDefinition>; type RenderProps = {| isFirstStep: boolean, isLastStep: boolean, isSubmitting: boolean, step: number, steps: Steps, setStep: number => void, visitedSteps: VisitedSteps, |}; type WizardProps = { children: RenderProps => React.Node | React.Node, /** * Each `step` is represented by a `StepDefinition` object with the following properties: * * * __`name: string`__: * The name of the step. Shown in the `Roadmap` component. * * * __`render: () => React.Node`__: * A function that returns the content that should be rendered for this step. * Shown in the `Wizard.View` component. * * * __`isValid?: () => Promise<boolean>`__: * An optional callback that will be called for validating existing step. * This will be called only when the step that is being transitioned to is not previously visited. * This function should always return a `Promise` — if a synchronous operation * is used to determine the result, you can simply return `Promise.resolve(true | false)`. * This is where you can define step validation logic (or use a third-party library like Formik) * and block all step changes. This is useful for create wizards where users shouldnt be allowed * to proceed to the next step if the current step is in an invalid state. * * * __`beforeEnter?: () => Promise<boolean>`__: * An optional callback used when Wizard attempts to visit the step. * This function should always return a `Promise` — if a synchronous operation * is used to determine the result, you can simply return `Promise.resolve(true | false)`. * This is where you can explicitly check for field values that must be valid before a step can * be visitied and block step changes if the requested step depends on a previous invalid step * or a field value. This is useful for editable Wizards where users can be allowed to switch between * steps even when the steps are invalid. * * * __`beforeLeave?: () => Promise<boolean>`__: * An optional callback used when Wizard attempts to change the step. * This function should always return a `Promise` — if a synchronous operation * is used to determine the result, you can simply return `Promise.resolve(true | false)`. * This is where you can add the mandatory condition which need to be checked before leaving the current step, * You can block the step change by returning `false` from the function. You can continue to move into next step by returning `true`. * This is useful for Wizards where a step wants to enforce some logic before a step transition is triggered. * */ steps: Steps, /** * When implementing an editable Wizard, you can specify the * initialVisited prop to show specific (or all) steps as visited. */ initialVisited?: VisitedSteps, /** * Used when performing an asynchronous action that should block * Wizard controls / step changes from occurring. */ isSubmitting?: boolean, }; const WizardContext = React.createContext<RenderProps>({ isFirstStep: false, isLastStep: false, isSubmitting: false, step: 0, steps: [], setStep: () => undefined, visitedSteps: [], }); function shouldAllowStepChange( { beforeLeave = () => Promise.resolve(true), isValid = () => Promise.resolve(true) }, { beforeEnter = () => Promise.resolve(true) }, isNextStepVisited = false ): Promise<boolean> { return new Promise((resolve, reject) => { isValid = isNextStepVisited ? () => Promise.resolve(true) : isValid; isValid().then(valid => { if (!valid) { return resolve(false); } else { beforeLeave().then(result => { if (!result) { return resolve(false); } else { beforeEnter().then(result => resolve(!!result)); } }); } }); }); } function Wizard({ children, initialVisited = [true], isSubmitting = false, steps, ...rest }: WizardProps) { if (!steps || steps.length === 0) { throw new Error("Wizard: `steps` prop cannot be empty"); } const [step, setStep] = React.useState(0); const [visitedSteps, setVisitedSteps] = React.useState<VisitedSteps>(initialVisited); const handleStepChange: (nextStep: number) => void = React.useCallback( (nextStepIndex: number): void => { if (nextStepIndex < 0 || nextStepIndex >= steps.length) { throw new Error("Wizard: Attempted to access an out-of-bounds step."); } const currentStep = steps[step]; const nextStep = steps[nextStepIndex]; shouldAllowStepChange(currentStep, nextStep, visitedSteps[nextStepIndex]).then(allow => { if (!allow) { if (nextStepIndex < step) { throw new Error( "Wizard: beforeEnter should never return false when attempting to go backwards." ); } return; } setVisitedSteps(prevState => { const newArray = prevState.slice(); newArray[nextStepIndex] = true; return newArray; }); setStep(nextStepIndex); }); }, [steps, step, setStep, setVisitedSteps] ); const renderProps: RenderProps = React.useMemo( () => ({ isFirstStep: step === 0, isLastStep: step === steps.length - 1, isSubmitting, step, steps, setStep: handleStepChange, visitedSteps, }), [isSubmitting, step, handleStepChange, visitedSteps] ); return ( <WizardContext.Provider value={renderProps}> {typeof children === "function" ? children(renderProps) : children} </WizardContext.Provider> ); } type WizardControlsProps = {| renderNextOnLastStep?: React.Node, |}; Wizard.Controls = ({ renderNextOnLastStep }: WizardControlsProps) => { const { isSubmitting, isLastStep, isFirstStep, step, setStep } = React.useContext<RenderProps>(WizardContext); return ( <span> <Button onClick={() => setStep(step - 1)} disabled={isFirstStep || isSubmitting}> &lt; Back </Button>{" "} {isLastStep && renderNextOnLastStep ? ( renderNextOnLastStep ) : ( <Button onClick={() => setStep(step + 1)} disabled={isLastStep || isSubmitting} variant="primary"> Next &gt; </Button> )} </span> ); }; Wizard.View = () => { const { step, steps } = React.useContext<RenderProps>(WizardContext); return steps[step].render(); }; Wizard.Roadmap = () => { const { steps, step, setStep, visitedSteps } = React.useContext<RenderProps>(WizardContext); const getStatus = idx => { if (step === idx) { return "current"; } return visitedSteps[idx] ? "enabled" : "disabled"; }; return ( <Roadmap> {steps.map((step, idx) => ( <Roadmap.Step index={idx} key={step.name} name={step.name} onClick={() => setStep(idx)} status={getStatus(idx)} /> ))} </Roadmap> ); }; export default Wizard;