use react-alert to render multiple notifications

This commit is contained in:
AlicjaSzu 2020-06-24 13:44:35 +02:00
parent 49df188703
commit 58c0a5859a
8 changed files with 155 additions and 175 deletions

58
package-lock.json generated
View file

@ -3587,6 +3587,14 @@
"csstype": "^2.2.0"
}
},
"@types/react-alert": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@types/react-alert/-/react-alert-5.2.0.tgz",
"integrity": "sha512-1cyu/vQ4X0CJVC3X4CrJHRBc/9BvTqOF9FH6iy/4TLwPng++GC4ftxS/UaM91FqsKjao7pPtqxUlW1cRafNHLw==",
"requires": {
"@types/react": "*"
}
},
"@types/react-dom": {
"version": "16.9.4",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.4.tgz",
@ -11582,8 +11590,7 @@
},
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"optional": true
"bundled": true
},
"aproba": {
"version": "1.2.0",
@ -11601,13 +11608,11 @@
},
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"optional": true
"bundled": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -11620,18 +11625,15 @@
},
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"optional": true
"bundled": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"optional": true
"bundled": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"optional": true
"bundled": true
},
"core-util-is": {
"version": "1.0.2",
@ -11734,8 +11736,7 @@
},
"inherits": {
"version": "2.0.3",
"bundled": true,
"optional": true
"bundled": true
},
"ini": {
"version": "1.3.5",
@ -11745,7 +11746,6 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -11758,20 +11758,17 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"bundled": true,
"optional": true
"bundled": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -11788,7 +11785,6 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -11861,8 +11857,7 @@
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"optional": true
"bundled": true
},
"object-assign": {
"version": "4.1.1",
@ -11872,7 +11867,6 @@
"once": {
"version": "1.4.0",
"bundled": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -11948,8 +11942,7 @@
},
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"optional": true
"bundled": true
},
"safer-buffer": {
"version": "2.1.2",
@ -11979,7 +11972,6 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -11997,7 +11989,6 @@
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -12036,13 +12027,11 @@
},
"wrappy": {
"version": "1.0.2",
"bundled": true,
"optional": true
"bundled": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
"optional": true
"bundled": true
}
}
},
@ -17687,6 +17676,15 @@
"prop-types": "^15.6.2"
}
},
"react-alert": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/react-alert/-/react-alert-7.0.1.tgz",
"integrity": "sha512-pHtMtP8gFQ4hLVQ/j7HIaayjvVNoMgoZHCag8YYBUpsLcamj0+435PJdZR7oh7AVbW0C2JPvNmS5+Md1Y59oew==",
"requires": {
"prop-types": "^15.7.2",
"react-transition-group": "^4.3.0"
}
},
"react-apollo": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/react-apollo/-/react-apollo-3.1.5.tgz",

View file

@ -48,6 +48,7 @@
"moment-timezone": "^0.5.26",
"qs": "^6.9.0",
"react": "^16.12.0",
"react-alert": "^7.0.1",
"react-apollo": "^3.1.4",
"react-dom": "^16.9.0",
"react-dropzone": "^8.2.0",
@ -100,6 +101,7 @@
"@types/react-sortable-hoc": "^0.7.1",
"@types/react-sortable-tree": "^0.3.6",
"@types/react-test-renderer": "^16.8.2",
"@types/react-alert": "^5.2.0",
"@types/semver-compare": "^1.0.1",
"@types/storybook__addon-storyshots": "^3.4.9",
"@types/storybook__react": "^4.0.2",

View file

@ -1,91 +1,37 @@
import Button from "@material-ui/core/Button";
import IconButton from "@material-ui/core/IconButton";
import Snackbar from "@material-ui/core/Snackbar";
import SnackbarContent from "@material-ui/core/SnackbarContent";
import Typography from "@material-ui/core/Typography";
import CloseIcon from "@material-ui/icons/Close";
import classNames from "classnames";
import React, { useState } from "react";
import { AlertComponentPropsWithStyle } from "react-alert";
import { FormattedMessage } from "react-intl";
import { IMessage, MessageContext } from "./";
import { IMessage } from "./";
import { useStyles } from "./styles";
interface Message extends IMessage {
key: string;
export interface IMessageManagerProps extends AlertComponentPropsWithStyle {
message: IMessage;
}
export const MessageManager = props => {
const { children } = props;
export const MessageManager: React.FC<IMessageManagerProps> = props => {
const {
close,
options: { timeout },
message: { actionBtn, expandText, status, title, text, onUndo }
} = props;
const [message, setMessage] = useState<Message>({
key: "0",
onUndo: undefined,
status: "info",
text: ""
});
const [opened, setOpened] = useState(false);
const [expand, setExpand] = useState(false);
const classes = useStyles({});
const {
actionBtn,
autohide = 9000,
expandText,
title,
text,
key,
onUndo,
status = "info"
} = message;
const queue = [];
const handleClose = (_, reason) => {
if (reason === "clickaway") {
return;
}
setOpened(false);
};
const processQueue = () => {
if (queue.length > 0) {
setMessage(queue.shift());
setOpened(true);
}
};
const handleExited = () => {
processQueue();
};
const pushMessage = (message: IMessage) => {
queue.push({
key: new Date().getTime(),
...message
});
if (opened) {
setOpened(false);
} else {
processQueue();
}
};
return (
<>
<Snackbar
key={key}
anchorOrigin={{
horizontal: "right",
vertical: "top"
}}
open={opened}
autoHideDuration={autohide}
onClose={handleClose}
onExited={handleExited}
ContentProps={{
"aria-describedby": "message-id"
}}
<div key={props.id} className={classes.snackbarContainer}>
<SnackbarContent
id={props.id}
key={props.id}
aria-describedby="message-id"
className={classNames(classes.snackbar, {
[classes.error]: status === "error",
[classes.success]: status === "success",
@ -105,10 +51,10 @@ export const MessageManager = props => {
</Typography>
</span>
}
title={title}
action={[
!!expandText ? (
<div
key="expanded"
className={classNames(classes.expandedContainer, {
[classes.expandedContainerInfo]: status === "info"
})}
@ -151,7 +97,7 @@ export const MessageManager = props => {
key="undo"
color="default"
size="small"
onClick={handleClose as any}
onClick={close}
data-tc="button-undo"
>
<FormattedMessage
@ -176,7 +122,7 @@ export const MessageManager = props => {
key="close"
aria-label="Close"
color="inherit"
onClick={handleClose as any}
onClick={close}
className={classNames(classes.closeBtn, {
[classes.closeBtnInfo]: status === "info"
})}
@ -186,20 +132,17 @@ export const MessageManager = props => {
<div className={classes.progressBarContainer} key="progressBar">
<div
className={classNames(classes.progressBar, {
[classes.progressBarActive]: opened,
[classes.progressBarActive]: true,
[classes.progressBarSuccess]: status === "success",
[classes.progressBarWarning]: status === "warning",
[classes.progressBarError]: status === "error"
})}
style={{ ["--animationTime" as any]: `${autohide}ms` }}
style={{ ["--animationTime" as any]: `${timeout}ms` }}
/>
</div>
]}
/>
<MessageContext.Provider value={pushMessage}>
{children}
</MessageContext.Provider>
</>
</div>
);
};

View file

@ -36,13 +36,15 @@ export const useStyles = makeStyles(
"& > div": {
"& button span": {
color: "#fff"
},
}
},
"& > div:first-child": {
"&:before": {
backgroundImage: `url(${errorIcon})`
},
backgroundColor: theme.palette.error.main,
color: "#fff"
}
}
},
backgroundColor: theme.palette.error.main,
color: "#fff"
},
expandBtn: {
"&:before": {
@ -122,6 +124,9 @@ export const useStyles = makeStyles(
},
snackbar: {
"& > div": {
paddingLeft: 60
},
"& > div:first-child": {
"&:before": {
backgroundImage: `url(${infoIcon})`,
backgroundRepeat: "no-repeat",
@ -134,22 +139,30 @@ export const useStyles = makeStyles(
top: 13,
width: 32
},
paddingLeft: 60,
paddingTop: 10,
position: "relative"
}
},
borderRadius: 4
},
snackbarContainer: {
borderRadius: 4,
display: "block",
margin: theme.spacing(2, 2, 0, 0),
maxWidth: 480,
pointerEvents: "all",
position: "relative"
},
success: {
"& > div": {
"& button span": {
color: "#fff"
},
"& > div:first-child": {
"&:before": {
backgroundImage: `url(${successIcon})`
},
backgroundColor: successColor,
}
},
"& button span": {
color: "#fff"
}
},
backgroundColor: successColor,
color: "#fff"
},
text: {
color: "#fff",
@ -159,16 +172,16 @@ export const useStyles = makeStyles(
paddingTop: 5
},
warning: {
"& > div": {
"& button span": {
color: "#fff"
},
"& > div:first-child": {
"&:before": {
backgroundImage: `url(${warningIcon})`
},
backgroundColor: warningColor,
}
},
"& button span": {
color: "#fff"
}
},
backgroundColor: warningColor,
color: "#fff"
}
}),
{ name: "MessageManager" }

View file

@ -1,9 +1,12 @@
import { IMessageContext, MessageContext } from "@saleor/components/messages";
import { useContext } from "react";
import { IMessage } from "@saleor/components/messages";
import { useAlert } from "react-alert";
export type UseNotifierResult = IMessageContext;
export type UseNotifierResult = (options: IMessage) => void;
function useNotifier(): UseNotifierResult {
const notify = useContext(MessageContext);
const alert = useAlert();
const notify = (options: IMessage) => {
alert.show(options, options.autohide && { timeout: options.autohide });
};
return notify;
}
export default useNotifier;

View file

@ -1,65 +1,66 @@
import Navigator from "@saleor/components/Navigator";
import useAppState from "@saleor/hooks/useAppState";
import { defaultDataIdFromObject, InMemoryCache } from "apollo-cache-inmemory";
import { API_URI, APP_MOUNT_URI, GTM_ID } from "./config";
import { Provider as AlertProvider, positions } from "react-alert";
import Auth, { getAuthToken, removeAuthToken } from "./auth";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import ConfigurationSection, { createConfigurationMenu } from "./configuration";
import { ErrorResponse, onError } from "apollo-link-error";
import { InMemoryCache, defaultDataIdFromObject } from "apollo-cache-inmemory";
import { ApolloClient } from "apollo-client";
import { ApolloLink } from "apollo-link";
import { BatchHttpLink } from "apollo-link-batch-http";
import { setContext } from "apollo-link-context";
import { ErrorResponse, onError } from "apollo-link-error";
import { createUploadLink } from "apollo-upload-client";
import React from "react";
import { ApolloProvider } from "react-apollo";
import { render } from "react-dom";
import ErrorBoundary from "react-error-boundary";
import TagManager from "react-gtm-module";
import { useIntl } from "react-intl";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import AppLayout from "./components/AppLayout";
import AppStateProvider from "./containers/AppState";
import AttributeSection from "./attributes";
import { attributeSection } from "./attributes/urls";
import Auth, { getAuthToken, removeAuthToken } from "./auth";
import AuthProvider from "./auth/AuthProvider";
import LoginLoading from "./auth/components/LoginLoading/LoginLoading";
import SectionRoute from "./auth/components/SectionRoute";
import { isJwtError } from "./auth/errors";
import { hasPermission } from "./auth/misc";
import BackgroundTasksProvider from "./containers/BackgroundTasks";
import { BatchHttpLink } from "apollo-link-batch-http";
import CategorySection from "./categories";
import CollectionSection from "./collections";
import AppLayout from "./components/AppLayout";
import { DateProvider } from "./components/Date";
import { LocaleProvider } from "./components/Locale";
import { MessageManager } from "./components/messages";
import { ShopProvider } from "./components/Shop";
import ThemeProvider from "./components/Theme";
import { WindowTitle } from "./components/WindowTitle";
import { API_URI, APP_MOUNT_URI, GTM_ID } from "./config";
import ConfigurationSection, { createConfigurationMenu } from "./configuration";
import AppStateProvider from "./containers/AppState";
import BackgroundTasksProvider from "./containers/BackgroundTasks";
import { CustomerSection } from "./customers";
import { DateProvider } from "./components/Date";
import DiscountSection from "./discounts";
import ErrorBoundary from "react-error-boundary";
import HomePage from "./home";
import { commonMessages } from "./intl";
import { LocaleProvider } from "./components/Locale";
import LoginLoading from "./auth/components/LoginLoading/LoginLoading";
import { MessageManager } from "./components/messages";
import NavigationSection from "./navigation";
import { navigationSection } from "./navigation/urls";
import Navigator from "@saleor/components/Navigator";
import { NotFound } from "./NotFound";
import OrdersSection from "./orders";
import PageSection from "./pages";
import { PermissionEnum } from "./types/globalTypes";
import PermissionGroupSection from "./permissionGroups";
import PluginsSection from "./plugins";
import ProductSection from "./products";
import ProductTypesSection from "./productTypes";
import React from "react";
import SectionRoute from "./auth/components/SectionRoute";
import ServiceSection from "./services";
import { serviceSection } from "./services/urls";
import ShippingSection from "./shipping";
import { ShopProvider } from "./components/Shop";
import SiteSettingsSection from "./siteSettings";
import StaffSection from "./staff";
import TagManager from "react-gtm-module";
import TaxesSection from "./taxes";
import ThemeProvider from "./components/Theme";
import TranslationsSection from "./translations";
import { PermissionEnum } from "./types/globalTypes";
import WarehouseSection from "./warehouses";
import { warehouseSection } from "./warehouses/urls";
import WebhooksSection from "./webhooks";
import { WindowTitle } from "./components/WindowTitle";
import { attributeSection } from "./attributes/urls";
import { commonMessages } from "./intl";
import { createUploadLink } from "apollo-upload-client";
import { hasPermission } from "./auth/misc";
import { isJwtError } from "./auth/errors";
import { navigationSection } from "./navigation/urls";
import { render } from "react-dom";
import { serviceSection } from "./services/urls";
import { setContext } from "apollo-link-context";
import useAppState from "@saleor/hooks/useAppState";
import { useIntl } from "react-intl";
import { warehouseSection } from "./warehouses/urls";
interface ResponseError extends ErrorResponse {
networkError?: Error & {
@ -126,6 +127,15 @@ const apolloClient = new ApolloClient({
link: invalidTokenLink.concat(authLink.concat(link))
});
const notificationOptions = {
containerStyle: {
zIndex: 1000
},
offset: "20px",
position: positions.TOP_RIGHT,
timeout: 3000
};
const App: React.FC = () => {
const isDark = localStorage.getItem("theme") === "true";
@ -135,7 +145,7 @@ const App: React.FC = () => {
<ThemeProvider isDefaultDark={isDark}>
<DateProvider>
<LocaleProvider>
<MessageManager>
<AlertProvider {...notificationOptions} template={MessageManager}>
<BackgroundTasksProvider>
<AppStateProvider>
<ShopProvider>
@ -143,7 +153,7 @@ const App: React.FC = () => {
</ShopProvider>
</AppStateProvider>
</BackgroundTasksProvider>
</MessageManager>
</AlertProvider>
</LocaleProvider>
</DateProvider>
</ThemeProvider>

View file

@ -7,6 +7,16 @@ import { MessageManager } from "../components/messages";
import ThemeProvider from "../components/Theme";
import { TimezoneProvider } from "../components/Timezone";
const messageProps = {
close: () => undefined,
id: "id",
message: {
text: "Test"
},
options: {},
style: {}
};
export const Decorator = storyFn => (
<IntlProvider defaultLocale={Locale.EN} locale={Locale.EN}>
<RawLocaleProvider
@ -18,7 +28,7 @@ export const Decorator = storyFn => (
<DateProvider value={+new Date("2018-08-07T14:30:44+00:00")}>
<TimezoneProvider value="America/New_York">
<ThemeProvider isDefaultDark={false}>
<MessageManager>
<MessageManager {...messageProps}>
<div
style={{
padding: 24

View file

@ -357,7 +357,8 @@ export default (colors: IThemeColors): Theme =>
"0 6px 10px 0px rgba(0, 0, 0, 0.15), 0 1px 18px 0px rgba(0, 0, 0, 0.12), 0 3px 5px -1px rgba(0, 0, 0, 0.10)",
color: colors.font.default,
display: "block",
maxWidth: 480
maxWidth: 480,
padding: 0
}
},
MuiSwitch: {