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"