diff --git a/package-lock.json b/package-lock.json
index 0db110917..816a78f27 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3587,14 +3587,6 @@
"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",
@@ -17676,15 +17668,6 @@
"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",
diff --git a/package.json b/package.json
index 7bb7e173a..9c25281c6 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,6 @@
"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",
@@ -101,7 +100,6 @@
"@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",
diff --git a/src/components/messages/Container.tsx b/src/components/messages/Container.tsx
new file mode 100644
index 000000000..c45cece2c
--- /dev/null
+++ b/src/components/messages/Container.tsx
@@ -0,0 +1,12 @@
+import React from "react";
+
+import { useStyles } from "./styles";
+
+const Container = ({ children }) => {
+ const classes = useStyles({});
+ return (
+ !!children.length &&
{children}
+ );
+};
+
+export default Container;
diff --git a/src/components/messages/MessageManager.tsx b/src/components/messages/MessageManager.tsx
index 1ea25d2c9..aee7b651e 100644
--- a/src/components/messages/MessageManager.tsx
+++ b/src/components/messages/MessageManager.tsx
@@ -5,19 +5,22 @@ 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 } from "./";
+import { IMessage, INotification } from "./";
import { useStyles } from "./styles";
-export interface IMessageManagerProps extends AlertComponentPropsWithStyle {
+export interface IMessageManagerProps extends INotification {
message: IMessage;
+ onMouseEnter: () => void;
+ onMouseLeave: () => void;
}
-export const MessageManager: React.FC = props => {
+export const MessageManagerTemplate: React.FC = props => {
const {
close,
+ onMouseEnter,
+ onMouseLeave,
options: { timeout },
message: { actionBtn, expandText, status = "info", title, text, onUndo }
} = props;
@@ -27,7 +30,12 @@ export const MessageManager: React.FC = props => {
const classes = useStyles({});
return (
-
+
= props => {
);
};
-
-export default MessageManager;
diff --git a/src/components/messages/MessageManagerProvider.tsx b/src/components/messages/MessageManagerProvider.tsx
new file mode 100644
index 000000000..769df0802
--- /dev/null
+++ b/src/components/messages/MessageManagerProvider.tsx
@@ -0,0 +1,156 @@
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import { createPortal } from "react-dom";
+import { TransitionGroup } from "react-transition-group";
+
+import {
+ INotification,
+ INotificationContext,
+ ITimer,
+ MessageContext,
+ MessageManagerTemplate,
+ types
+} from ".";
+import Container from "./Container";
+import Transition from "./Transition";
+
+const containerStyle = {
+ display: "grid",
+ gridTemplateRows: "repeat(auto-fill, minmax(90px, 1fr)",
+ justifyContent: "end",
+ zIndex: 1200
+};
+
+const MessageManagerProvider = ({ children }) => {
+ const root = useRef(null);
+ const notificationContext = useRef
(null);
+ const timersArr = useRef([]);
+ const [notifications, setNotifications] = useState([]);
+
+ useEffect(() => {
+ root.current = document.createElement("div");
+ root.current.id = "__message-manager__";
+ document.body.appendChild(root.current);
+ const timersArrRef = timersArr.current;
+
+ return () => {
+ timersArrRef.forEach(timer => clearTimeout(timer.timeoutId));
+ if (root.current) {
+ document.body.removeChild(root.current);
+ }
+ };
+ }, []);
+
+ const timerCallback = (notification: INotification) => {
+ remove(notification);
+ timersArr.current = timersArr.current.filter(
+ timer => timer.id !== notification.id
+ );
+ };
+
+ const remove = useCallback(notification => {
+ setNotifications(currentNotifications =>
+ currentNotifications.filter(n => n.id !== notification.id)
+ );
+ }, []);
+
+ const show = useCallback(
+ (message = {}, options = {}) => {
+ const id = Math.random()
+ .toString(36)
+ .substr(2, 9);
+
+ const notificationOptions = {
+ timeout: 4000,
+ type: types.INFO,
+ ...options
+ };
+
+ const notification = {
+ close: () => remove(alert),
+ id,
+ message,
+ options: notificationOptions
+ };
+
+ const {
+ options: { timeout }
+ } = notification;
+
+ const timeoutId = window.setTimeout(() => {
+ timerCallback(notification);
+ }, timeout);
+
+ timersArr.current.push({
+ id: notification.id,
+ notification,
+ remaining: timeout,
+ start: new Date().getTime(),
+ timeoutId
+ });
+
+ setNotifications(state => [notification, ...state]);
+
+ return notification;
+ },
+ [remove]
+ );
+
+ const getCurrentTimer = (notification: INotification) => {
+ const currentTimerIndex = timersArr.current.findIndex(
+ timer => timer.id === notification.id
+ );
+ return timersArr.current[currentTimerIndex];
+ };
+
+ const pauseTimer = (notification: INotification) => {
+ const currentTimer = getCurrentTimer(notification);
+ if (currentTimer) {
+ currentTimer.remaining =
+ currentTimer.remaining - (new Date().getTime() - currentTimer.start);
+ window.clearTimeout(currentTimer.timeoutId);
+ }
+ };
+ const resumeTimer = (notification: INotification) => {
+ const currentTimer = getCurrentTimer(notification);
+ if (currentTimer) {
+ currentTimer.start = new Date().getTime();
+ currentTimer.timeoutId = window.setTimeout(
+ () => timerCallback(notification),
+ currentTimer.remaining
+ );
+ }
+ };
+
+ notificationContext.current = {
+ remove,
+ show
+ };
+
+ return (
+
+ {children}
+ {root.current &&
+ createPortal(
+
+ {!!notifications.length &&
+ notifications.map(notification => (
+
+ pauseTimer(notification)}
+ onMouseLeave={() => resumeTimer(notification)}
+ {...notification}
+ />
+
+ ))}
+ ,
+ root.current
+ )}
+
+ );
+};
+
+export default MessageManagerProvider;
diff --git a/src/components/messages/Transition.tsx b/src/components/messages/Transition.tsx
new file mode 100644
index 000000000..e14c12483
--- /dev/null
+++ b/src/components/messages/Transition.tsx
@@ -0,0 +1,31 @@
+import React from "react";
+import { Transition as MessageManagerTransition } from "react-transition-group";
+
+const duration = 250;
+
+const defaultStyle = {
+ opacity: 0,
+ transition: `opacity ${duration}ms ease`
+};
+
+const transitionStyles = {
+ entered: { opacity: 1 },
+ entering: { opacity: 0 }
+};
+
+const Transition = ({ children, ...props }) => (
+
+ {state => (
+
+ {children}
+
+ )}
+
+);
+
+export default Transition;
diff --git a/src/components/messages/index.ts b/src/components/messages/index.ts
index 9e98a4d2c..84cd6265a 100644
--- a/src/components/messages/index.ts
+++ b/src/components/messages/index.ts
@@ -1,4 +1,6 @@
-import { positions } from "react-alert";
+import { createContext } from "react";
+
+export type Status = "success" | "error" | "info" | "warning";
export interface IMessage {
actionBtn?: {
label: string;
@@ -9,19 +11,45 @@ export interface IMessage {
title?: string;
text: string;
onUndo?: () => void;
- status?: "success" | "error" | "info" | "warning";
+ status?: Status;
}
-export const notificationOptions = {
- containerStyle: {
- display: "grid",
- gridTemplateRows: "repeat(auto-fill, minmax(90px, 1fr)",
- justifyContent: "end",
- zIndex: 1200
- },
+export interface IOptions {
+ timeout: number;
+ type?: Status;
+}
- position: positions.TOP_RIGHT,
- timeout: 3000
+export interface INotification {
+ id: string;
+ message: IMessage;
+ options: IOptions;
+ close: () => void;
+}
+
+export interface ITimer {
+ id: string;
+ notification: INotification;
+ remaining: number;
+ start: number;
+ timeoutId: number;
+}
+
+export const types = {
+ ERROR: "error",
+ INFO: "info",
+ SUCCESS: "success",
+ WARNING: "warning"
};
+export interface INotificationContext {
+ show: (message: IMessage, options?: IOptions) => void;
+ remove: (notification: INotification) => void;
+}
+
+export type IMessageContext = (message: IMessage) => void;
+export const MessageContext = createContext<
+ React.MutableRefObject
+>(null);
export * from "./MessageManager";
+export * from "./MessageManagerProvider";
+export { default } from "./MessageManagerProvider";
diff --git a/src/components/messages/styles.ts b/src/components/messages/styles.ts
index 2375c0a5c..f15716624 100644
--- a/src/components/messages/styles.ts
+++ b/src/components/messages/styles.ts
@@ -8,6 +8,7 @@ import { darken } from "@material-ui/core/styles/colorManipulator";
const successColor = "#60DAA0";
const warningColor = "#FFB84E";
const infoColor = "#CAD8DF";
+const errorColor = "#FE6E76";
export const useStyles = makeStyles(
theme => ({
@@ -32,6 +33,17 @@ export const useStyles = makeStyles(
closeBtnInfo: {
color: theme.palette.text.primary
},
+ container: {
+ alignItems: "flex-end",
+ display: "flex",
+ flexDirection: "column",
+ justifyContent: "center",
+ left: 0,
+ pointerEvents: "none",
+ position: "fixed",
+ top: 0,
+ width: "100%"
+ },
error: {
"& > div": {
"& button span": {
@@ -43,7 +55,7 @@ export const useStyles = makeStyles(
backgroundImage: `url(${errorIcon})`
}
},
- backgroundColor: theme.palette.error.main,
+ backgroundColor: errorColor,
color: "#fff"
},
expandBtn: {
@@ -119,7 +131,7 @@ export const useStyles = makeStyles(
width: "calc(100%)"
},
progressBarError: {
- backgroundColor: darken(theme.palette.error.main, 0.2)
+ backgroundColor: darken(errorColor, 0.2)
},
progressBarSuccess: {
backgroundColor: darken(successColor, 0.2)
@@ -146,6 +158,11 @@ export const useStyles = makeStyles(
paddingTop: 16,
position: "relative"
},
+ "&:hover": {
+ "& [class*='progressBar']": {
+ animationPlayState: "paused"
+ }
+ },
borderRadius: 4,
paddingBottom: 15,
paddingRight: 45,
diff --git a/src/hooks/useNotifier.ts b/src/hooks/useNotifier.ts
index d5e3ed55f..a6388f7dd 100644
--- a/src/hooks/useNotifier.ts
+++ b/src/hooks/useNotifier.ts
@@ -1,11 +1,20 @@
import { IMessage } from "@saleor/components/messages";
-import { useAlert } from "react-alert";
+import { IMessageContext, MessageContext } from "@saleor/components/messages";
+import { useContext, useMemo } from "react";
+
+export type UseNotifierResult = IMessageContext;
-export type UseNotifierResult = (options: IMessage) => void;
function useNotifier(): UseNotifierResult {
- const alert = useAlert();
+ const notificationContext = useContext(MessageContext);
+ const notification = useMemo(() => notificationContext.current, [
+ notificationContext
+ ]);
+
const notify = (options: IMessage) => {
- alert.show(options, options.autohide && { timeout: options.autohide });
+ notification.show(
+ options,
+ options.autohide && { timeout: options.autohide }
+ );
};
return notify;
}
diff --git a/src/index.tsx b/src/index.tsx
index 211cfa7b7..9cc19d811 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,66 +1,65 @@
-import { API_URI, APP_MOUNT_URI, GTM_ID } from "./config";
-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 { MessageManager, notificationOptions } from "./components/messages";
-
-import { Provider as AlertProvider } from "react-alert";
+import Navigator from "@saleor/components/Navigator";
+import useAppState from "@saleor/hooks/useAppState";
+import { defaultDataIdFromObject, InMemoryCache } from "apollo-cache-inmemory";
import { ApolloClient } from "apollo-client";
import { ApolloLink } from "apollo-link";
-import { ApolloProvider } from "react-apollo";
-import AppLayout from "./components/AppLayout";
-import AppStateProvider from "./containers/AppState";
-import AttributeSection from "./attributes";
-import AuthProvider from "./auth/AuthProvider";
-import BackgroundTasksProvider from "./containers/BackgroundTasks";
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 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 CategorySection from "./categories";
import CollectionSection from "./collections";
-import { CustomerSection } from "./customers";
+import AppLayout from "./components/AppLayout";
import { DateProvider } from "./components/Date";
-import DiscountSection from "./discounts";
-import ErrorBoundary from "react-error-boundary";
-import HomePage from "./home";
import { LocaleProvider } from "./components/Locale";
-import LoginLoading from "./auth/components/LoginLoading/LoginLoading";
+import MessageManagerProvider 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 DiscountSection from "./discounts";
+import HomePage from "./home";
+import { commonMessages } from "./intl";
import NavigationSection from "./navigation";
-import Navigator from "@saleor/components/Navigator";
+import { navigationSection } from "./navigation/urls";
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 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";
+import WebhooksSection from "./webhooks";
interface ResponseError extends ErrorResponse {
networkError?: Error & {
@@ -136,7 +135,7 @@ const App: React.FC = () => {
-
+
@@ -144,7 +143,7 @@ const App: React.FC = () => {
-
+
diff --git a/src/storybook/Decorator.tsx b/src/storybook/Decorator.tsx
index 9f8426de2..66e548747 100644
--- a/src/storybook/Decorator.tsx
+++ b/src/storybook/Decorator.tsx
@@ -1,10 +1,9 @@
import { Locale, RawLocaleProvider } from "@saleor/components/Locale";
import React from "react";
-import { Provider as AlertProvider } from "react-alert";
import { IntlProvider } from "react-intl";
import { Provider as DateProvider } from "../components/Date/DateContext";
-import { MessageManager, notificationOptions } from "../components/messages";
+import MessageManagerProvider from "../components/messages";
import ThemeProvider from "../components/Theme";
import { TimezoneProvider } from "../components/Timezone";
@@ -19,7 +18,7 @@ export const Decorator = storyFn => (
-
+
(
>
{storyFn()}
-
+