From 8b39bf4193c9f404c7752d3a8f08cd027ea33001 Mon Sep 17 00:00:00 2001 From: Maciej Lenartowicz Date: Thu, 7 Nov 2019 17:21:14 +0000 Subject: [PATCH] Loading and errors HOCs --- src/bootstrap/Spinner.js | 2 +- src/form/components/ForisForm.js | 57 +++++---- src/form/components/alerts.js | 46 -------- src/index.js | 4 + src/utils/ErrorMessage.js | 16 +++ .../conditionalHOCs.test.js.snap | 61 ++++++++++ src/utils/__tests__/conditionalHOCs.test.js | 110 ++++++++++++++++++ src/utils/conditionalHOCs.js | 52 +++++++++ 8 files changed, 276 insertions(+), 72 deletions(-) delete mode 100644 src/form/components/alerts.js create mode 100644 src/utils/ErrorMessage.js create mode 100644 src/utils/__tests__/__snapshots__/conditionalHOCs.test.js.snap create mode 100644 src/utils/__tests__/conditionalHOCs.test.js create mode 100644 src/utils/conditionalHOCs.js diff --git a/src/bootstrap/Spinner.js b/src/bootstrap/Spinner.js index c45776a..43ca59f 100644 --- a/src/bootstrap/Spinner.js +++ b/src/bootstrap/Spinner.js @@ -28,7 +28,7 @@ export function Spinner({ }) { if (!fullScreen) { return ( -
+
{children}
); diff --git a/src/form/components/ForisForm.js b/src/form/components/ForisForm.js index c03badc..f739f5b 100644 --- a/src/form/components/ForisForm.js +++ b/src/form/components/ForisForm.js @@ -5,16 +5,19 @@ * See /LICENSE for more information. */ -import React, { useEffect, useState } from "react"; +import React, { useEffect } from "react"; import PropTypes from "prop-types"; import { Spinner } from "bootstrap/Spinner"; import { useAPIPost } from "api/hooks"; import { Prompt } from "react-router"; +import { API_STATE } from "api/utils"; +import { ErrorMessage } from "utils/ErrorMessage"; +import { useAlert } from "alertContext/AlertContext"; +import { ALERT_TYPES } from "bootstrap/Alert"; import { useForisModule, useForm } from "../hooks"; import { STATES as SUBMIT_BUTTON_STATES, SubmitButton } from "./SubmitButton"; -import { FailAlert, SuccessAlert } from "./alerts"; ForisForm.propTypes = { /** WebSocket object see `scr/common/WebSockets.js`. */ @@ -69,22 +72,34 @@ export function ForisForm({ children, }) { const [formState, onFormChangeHandler, resetFormData] = useForm(validator, prepData); + const [setAlert] = useAlert(); const [forisModuleState] = useForisModule(ws, forisConfig); useEffect(() => { - if (forisModuleState.data) { + if (forisModuleState.state === API_STATE.SUCCESS) { resetFormData(forisModuleState.data); } - }, [forisModuleState.data, resetFormData, prepData]); + }, [forisModuleState, resetFormData, prepData]); const [postState, post] = useAPIPost(forisConfig.endpoint); useEffect(() => { - if (postState.isSuccess) postCallback(); - }, [postCallback, postState.isSuccess]); + if (postState.state === API_STATE.SUCCESS) { + postCallback(); + setAlert(_("Settings saved successfully"), ALERT_TYPES.SUCCESS); + } else if (postState.state === API_STATE.ERROR) { + setAlert(_("Cannot save settings")); + } + }, [postCallback, postState.state, setAlert]); + if (forisModuleState.state === API_STATE.ERROR) { + return ; + } + if (!formState.data) { + return ; + } - function onSubmitHandler(e) { - e.preventDefault(); + function onSubmitHandler(event) { + event.preventDefault(); resetFormData(); const copiedFormData = JSON.parse(JSON.stringify(formState.data)); const preparedData = prepDataToSubmit(copiedFormData); @@ -92,16 +107,18 @@ export function ForisForm({ } function getSubmitButtonState() { - if (postState.isSending) return SUBMIT_BUTTON_STATES.SAVING; - if (forisModuleState.isLoading) return SUBMIT_BUTTON_STATES.LOAD; + if (postState.state === API_STATE.SENDING) { + return SUBMIT_BUTTON_STATES.SAVING; + } + if (forisModuleState.state === API_STATE.SENDING) { + return SUBMIT_BUTTON_STATES.LOAD; + } return SUBMIT_BUTTON_STATES.READY; } - const [alertIsDismissed, setAlertIsDismissed] = useState(false); - - if (!formState.data) return ; - - const formIsDisabled = disabled || forisModuleState.isLoading || postState.isSending; + const formIsDisabled = (disabled + || forisModuleState.state === API_STATE.SENDING + || postState.state === API_STATE.SENDING); const submitButtonIsDisabled = disabled || !!formState.errors; const childrenWithFormProps = React.Children.map( @@ -123,19 +140,9 @@ export function ForisForm({ return _("Changes you made may not be saved. Are you sure you want to leave?"); } - let alert = null; - if (!alertIsDismissed) { - if (postState.isSuccess) { - alert = setAlertIsDismissed(true)} />; - } else if (postState.isError) { - alert = setAlertIsDismissed(true)} />; - } - } - return ( <> - {alert}
{childrenWithFormProps} - - - ); -} - -FailAlert.propTypes = { - onDismiss: PropTypes.func.isRequired, -}; - -export function FailAlert({ onDismiss }) { - return ( - - - - ); -} diff --git a/src/index.js b/src/index.js index 22cfc23..5f755d9 100644 --- a/src/index.js +++ b/src/index.js @@ -50,6 +50,10 @@ export { WebSockets } from "webSockets/WebSockets"; // Utils export { Portal } from "utils/Portal"; export { undefinedIfEmpty, withoutUndefinedKeys, onlySpecifiedKeys } from "utils/objectHelpers"; +export { + withEither, withSpinner, withSending, withSpinnerOnSending, withError, withErrorMessage, +} from "utils/conditionalHOCs"; +export { ErrorMessage } from "utils/ErrorMessage"; // Foris URL export { ForisURLs, REFORIS_URL_PREFIX } from "forisUrls"; diff --git a/src/utils/ErrorMessage.js b/src/utils/ErrorMessage.js new file mode 100644 index 0000000..22e4f7b --- /dev/null +++ b/src/utils/ErrorMessage.js @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/) + * + * This is free software, licensed under the GNU General Public License v3. + * See /LICENSE for more information. + */ + +import React from "react"; + +export function ErrorMessage() { + return ( +

+ {_("An error occurred while fetching data.")} +

+ ); +} diff --git a/src/utils/__tests__/__snapshots__/conditionalHOCs.test.js.snap b/src/utils/__tests__/__snapshots__/conditionalHOCs.test.js.snap new file mode 100644 index 0000000..f2e1040 --- /dev/null +++ b/src/utils/__tests__/__snapshots__/conditionalHOCs.test.js.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`conditional HOCs withError should render error message 1`] = ` +
+

+ An error occurred while fetching data. +

+
+`; + +exports[`conditional HOCs withErrorMessage should render error message 1`] = ` +
+

+ An error occurred while fetching data. +

+
+`; + +exports[`conditional HOCs withSpinner should render spinner 1`] = ` +
+
+
+ +
+
+
+
+`; + +exports[`conditional HOCs withSpinnerOnSending should render spinner 1`] = ` +
+
+
+ +
+
+
+
+`; diff --git a/src/utils/__tests__/conditionalHOCs.test.js b/src/utils/__tests__/conditionalHOCs.test.js new file mode 100644 index 0000000..eb8985e --- /dev/null +++ b/src/utils/__tests__/conditionalHOCs.test.js @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/) + * + * This is free software, licensed under the GNU General Public License v3. + * See /LICENSE for more information. + */ + +import React from "react"; +import { render } from "customTestRender"; +import { + withEither, withSpinner, withSending, withSpinnerOnSending, withError, withErrorMessage, +} from "../conditionalHOCs"; +import { API_STATE } from "api/utils"; + +describe("conditional HOCs", () => { + const First = () =>

First

; + const Alternative = () =>

Alternative

; + + describe("withEither", () => { + it("should render First component", () => { + const withAlternative = withEither(() => false, Alternative); + const FirstWithConditional = withAlternative(First); + const { getByText } = render(); + expect(getByText("First")).toBeDefined(); + }); + + it("should render Alternative component", () => { + const withAlternative = withEither(() => true, Alternative); + const FirstWithConditional = withAlternative(First); + const { getByText } = render(); + expect(getByText("Alternative")).toBeDefined(); + }); + }); + + describe("withSpinner", () => { + it("should render First component", () => { + const withSpinnerHidden = withSpinner(() => false); + const FirstWithConditional = withSpinnerHidden(First); + const { getByText } = render(); + expect(getByText("First")).toBeDefined(); + }); + + it("should render spinner", () => { + const withSpinnerVisible = withSpinner(() => true); + const FirstWithConditional = withSpinnerVisible(First); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + }); + + describe("withSending", () => { + it("should render First component", () => { + const withAlternative = withSending(Alternative); + const FirstWithConditional = withAlternative(First); + const { getByText } = render(); + expect(getByText("First")).toBeDefined(); + }); + + it("should render Alternative component", () => { + const withAlternative = withSending(Alternative); + const FirstWithConditional = withAlternative(First); + const { getByText } = render(); + expect(getByText("Alternative")).toBeDefined(); + }); + }); + + describe("withSpinnerOnSending", () => { + it("should render First component", () => { + const FirstWithConditional = withSpinnerOnSending(First); + const { getByText } = render(); + expect(getByText("First")).toBeDefined(); + }); + + it("should render spinner", () => { + const FirstWithConditional = withSpinnerOnSending(First); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + }); + + describe("withError", () => { + it("should render First component", () => { + const withErrorHidden = withError(() => false); + const FirstWithConditional = withErrorHidden(First); + const { getByText } = render(); + expect(getByText("First")).toBeDefined(); + }); + + it("should render error message", () => { + const withErrorVisible = withError(() => true); + const FirstWithConditional = withErrorVisible(First); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + }); + + describe("withErrorMessage", () => { + it("should render First component", () => { + const FirstWithConditional = withErrorMessage(First); + const { getByText } = render(); + expect(getByText("First")).toBeDefined(); + }); + + it("should render error message", () => { + const FirstWithConditional = withErrorMessage(First); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/src/utils/conditionalHOCs.js b/src/utils/conditionalHOCs.js new file mode 100644 index 0000000..4c38fb4 --- /dev/null +++ b/src/utils/conditionalHOCs.js @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/) + * + * This is free software, licensed under the GNU General Public License v3. + * See /LICENSE for more information. + */ + +import React from "react"; + +import { Spinner } from "bootstrap/Spinner"; +import { API_STATE } from "api/utils"; +import { ErrorMessage } from "./ErrorMessage"; + +function withEither(conditionalFn, Either) { + return (Component) => (props) => { + if (conditionalFn(props)) { + return ; + } + return ; + }; +} + +// Loading + +function isSending(props) { + if (Array.isArray(props.apiState)) { + return props.apiState.some( + (state) => [API_STATE.INIT, API_STATE.SENDING].includes(state), + ); + } + return [API_STATE.INIT, API_STATE.SENDING].includes(props.apiState); +} + +const withSpinner = (conditionalFn) => withEither(conditionalFn, Spinner); +const withSending = (Either) => withEither(isSending, Either); +const withSpinnerOnSending = withSpinner(isSending); + +// Error handling + +const withError = (conditionalFn) => withEither(conditionalFn, ErrorMessage); +const withErrorMessage = withError( + (props) => { + if (Array.isArray(props.apiState)) { + return props.apiState.includes(API_STATE.ERROR); + } + return props.apiState === API_STATE.ERROR; + }, +); + +export { + withEither, withSpinner, withSending, withSpinnerOnSending, withError, withErrorMessage, +};