1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00
lens/dashboard/client/components/wizard/wizard.tsx
Jari Kolehmainen 1d0815abd2
Lens app source code (#119)
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
2020-03-15 09:52:02 +02:00

231 lines
6.7 KiB
TypeScript
Executable File

import "./wizard.scss";
import * as React from "react";
import { Trans } from "@lingui/macro";
import { cssNames, prevDefault } from "../../utils";
import { Button } from "../button";
import { Stepper } from "../stepper";
import { SubTitle } from "../layout/sub-title";
import { Spinner } from "../spinner";
interface WizardCommonProps<D = any> {
data?: Partial<D>;
save?: (data: Partial<D>, callback?: () => void) => void;
reset?: () => void;
done?: () => void;
hideSteps?: boolean;
}
export interface WizardProps extends WizardCommonProps {
className?: string;
step?: number;
title?: string;
header?: React.ReactNode;
onChange?: (step: number) => void;
}
interface State {
step?: number;
}
export class Wizard extends React.Component<WizardProps, State> {
public state: State = {
step: this.getValidStep(this.props.step)
};
get steps() {
const { className, title, step, header, onChange, children, ...commonProps } = this.props;
const steps = React.Children.toArray(children) as WizardStepElem[];
return steps.filter(step => !step.props.skip).map((stepElem, i) => {
const stepProps = stepElem.props;
return React.cloneElement(stepElem, {
step: i + 1,
wizard: this,
next: this.nextStep,
prev: this.prevStep,
first: this.firstStep,
last: this.lastStep,
isFirst: this.isFirstStep,
isLast: this.isLastStep,
...commonProps,
...stepProps
} as WizardStepProps<any>)
});
}
get step() {
return this.state.step;
}
set step(step: number) {
step = this.getValidStep(step);
if (step === this.step) return;
this.setState({ step }, () => {
if (this.props.onChange) {
this.props.onChange(step);
}
});
}
protected getValidStep(step: number) {
return Math.min(Math.max(1, step), this.steps.length) || 1;
}
isFirstStep = () => this.step === 1;
isLastStep = () => this.step === this.steps.length;
firstStep = (): any => this.step = 1;
nextStep = (): any => this.step++;
prevStep = (): any => this.step--;
lastStep = (): any => this.step = this.steps.length;
render() {
const { className, title, header, hideSteps } = this.props;
const steps = this.steps.map(stepElem => ({ title: stepElem.props.title }))
const step = React.cloneElement(this.steps[this.step - 1]);
return (
<div className={cssNames("Wizard", className)}>
<div className="header">
{header}
{title ? <SubTitle title={title}/> : null}
{!hideSteps && steps.length > 1 ? <Stepper steps={steps} step={this.step}/> : null}
</div>
{step}
</div>
);
}
}
export interface WizardStepProps<D = any> extends WizardCommonProps<D> {
wizard?: Wizard;
title?: string;
className?: string | object;
contentClass?: string | object;
customButtons?: React.ReactNode; // render custom buttons block in footer
moreButtons?: React.ReactNode; // add more buttons to section in the footer
loading?: boolean; // indicator of loading content for the step
waiting?: boolean; // indicator of waiting response before going to next step
disabledNext?: boolean; // disable next button flag, e.g when filling step is not finished
hideNextBtn?: boolean;
hideBackBtn?: boolean;
step?: number;
prevLabel?: React.ReactNode; // custom label for prev button
nextLabel?: React.ReactNode; // custom label for next button
next?: () => void | boolean | Promise<any>; // custom action for next button
prev?: () => void; // custom action for prev button
first?: () => void;
last?: () => void;
isFirst?: () => boolean;
isLast?: () => boolean;
beforeContent?: React.ReactNode;
afterContent?: React.ReactNode;
noValidate?: boolean; // no validate form attribute
skip?: boolean; // don't render the step
scrollable?: boolean;
}
interface WizardStepState {
waiting?: boolean;
}
type WizardStepElem = React.ReactElement<WizardStepProps>;
export class WizardStep extends React.Component<WizardStepProps, WizardStepState> {
private form: HTMLFormElement;
public state: WizardStepState = {};
private unmounting = false;
static defaultProps: WizardStepProps = {
scrollable: true,
}
componentWillUnmount() {
this.unmounting = true;
}
prev = () => {
const { isFirst, prev, done } = this.props;
if (isFirst() && done) done();
else prev();
}
next = () => {
const next = this.props.next;
const nextStep = this.props.wizard.nextStep;
if (nextStep !== next) {
const result = next();
if (result instanceof Promise) {
this.setState({ waiting: true });
result.then(nextStep).finally(() => {
if (this.unmounting) return;
this.setState({ waiting: false });
});
}
else if (typeof result === "boolean" && result) {
nextStep();
}
}
else {
nextStep();
}
}
submit = () => {
if (!this.form.noValidate) {
const valid = this.form.checkValidity();
if (!valid) return;
}
this.next();
}
renderLoading() {
return (
<div className="step-loading flex center">
<Spinner/>
</div>
)
}
render() {
const {
step, isFirst, isLast, children,
loading, customButtons, disabledNext, scrollable,
hideNextBtn, hideBackBtn, beforeContent, afterContent, noValidate, skip, moreButtons,
} = this.props;
let { className, contentClass, nextLabel, prevLabel, waiting } = this.props;
if (skip) {
return;
}
waiting = (waiting !== undefined) ? waiting : this.state.waiting;
className = cssNames(`WizardStep step${step}`, className);
contentClass = cssNames("step-content", { scrollable }, contentClass);
prevLabel = prevLabel || (isFirst() ? <Trans>Cancel</Trans> : <Trans>Back</Trans>);
nextLabel = nextLabel || (isLast() ? <Trans>Submit</Trans> : <Trans>Next</Trans>);
return (
<form className={className}
onSubmit={prevDefault(this.submit)} noValidate={noValidate}
ref={e => this.form = e}>
{beforeContent}
<div className={contentClass}>
{loading ? this.renderLoading() : children}
</div>
{customButtons !== undefined ? customButtons : (
<div className="buttons flex gaps align-center">
{moreButtons}
<Button
className="back-btn"
plain label={prevLabel} hidden={hideBackBtn}
onClick={this.prev}
/>
<Button
primary type="submit"
label={nextLabel} hidden={hideNextBtn}
waiting={waiting} disabled={disabledNext}
/>
</div>
)}
{afterContent}
</form>
)
}
}