From adebcc445221a02c3db61eeac4c7a9370409fa38 Mon Sep 17 00:00:00 2001 From: AlicjaSzu Date: Tue, 30 Jun 2020 19:41:43 +0200 Subject: [PATCH] remove react-alert lib, create MessageManagerProvider --- package-lock.json | 17 -- package.json | 2 - src/components/messages/Container.tsx | 12 ++ src/components/messages/MessageManager.tsx | 20 ++- .../messages/MessageManagerProvider.tsx | 156 ++++++++++++++++++ src/components/messages/Transition.tsx | 31 ++++ src/components/messages/index.ts | 50 ++++-- src/components/messages/styles.ts | 21 ++- src/hooks/useNotifier.ts | 17 +- src/index.tsx | 83 +++++----- src/storybook/Decorator.tsx | 7 +- 11 files changed, 327 insertions(+), 89 deletions(-) create mode 100644 src/components/messages/Container.tsx create mode 100644 src/components/messages/MessageManagerProvider.tsx create mode 100644 src/components/messages/Transition.tsx 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()}
-
+