basic/wizard/WizardModal.js
/**
* Created by jakub.niezgoda on 20/07/2018.
*/
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { ErrorMessage, Confirm, Modal, Step } from '../index';
import '../../styles/Wizard.css';
/**
* WizardModal component allows you to present step-by-step process in modal window providing convenient way
* to navigate between steps and store step's data during the process.
*
* Steps have to be constructed using {@link createWizardStep} function.
*
* ## Access
* `Stage.Basic.Wizard.Modal`
*
* ## Usage
*
* ![WizardModal](manual/asset/wizard/WizardModal_0.png)
* ```
* const wizardTitle = 'Hello World Wizard';
* const helloWorldWizardSteps = [
* InfrastructureStep, PluginsStep, SecretsStep, InputsStep, ConfirmationStep, InstallStep
* ];
*
* <Wizard.Modal header={wizardTitle} open={this.state.open} steps={helloWorldWizardSteps}
* onClose={this.closeWizard.bind(this)} toolbox={toolbox} />
*```
*/
export default class WizardModal extends Component {
constructor(props) {
super(props);
this.state = WizardModal.initialState(props.steps);
}
/**
* @property {boolean} open Controls whether or not the wizard modal is displayed
* @property {function(event: SyntheticEvent, data: object)} onClose Function called when wizard is to be closed
* @property {object[]} steps List of objects describing the steps (@see wizardUtils.createWizardStep function for details)
* @property {object} toolbox Toolbox object
*/
static propTypes = {
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
steps: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
Content: PropTypes.func.isRequired,
Actions: PropTypes.func.isRequired
})
).isRequired,
toolbox: PropTypes.object.isRequired
};
/**
* Active step state
*/
static ACTIVE_STATE = 'active';
/**
* Completed step state
*/
static COMPLETED_STATE = 'completed';
/**
* Disabled step state
*/
static DISABLED_STATE = 'disabled';
static initialState = steps => {
const activeStepIndex = 0;
const previousStepIndex = -1;
const stepsList = [];
const stepsObject = {};
for (const step of steps) {
const stepName = WizardModal.getStepNameById(step.id);
stepsList.push(stepName);
stepsObject[stepName] = { state: WizardModal.DISABLED_STATE, data: {}, errors: {} };
}
stepsObject[stepsList[activeStepIndex]].state = WizardModal.ACTIVE_STATE;
return {
activeStepIndex,
previousStepIndex,
steps: stepsList,
...stepsObject,
wizardData: {},
loading: false,
error: null,
showCloseModal: false
};
};
/**
* @param {string} id step ID
* @returns {string} step name used in wizard state
*/
static getStepNameById(id) {
return `${id}Step`;
}
shouldComponentUpdate(nextProps, nextState) {
return !_.isEqual(this.props.open, nextProps.open) || !_.isEqual(this.state, nextState);
}
componentDidUpdate(prevProps) {
if (prevProps.open && !this.props.open) {
// reset wizard on close
this.setState({ ...WizardModal.initialState(this.props.steps) });
}
}
/**
* @param {string} index step index in steps list
* @returns {string} step name used in wizard state
*/
getStepNameByIndex(index) {
return WizardModal.getStepNameById(this.props.steps[index].id);
}
showCloseModal() {
this.setState({ showCloseModal: true });
}
hideCloseModal() {
this.setState({ showCloseModal: false });
}
/**
* Function called on click on Start Over action button. Changes WizardModal state.
*
* @param {boolean} resetData If set to true, then wizard data will be reset
*/
onStartOver(resetData) {
if (resetData) {
this.setState({ ...WizardModal.initialState(this.props.steps) });
} else {
const activeStepName = this.getStepNameByIndex(0);
const stepsObject = {};
for (const step of this.props.steps) {
const stepName = WizardModal.getStepNameById(step.id);
stepsObject[stepName] = { ...this.state[stepName], state: WizardModal.DISABLED_STATE, errors: {} };
}
stepsObject[activeStepName].state = WizardModal.ACTIVE_STATE;
this.setState({
activeStepIndex: 0,
previousStepIndex: this.state.activeStepIndex,
...stepsObject,
error: null,
loading: false
});
}
}
/**
* Function called on click on Next action button. Changes WizardModal state by merging stepOutputData with wizardData
* changing state of current step to WizardModal.COMPLETED_STATE and incrementing index of active step.
*
* @param {string} id step ID
* @param {object} [stepOutputData] object with step data to be merged into wizardData
*/
onNext(id, stepOutputData) {
if (this.getStepNameByIndex(this.state.activeStepIndex) !== WizardModal.getStepNameById(id)) {
return;
}
const activeStepIndex = this.state.activeStepIndex + 1;
const previousStepIndex = this.state.activeStepIndex;
const activeStepName = this.getStepNameByIndex(activeStepIndex);
const previousStepName = this.getStepNameByIndex(previousStepIndex);
const wizardData = { ...this.state.wizardData, ...stepOutputData };
this.setState({
activeStepIndex,
previousStepIndex,
[activeStepName]: { ...this.state[activeStepName], state: WizardModal.ACTIVE_STATE },
[previousStepName]: { ...this.state[previousStepName], state: WizardModal.COMPLETED_STATE },
wizardData,
error: null,
loading: false
});
}
/**
* Function called on click on Back action button. Changes WizardModal state by merging stepOutputData with wizardData
* changing state of current step to WizardModal.COMPLETED_STATE and decrementing index of active step.
*
* @param {string} id step ID
* @param {object} [stepOutputData] object with step data to be merged into wizardData
*/
onPrev(id, stepOutputData) {
if (this.getStepNameByIndex(this.state.activeStepIndex) !== WizardModal.getStepNameById(id)) {
return;
}
const activeStepIndex = this.state.activeStepIndex - 1;
const previousStepIndex = this.state.activeStepIndex;
const activeStepName = this.getStepNameByIndex(activeStepIndex);
const previousStepName = this.getStepNameByIndex(previousStepIndex);
const wizardData = { ...this.state.wizardData, ...stepOutputData };
this.setState({
activeStepIndex,
previousStepIndex,
[activeStepName]: { ...this.state[activeStepName], state: WizardModal.ACTIVE_STATE },
[previousStepName]: { ...this.state[previousStepName], state: WizardModal.DISABLED_STATE },
wizardData,
error: null
});
}
/**
* Function called to show error in wizard. Changes WizardModal state to show error message and/or errors in the form
*
* @param {string} id step ID
* @param {string} errorMessage error message to be shown in wizard
* @param {object} [errors=undefined] object with errors used to mark fields in form as 'containing errors'
*/
onError(id, errorMessage, errors) {
if (this.getStepNameByIndex(this.state.activeStepIndex) !== WizardModal.getStepNameById(id)) {
return;
}
if (!_.isNil(errors)) {
const stepName = WizardModal.getStepNameById(id);
const stepState = this.state[stepName];
return new Promise(resolve =>
this.setState({ [stepName]: { ...stepState, errors }, error: errorMessage, loading: false }, resolve)
);
}
return new Promise(resolve => this.setState({ error: errorMessage, loading: false }, resolve));
}
/**
* Function called to turn on loading state
*/
onLoading() {
return new Promise(resolve => this.setState({ loading: true }, resolve));
}
/**
* Function called to turn off loading state
*/
onReady() {
return new Promise(resolve => this.setState({ loading: false }, resolve));
}
/**
* Function called on step content update to update wizard state - either stepData or wizardData
*
* @param {string} id step ID
* @param {string} data object with step data
* @param {object} [internal=true] If true then data is treated as step internal data, if false, then as wizard global data
*/
onStepDataChanged(id, data, internal = true) {
if (internal) {
// internal step data => state[stepId]
const stepName = WizardModal.getStepNameById(id);
const stepState = this.state[stepName];
this.setState({ [stepName]: { ...stepState, data, errors: {} }, error: null });
} else {
// step output data => state.wizardData
const wizardData = { ...this.state.wizardData, ...data };
this.setState({ wizardData });
}
}
/**
* Function called to provide current step data from wizard
*
* @param {string} id step ID
* @returns {Promise<{stepData: object}, error>} Promise containing step data object
*/
fetchStepData(id) {
const stepName = WizardModal.getStepNameById(id);
const stepState = this.state[stepName];
return Promise.resolve({ stepData: stepState.data });
}
render() {
const { steps } = this.props;
const ActiveStep = steps[this.state.activeStepIndex];
const activeStepName = this.getStepNameByIndex(this.state.activeStepIndex);
const activeStepObject = this.state[activeStepName];
const className = `wizardModal ${activeStepName}`;
return (
<Modal
open={this.props.open}
onClose={this.props.onClose}
className={className}
closeIcon={false}
closeOnEscape={false}
closeOnDimmerClick={false}
>
<Modal.Header>{this.props.header}</Modal.Header>
<Modal.Description>
<Step.Group ordered fluid widths={steps.length}>
{_.map(steps, (step, index) => (
<Step
active={this.state[this.getStepNameByIndex(index)].state === WizardModal.ACTIVE_STATE}
completed={
this.state[this.getStepNameByIndex(index)].state === WizardModal.COMPLETED_STATE
}
disabled={
this.state[this.getStepNameByIndex(index)].state === WizardModal.DISABLED_STATE
}
id={step.id}
key={step.id}
>
<Step.Content>
<Step.Title>{step.title}</Step.Title>
<Step.Description>{step.description}</Step.Description>
</Step.Content>
</Step>
))}
</Step.Group>
<ErrorMessage
error={this.state.error}
onDismiss={() =>
this.setState({ [activeStepName]: { ...activeStepObject, errors: {} }, error: null })
}
autoHide
/>
</Modal.Description>
<Modal.Content>
<ActiveStep.Content
stepData={activeStepObject.data}
wizardData={this.state.wizardData}
onLoading={this.onLoading.bind(this)}
onReady={this.onReady.bind(this)}
onError={this.onError.bind(this)}
onChange={this.onStepDataChanged.bind(this)}
errors={activeStepObject.errors}
loading={this.state.loading}
toolbox={this.props.toolbox}
/>
</Modal.Content>
<Modal.Actions>
<ActiveStep.Actions
onClose={this.showCloseModal.bind(this)}
onStartOver={this.onStartOver.bind(this)}
onPrev={this.onPrev.bind(this)}
onNext={this.onNext.bind(this)}
onError={this.onError.bind(this)}
onLoading={this.onLoading.bind(this)}
onReady={this.onReady.bind(this)}
disabled={this.state.loading}
showPrev={this.state.activeStepIndex !== 0}
fetchData={this.fetchStepData.bind(this, ActiveStep.id)}
wizardData={this.state.wizardData}
toolbox={this.props.toolbox}
/>
<Confirm
content="Are you sure you want to close the wizard?"
open={this.state.showCloseModal}
onConfirm={this.props.onClose}
onCancel={this.hideCloseModal.bind(this)}
/>
</Modal.Actions>
</Modal>
);
}
}