remove react-alert lib, create MessageManagerProvider

This commit is contained in:
AlicjaSzu 2020-06-30 19:41:43 +02:00
parent 5960eb4644
commit adebcc4452
11 changed files with 327 additions and 89 deletions

17
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -0,0 +1,12 @@
import React from "react";
import { useStyles } from "./styles";
const Container = ({ children }) => {
const classes = useStyles({});
return (
!!children.length && <div className={classes.container}>{children}</div>
);
};
export default Container;

View file

@ -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<IMessageManagerProps> = props => {
export const MessageManagerTemplate: React.FC<IMessageManagerProps> = 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<IMessageManagerProps> = props => {
const classes = useStyles({});
return (
<div key={props.id} className={classes.snackbarContainer}>
<div
key={props.id}
className={classes.snackbarContainer}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<SnackbarContent
id={props.id}
key={props.id}
@ -145,5 +153,3 @@ export const MessageManager: React.FC<IMessageManagerProps> = props => {
</div>
);
};
export default MessageManager;

View file

@ -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<INotificationContext>(null);
const timersArr = useRef<ITimer[]>([]);
const [notifications, setNotifications] = useState<INotification[]>([]);
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 (
<MessageContext.Provider value={notificationContext}>
{children}
{root.current &&
createPortal(
<TransitionGroup
appear
options={{ containerStyle, position: "top right" }}
component={Container}
>
{!!notifications.length &&
notifications.map(notification => (
<Transition key={notification.id}>
<MessageManagerTemplate
onMouseEnter={() => pauseTimer(notification)}
onMouseLeave={() => resumeTimer(notification)}
{...notification}
/>
</Transition>
))}
</TransitionGroup>,
root.current
)}
</MessageContext.Provider>
);
};
export default MessageManagerProvider;

View file

@ -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 }) => (
<MessageManagerTransition {...props} timeout={duration}>
{state => (
<div
style={{
...defaultStyle,
...transitionStyles[state]
}}
>
{children}
</div>
)}
</MessageManagerTransition>
);
export default Transition;

View file

@ -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<INotificationContext>
>(null);
export * from "./MessageManager";
export * from "./MessageManagerProvider";
export { default } from "./MessageManagerProvider";

View file

@ -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,

View file

@ -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;
}

View file

@ -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 = () => {
<ThemeProvider isDefaultDark={isDark}>
<DateProvider>
<LocaleProvider>
<AlertProvider {...notificationOptions} template={MessageManager}>
<MessageManagerProvider>
<BackgroundTasksProvider>
<AppStateProvider>
<ShopProvider>
@ -144,7 +143,7 @@ const App: React.FC = () => {
</ShopProvider>
</AppStateProvider>
</BackgroundTasksProvider>
</AlertProvider>
</MessageManagerProvider>
</LocaleProvider>
</DateProvider>
</ThemeProvider>

View file

@ -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 => (
<DateProvider value={+new Date("2018-08-07T14:30:44+00:00")}>
<TimezoneProvider value="America/New_York">
<ThemeProvider isDefaultDark={false}>
<AlertProvider {...notificationOptions} template={MessageManager}>
<MessageManagerProvider>
<div
style={{
padding: 24
@ -27,7 +26,7 @@ export const Decorator = storyFn => (
>
{storyFn()}
</div>
</AlertProvider>
</MessageManagerProvider>
</ThemeProvider>
</TimezoneProvider>
</DateProvider>