mirror of
https://gitlab.nic.cz/turris/reforis/foris-js.git
synced 2024-12-26 00:21:36 +01:00
Loading and errors HOCs
This commit is contained in:
parent
644726a0fc
commit
8b39bf4193
|
@ -28,7 +28,7 @@ export function Spinner({
|
||||||
}) {
|
}) {
|
||||||
if (!fullScreen) {
|
if (!fullScreen) {
|
||||||
return (
|
return (
|
||||||
<div className={`spinner-wrapper ${className || ""}`} {...props}>
|
<div className={`spinner-wrapper ${className || "my-3 text-center"}`} {...props}>
|
||||||
<SpinnerElement>{children}</SpinnerElement>
|
<SpinnerElement>{children}</SpinnerElement>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,16 +5,19 @@
|
||||||
* See /LICENSE for more information.
|
* See /LICENSE for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect } from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
import { Spinner } from "bootstrap/Spinner";
|
import { Spinner } from "bootstrap/Spinner";
|
||||||
import { useAPIPost } from "api/hooks";
|
import { useAPIPost } from "api/hooks";
|
||||||
|
|
||||||
import { Prompt } from "react-router";
|
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 { useForisModule, useForm } from "../hooks";
|
||||||
import { STATES as SUBMIT_BUTTON_STATES, SubmitButton } from "./SubmitButton";
|
import { STATES as SUBMIT_BUTTON_STATES, SubmitButton } from "./SubmitButton";
|
||||||
import { FailAlert, SuccessAlert } from "./alerts";
|
|
||||||
|
|
||||||
ForisForm.propTypes = {
|
ForisForm.propTypes = {
|
||||||
/** WebSocket object see `scr/common/WebSockets.js`. */
|
/** WebSocket object see `scr/common/WebSockets.js`. */
|
||||||
|
@ -69,22 +72,34 @@ export function ForisForm({
|
||||||
children,
|
children,
|
||||||
}) {
|
}) {
|
||||||
const [formState, onFormChangeHandler, resetFormData] = useForm(validator, prepData);
|
const [formState, onFormChangeHandler, resetFormData] = useForm(validator, prepData);
|
||||||
|
const [setAlert] = useAlert();
|
||||||
|
|
||||||
const [forisModuleState] = useForisModule(ws, forisConfig);
|
const [forisModuleState] = useForisModule(ws, forisConfig);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (forisModuleState.data) {
|
if (forisModuleState.state === API_STATE.SUCCESS) {
|
||||||
resetFormData(forisModuleState.data);
|
resetFormData(forisModuleState.data);
|
||||||
}
|
}
|
||||||
}, [forisModuleState.data, resetFormData, prepData]);
|
}, [forisModuleState, resetFormData, prepData]);
|
||||||
|
|
||||||
const [postState, post] = useAPIPost(forisConfig.endpoint);
|
const [postState, post] = useAPIPost(forisConfig.endpoint);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (postState.isSuccess) postCallback();
|
if (postState.state === API_STATE.SUCCESS) {
|
||||||
}, [postCallback, postState.isSuccess]);
|
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 <ErrorMessage />;
|
||||||
|
}
|
||||||
|
if (!formState.data) {
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
|
||||||
function onSubmitHandler(e) {
|
function onSubmitHandler(event) {
|
||||||
e.preventDefault();
|
event.preventDefault();
|
||||||
resetFormData();
|
resetFormData();
|
||||||
const copiedFormData = JSON.parse(JSON.stringify(formState.data));
|
const copiedFormData = JSON.parse(JSON.stringify(formState.data));
|
||||||
const preparedData = prepDataToSubmit(copiedFormData);
|
const preparedData = prepDataToSubmit(copiedFormData);
|
||||||
|
@ -92,16 +107,18 @@ export function ForisForm({
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSubmitButtonState() {
|
function getSubmitButtonState() {
|
||||||
if (postState.isSending) return SUBMIT_BUTTON_STATES.SAVING;
|
if (postState.state === API_STATE.SENDING) {
|
||||||
if (forisModuleState.isLoading) return SUBMIT_BUTTON_STATES.LOAD;
|
return SUBMIT_BUTTON_STATES.SAVING;
|
||||||
|
}
|
||||||
|
if (forisModuleState.state === API_STATE.SENDING) {
|
||||||
|
return SUBMIT_BUTTON_STATES.LOAD;
|
||||||
|
}
|
||||||
return SUBMIT_BUTTON_STATES.READY;
|
return SUBMIT_BUTTON_STATES.READY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [alertIsDismissed, setAlertIsDismissed] = useState(false);
|
const formIsDisabled = (disabled
|
||||||
|
|| forisModuleState.state === API_STATE.SENDING
|
||||||
if (!formState.data) return <Spinner className="row justify-content-center" />;
|
|| postState.state === API_STATE.SENDING);
|
||||||
|
|
||||||
const formIsDisabled = disabled || forisModuleState.isLoading || postState.isSending;
|
|
||||||
const submitButtonIsDisabled = disabled || !!formState.errors;
|
const submitButtonIsDisabled = disabled || !!formState.errors;
|
||||||
|
|
||||||
const childrenWithFormProps = React.Children.map(
|
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?");
|
return _("Changes you made may not be saved. Are you sure you want to leave?");
|
||||||
}
|
}
|
||||||
|
|
||||||
let alert = null;
|
|
||||||
if (!alertIsDismissed) {
|
|
||||||
if (postState.isSuccess) {
|
|
||||||
alert = <SuccessAlert onDismiss={() => setAlertIsDismissed(true)} />;
|
|
||||||
} else if (postState.isError) {
|
|
||||||
alert = <FailAlert onDismiss={() => setAlertIsDismissed(true)} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Prompt message={getMessageOnLeavingPage} />
|
<Prompt message={getMessageOnLeavingPage} />
|
||||||
{alert}
|
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
{childrenWithFormProps}
|
{childrenWithFormProps}
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 PropTypes from "prop-types";
|
|
||||||
|
|
||||||
import { Alert } from "bootstrap/Alert";
|
|
||||||
import { Portal } from "utils/Portal";
|
|
||||||
|
|
||||||
SuccessAlert.propTypes = {
|
|
||||||
onDismiss: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ALERT_CONTAINER_ID = "alert-container";
|
|
||||||
|
|
||||||
export function SuccessAlert({ onDismiss }) {
|
|
||||||
return (
|
|
||||||
<Portal containerId={ALERT_CONTAINER_ID}>
|
|
||||||
<Alert
|
|
||||||
type="success"
|
|
||||||
message={_("Settings were successfully saved.")}
|
|
||||||
onDismiss={onDismiss}
|
|
||||||
/>
|
|
||||||
</Portal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
FailAlert.propTypes = {
|
|
||||||
onDismiss: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function FailAlert({ onDismiss }) {
|
|
||||||
return (
|
|
||||||
<Portal containerId={ALERT_CONTAINER_ID}>
|
|
||||||
<Alert
|
|
||||||
type="danger"
|
|
||||||
message={_("Settings update was failed.")}
|
|
||||||
onDismiss={onDismiss}
|
|
||||||
/>
|
|
||||||
</Portal>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -50,6 +50,10 @@ export { WebSockets } from "webSockets/WebSockets";
|
||||||
// Utils
|
// Utils
|
||||||
export { Portal } from "utils/Portal";
|
export { Portal } from "utils/Portal";
|
||||||
export { undefinedIfEmpty, withoutUndefinedKeys, onlySpecifiedKeys } from "utils/objectHelpers";
|
export { undefinedIfEmpty, withoutUndefinedKeys, onlySpecifiedKeys } from "utils/objectHelpers";
|
||||||
|
export {
|
||||||
|
withEither, withSpinner, withSending, withSpinnerOnSending, withError, withErrorMessage,
|
||||||
|
} from "utils/conditionalHOCs";
|
||||||
|
export { ErrorMessage } from "utils/ErrorMessage";
|
||||||
|
|
||||||
// Foris URL
|
// Foris URL
|
||||||
export { ForisURLs, REFORIS_URL_PREFIX } from "forisUrls";
|
export { ForisURLs, REFORIS_URL_PREFIX } from "forisUrls";
|
||||||
|
|
16
src/utils/ErrorMessage.js
Normal file
16
src/utils/ErrorMessage.js
Normal file
|
@ -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 (
|
||||||
|
<p className="text-center text-danger">
|
||||||
|
{_("An error occurred while fetching data.")}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`conditional HOCs withError should render error message 1`] = `
|
||||||
|
<div>
|
||||||
|
<p
|
||||||
|
class="text-center text-danger"
|
||||||
|
>
|
||||||
|
An error occurred while fetching data.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`conditional HOCs withErrorMessage should render error message 1`] = `
|
||||||
|
<div>
|
||||||
|
<p
|
||||||
|
class="text-center text-danger"
|
||||||
|
>
|
||||||
|
An error occurred while fetching data.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`conditional HOCs withSpinner should render spinner 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="spinner-wrapper my-3 text-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="spinner-border "
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="sr-only"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="spinner-text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`conditional HOCs withSpinnerOnSending should render spinner 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="spinner-wrapper my-3 text-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="spinner-border "
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="sr-only"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="spinner-text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
110
src/utils/__tests__/conditionalHOCs.test.js
Normal file
110
src/utils/__tests__/conditionalHOCs.test.js
Normal file
|
@ -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 = () => <p>First</p>;
|
||||||
|
const Alternative = () => <p>Alternative</p>;
|
||||||
|
|
||||||
|
describe("withEither", () => {
|
||||||
|
it("should render First component", () => {
|
||||||
|
const withAlternative = withEither(() => false, Alternative);
|
||||||
|
const FirstWithConditional = withAlternative(First);
|
||||||
|
const { getByText } = render(<FirstWithConditional />);
|
||||||
|
expect(getByText("First")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render Alternative component", () => {
|
||||||
|
const withAlternative = withEither(() => true, Alternative);
|
||||||
|
const FirstWithConditional = withAlternative(First);
|
||||||
|
const { getByText } = render(<FirstWithConditional />);
|
||||||
|
expect(getByText("Alternative")).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("withSpinner", () => {
|
||||||
|
it("should render First component", () => {
|
||||||
|
const withSpinnerHidden = withSpinner(() => false);
|
||||||
|
const FirstWithConditional = withSpinnerHidden(First);
|
||||||
|
const { getByText } = render(<FirstWithConditional />);
|
||||||
|
expect(getByText("First")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render spinner", () => {
|
||||||
|
const withSpinnerVisible = withSpinner(() => true);
|
||||||
|
const FirstWithConditional = withSpinnerVisible(First);
|
||||||
|
const { container } = render(<FirstWithConditional />);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("withSending", () => {
|
||||||
|
it("should render First component", () => {
|
||||||
|
const withAlternative = withSending(Alternative);
|
||||||
|
const FirstWithConditional = withAlternative(First);
|
||||||
|
const { getByText } = render(<FirstWithConditional apiState={API_STATE.SUCCESS} />);
|
||||||
|
expect(getByText("First")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render Alternative component", () => {
|
||||||
|
const withAlternative = withSending(Alternative);
|
||||||
|
const FirstWithConditional = withAlternative(First);
|
||||||
|
const { getByText } = render(<FirstWithConditional apiState={API_STATE.SENDING} />);
|
||||||
|
expect(getByText("Alternative")).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("withSpinnerOnSending", () => {
|
||||||
|
it("should render First component", () => {
|
||||||
|
const FirstWithConditional = withSpinnerOnSending(First);
|
||||||
|
const { getByText } = render(<FirstWithConditional apiState={API_STATE.SUCCESS} />);
|
||||||
|
expect(getByText("First")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render spinner", () => {
|
||||||
|
const FirstWithConditional = withSpinnerOnSending(First);
|
||||||
|
const { container } = render(<FirstWithConditional apiState={API_STATE.SENDING} />);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("withError", () => {
|
||||||
|
it("should render First component", () => {
|
||||||
|
const withErrorHidden = withError(() => false);
|
||||||
|
const FirstWithConditional = withErrorHidden(First);
|
||||||
|
const { getByText } = render(<FirstWithConditional />);
|
||||||
|
expect(getByText("First")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render error message", () => {
|
||||||
|
const withErrorVisible = withError(() => true);
|
||||||
|
const FirstWithConditional = withErrorVisible(First);
|
||||||
|
const { container } = render(<FirstWithConditional />);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("withErrorMessage", () => {
|
||||||
|
it("should render First component", () => {
|
||||||
|
const FirstWithConditional = withErrorMessage(First);
|
||||||
|
const { getByText } = render(<FirstWithConditional apiState={API_STATE.SUCCESS} />);
|
||||||
|
expect(getByText("First")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render error message", () => {
|
||||||
|
const FirstWithConditional = withErrorMessage(First);
|
||||||
|
const { container } = render(<FirstWithConditional apiState={API_STATE.ERROR} />);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
52
src/utils/conditionalHOCs.js
Normal file
52
src/utils/conditionalHOCs.js
Normal file
|
@ -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 <Either />;
|
||||||
|
}
|
||||||
|
return <Component {...props} />;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user