diff --git a/README.md b/README.md index 2386223..cf2b408 100644 --- a/README.md +++ b/README.md @@ -242,6 +242,62 @@ const MyObjectsTable = () => ( ); ``` +## Wizard + +Display a Step by Step customizable wizard + +``` +const getStepsBaseInfo = [ + { + key: "general-info", + label: "General info", + component: GeneralInfoStep, + validationKeys: ["name"], + description: "Description for a wizard step", + help: "Help text", + }, + { + key: "summary", + label: "Summary", + component: SaveStep, + validationKeys: [], + description: undefined, + help: undefined, + }, +]; + +onStepChangeRequest = async currentStep => { + return getValidationMessages( + currentStep.validationKeys + ); +}; + +const MyWizard = props => { + const steps = getStepsBaseInfo.map(step => ({ + ...step, + props: { + onCancel: () => console.log("User wants to cancel the wizard!"), + }, + })); + + const urlHash = props.location.hash.slice(1); + const stepExists = steps.find(step => step.key === urlHash); + const firstStepKey = steps.map(step => step.key)[0]; + const initialStepKey = stepExists ? urlHash : firstStepKey; + const lastClickableStepIndex = props.isEdit ? steps.length - 1 : 0; + + return ( + + ); +}; +``` + # Setup ``` diff --git a/i18n/en.pot b/i18n/en.pot index 3c9e9d5..3a6a249 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2019-04-26T10:47:18.582Z\n" -"PO-Revision-Date: 2019-04-26T10:47:18.582Z\n" +"POT-Creation-Date: 2019-06-28T07:35:50.513Z\n" +"PO-Revision-Date: 2019-06-28T07:35:50.513Z\n" msgid "Save" msgstr "" @@ -96,3 +96,12 @@ msgstr "" msgid "Search by name" msgstr "" + +msgid "Help" +msgstr "" + +msgid "Previous" +msgstr "" + +msgid "Next" +msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 89e0074..5cbf2d1 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2019-04-26T10:47:18.582Z\n" +"POT-Creation-Date: 2019-06-28T07:35:50.513Z\n" "PO-Revision-Date: 2019-02-14T11:21:26.165Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -9,10 +9,10 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1)\n" msgid "Save" -msgstr "" +msgstr "Guardar" msgid "Cancel" -msgstr "" +msgstr "Cancelar" msgid "Actions" msgstr "Acciones" @@ -51,10 +51,10 @@ msgid "selected" msgstr "seleccionados" msgid "There are {{total}} items selected in all pages." -msgstr "" +msgstr "Hay {{total}} elementos seleccionados en todas las páginas." msgid "Clear selection" -msgstr "" +msgstr "Borrar selección" msgid "There are {{count}} items selected ({{invisible}} on other pages)." msgid_plural "" @@ -97,3 +97,12 @@ msgstr "Grupo de unidad organizativa" msgid "Search by name" msgstr "Buscar por nombre" + +msgid "Help" +msgstr "Ayuda" + +msgid "Previous" +msgstr "Anterior" + +msgid "Next" +msgstr "Siguiente" diff --git a/i18n/fr.po b/i18n/fr.po index 96eb4ca..10174ec 100644 --- a/i18n/fr.po +++ b/i18n/fr.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2019-04-26T10:47:18.582Z\n" +"POT-Creation-Date: 2019-06-28T07:35:50.513Z\n" "PO-Revision-Date: 2019-02-14T11:21:26.165Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -97,3 +97,12 @@ msgstr "" msgid "Search by name" msgstr "" + +msgid "Help" +msgstr "" + +msgid "Previous" +msgstr "" + +msgid "Next" +msgstr "" diff --git a/package.json b/package.json index 3553e03..f3d24cc 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "lodash": "^4.17.11", "material-ui-pickers": "^2.1.2", "moment": "^2.22.2", - "nano-memoize": "^1.0.3", + "nano-memoize": "^1.1.5", "node-sass": "^4.11.0", "throttle-debounce": "^2.1.0" }, diff --git a/src/index.ts b/src/index.ts index 4420bd8..4533447 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import SnackbarProvider from "./snackbar/SnackbarProvider"; import { withLoading } from "./loading"; import LoadingProvider from "./loading/LoadingProvider"; import ObjectsTable from "./objects-table/ObjectsTable"; +import Wizard from "./wizard/Wizard"; import "./locales"; @@ -26,4 +27,5 @@ export { LoadingProvider, withLoading, ObjectsTable, + Wizard, }; diff --git a/src/wizard/Wizard.jsx b/src/wizard/Wizard.jsx new file mode 100644 index 0000000..4c7a261 --- /dev/null +++ b/src/wizard/Wizard.jsx @@ -0,0 +1,247 @@ +import React from "react"; +import PropTypes from "prop-types"; +import _ from "lodash"; +import memoize from "nano-memoize"; +import i18n from "@dhis2/d2-i18n"; +import { withStyles } from "@material-ui/core/styles"; +import Paper from "@material-ui/core/Paper"; +import Stepper from "@material-ui/core/Stepper"; +import Step from "@material-ui/core/Step"; +import StepButton from "@material-ui/core/StepButton"; +import Button from "@material-ui/core/Button"; +import { IconButton } from "@material-ui/core"; +import Icon from "@material-ui/core/Icon"; + +import { withSnackbar } from "../snackbar"; +import DialogButton from "../dialog-button/DialogButton"; + +const styles = theme => ({ + root: { + width: "100%", + }, + description: { + marginBottom: 15, + marginLeft: 3, + fontSize: "1.1em", + }, + button: { + margin: theme.spacing.unit, + marginRight: 5, + padding: 10, + }, + buttonDisabled: { + color: "grey !important", + }, + buttonContainer: { + display: "flex", + justifyContent: "flex-end", + paddingTop: 10, + }, + stepButton: { + width: "auto", + }, + contents: { + margin: 10, + padding: 25, + }, + messages: { + padding: 0, + listStyleType: "none", + color: "red", + }, + stepper: { + marginLeft: 10, + marginRight: 10, + }, +}); + +class Wizard extends React.Component { + state = { + currentStepKey: this.props.initialStepKey, + lastClickableStepIndex: this.props.lastClickableStepIndex || 0, + messages: [], + }; + + static propTypes = { + initialStepKey: PropTypes.string.isRequired, + onStepChangeRequest: PropTypes.func.isRequired, + useSnackFeedback: PropTypes.bool, + snackbar: PropTypes.object.isRequired, + steps: PropTypes.arrayOf( + PropTypes.shape({ + key: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + component: PropTypes.func.isRequired, + }) + ).isRequired, + lastClickableStepIndex: PropTypes.number, + }; + + static defaultProps = { + useSnackFeedback: false, + lastClickableStepIndex: 0, + }; + + getAdjacentSteps = () => { + const { steps } = this.props; + const { currentStepKey } = this.state; + const index = _(steps).findIndex(step => step.key === currentStepKey); + const prevStepKey = index >= 1 ? steps[index - 1].key : null; + const nextStepKey = index >= 0 && index < steps.length - 1 ? steps[index + 1].key : null; + return { prevStepKey, nextStepKey }; + }; + + nextStep = () => { + const { nextStepKey } = this.getAdjacentSteps(); + this.setStep(nextStepKey); + }; + + prevStep = () => { + const { prevStepKey } = this.getAdjacentSteps(); + this.setStep(prevStepKey); + }; + + renderNavigationButton = ({ stepKey, onClick, label }) => { + return ( + + ); + }; + + setStep = async newStepKey => { + const { currentStepKey, lastClickableStepIndex } = this.state; + const { onStepChangeRequest, steps } = this.props; + const stepsByKey = _.keyBy(steps, "key"); + const newStep = stepsByKey[newStepKey]; + const currentStep = stepsByKey[currentStepKey]; + const currentStepIndex = _(steps).findIndex(step => step.key === currentStepKey); + const newStepIndex = _(steps).findIndex(step => step.key === newStepKey); + const shouldValidate = newStepIndex > currentStepIndex; + const errorMessages = shouldValidate ? await onStepChangeRequest(currentStep, newStep) : []; + + if (_(errorMessages).isEmpty()) { + const newLastClickableStepIndex = Math.max(lastClickableStepIndex, newStepIndex); + this.setState({ + currentStepKey: newStepKey, + lastClickableStepIndex: newLastClickableStepIndex, + messages: [], + }); + } else { + if (this.props.useSnackFeedback) { + this.props.snackbar.error(errorMessages.join("\n"), { + autoHideDuration: null, + }); + } else { + this.setState({ messages: errorMessages }); + } + } + }; + + onStepClicked = memoize(stepKey => () => { + this.setStep(stepKey); + }); + + renderHelp = ({ step }) => { + const Button = ({ onClick }) => ( + + help + + ); + + return ( + + ); + }; + + renderFeedbackMessages = () => { + const { classes, useSnackFeedback } = this.props; + const { messages } = this.state; + + if (useSnackFeedback || messages.length === 0) { + return null; + } else { + return ( +
+
    + {messages.map((message, index) => ( +
  • {message}
  • + ))} +
+
+ ); + } + }; + + render() { + const { classes, steps } = this.props; + const { currentStepKey, lastClickableStepIndex } = this.state; + const index = _(steps).findIndex(step => step.key === currentStepKey); + const currentStepIndex = index >= 0 ? index : 0; + const currentStep = steps[currentStepIndex]; + const { prevStepKey, nextStepKey } = this.getAdjacentSteps(); + const NavigationButton = this.renderNavigationButton; + const Help = this.renderHelp; + const FeedbackMessages = this.renderFeedbackMessages; + + return ( +
+ + {steps.map((step, index) => ( + lastClickableStepIndex} + > + + {step.label} + + + {step.help && step === currentStep ? : null} + + ))} + + + + + + {currentStep.description && ( +
{currentStep.description}
+ )} + {} +
+ + + +
+
+
+ ); + } +} + +export default withSnackbar(withStyles(styles)(Wizard)); diff --git a/yarn.lock b/yarn.lock index d3539c1..2cd77c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4771,10 +4771,10 @@ nan@^2.10.0, nan@^2.9.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552" integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw== -nano-memoize@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/nano-memoize/-/nano-memoize-1.0.3.tgz#d755396cf9dc8ffd316a2bcf30ec0f89d1c707ea" - integrity sha512-U8vIl+g8gkWQD8vKAPI4Q4hJIJznRfT/AZZE2bagHmvEx9AaYSptQWC1f0saYPDJLHPLD7ZonleziM2Q0m5dLA== +nano-memoize@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/nano-memoize/-/nano-memoize-1.1.5.tgz#70de03b987a0435d5bd7be9425cce8c74ac440bc" + integrity sha512-AV4GIsQBJU8jpYWMClIm9cxSWRXZtgbkkaSXz9mSpTrJFLkMN3eXkTJDeO4SykHomjckiYE4Ba7UELto7KRMpA== nanomatch@^1.2.9: version "1.2.13"