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" "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": { "@types/react-dom": {
"version": "16.9.4", "version": "16.9.4",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.4.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.4.tgz",
@ -17676,15 +17668,6 @@
"prop-types": "^15.6.2" "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": { "react-apollo": {
"version": "3.1.5", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/react-apollo/-/react-apollo-3.1.5.tgz", "resolved": "https://registry.npmjs.org/react-apollo/-/react-apollo-3.1.5.tgz",

View file

@ -48,7 +48,6 @@
"moment-timezone": "^0.5.26", "moment-timezone": "^0.5.26",
"qs": "^6.9.0", "qs": "^6.9.0",
"react": "^16.12.0", "react": "^16.12.0",
"react-alert": "^7.0.1",
"react-apollo": "^3.1.4", "react-apollo": "^3.1.4",
"react-dom": "^16.9.0", "react-dom": "^16.9.0",
"react-dropzone": "^8.2.0", "react-dropzone": "^8.2.0",
@ -101,7 +100,6 @@
"@types/react-sortable-hoc": "^0.7.1", "@types/react-sortable-hoc": "^0.7.1",
"@types/react-sortable-tree": "^0.3.6", "@types/react-sortable-tree": "^0.3.6",
"@types/react-test-renderer": "^16.8.2", "@types/react-test-renderer": "^16.8.2",
"@types/react-alert": "^5.2.0",
"@types/semver-compare": "^1.0.1", "@types/semver-compare": "^1.0.1",
"@types/storybook__addon-storyshots": "^3.4.9", "@types/storybook__addon-storyshots": "^3.4.9",
"@types/storybook__react": "^4.0.2", "@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 CloseIcon from "@material-ui/icons/Close";
import classNames from "classnames"; import classNames from "classnames";
import React, { useState } from "react"; import React, { useState } from "react";
import { AlertComponentPropsWithStyle } from "react-alert";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { IMessage } from "./"; import { IMessage, INotification } from "./";
import { useStyles } from "./styles"; import { useStyles } from "./styles";
export interface IMessageManagerProps extends AlertComponentPropsWithStyle { export interface IMessageManagerProps extends INotification {
message: IMessage; message: IMessage;
onMouseEnter: () => void;
onMouseLeave: () => void;
} }
export const MessageManager: React.FC<IMessageManagerProps> = props => { export const MessageManagerTemplate: React.FC<IMessageManagerProps> = props => {
const { const {
close, close,
onMouseEnter,
onMouseLeave,
options: { timeout }, options: { timeout },
message: { actionBtn, expandText, status = "info", title, text, onUndo } message: { actionBtn, expandText, status = "info", title, text, onUndo }
} = props; } = props;
@ -27,7 +30,12 @@ export const MessageManager: React.FC<IMessageManagerProps> = props => {
const classes = useStyles({}); const classes = useStyles({});
return ( return (
<div key={props.id} className={classes.snackbarContainer}> <div
key={props.id}
className={classes.snackbarContainer}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<SnackbarContent <SnackbarContent
id={props.id} id={props.id}
key={props.id} key={props.id}
@ -145,5 +153,3 @@ export const MessageManager: React.FC<IMessageManagerProps> = props => {
</div> </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 { export interface IMessage {
actionBtn?: { actionBtn?: {
label: string; label: string;
@ -9,19 +11,45 @@ export interface IMessage {
title?: string; title?: string;
text: string; text: string;
onUndo?: () => void; onUndo?: () => void;
status?: "success" | "error" | "info" | "warning"; status?: Status;
} }
export const notificationOptions = { export interface IOptions {
containerStyle: { timeout: number;
display: "grid", type?: Status;
gridTemplateRows: "repeat(auto-fill, minmax(90px, 1fr)", }
justifyContent: "end",
zIndex: 1200
},
position: positions.TOP_RIGHT, export interface INotification {
timeout: 3000 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 "./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 successColor = "#60DAA0";
const warningColor = "#FFB84E"; const warningColor = "#FFB84E";
const infoColor = "#CAD8DF"; const infoColor = "#CAD8DF";
const errorColor = "#FE6E76";
export const useStyles = makeStyles( export const useStyles = makeStyles(
theme => ({ theme => ({
@ -32,6 +33,17 @@ export const useStyles = makeStyles(
closeBtnInfo: { closeBtnInfo: {
color: theme.palette.text.primary 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: { error: {
"& > div": { "& > div": {
"& button span": { "& button span": {
@ -43,7 +55,7 @@ export const useStyles = makeStyles(
backgroundImage: `url(${errorIcon})` backgroundImage: `url(${errorIcon})`
} }
}, },
backgroundColor: theme.palette.error.main, backgroundColor: errorColor,
color: "#fff" color: "#fff"
}, },
expandBtn: { expandBtn: {
@ -119,7 +131,7 @@ export const useStyles = makeStyles(
width: "calc(100%)" width: "calc(100%)"
}, },
progressBarError: { progressBarError: {
backgroundColor: darken(theme.palette.error.main, 0.2) backgroundColor: darken(errorColor, 0.2)
}, },
progressBarSuccess: { progressBarSuccess: {
backgroundColor: darken(successColor, 0.2) backgroundColor: darken(successColor, 0.2)
@ -146,6 +158,11 @@ export const useStyles = makeStyles(
paddingTop: 16, paddingTop: 16,
position: "relative" position: "relative"
}, },
"&:hover": {
"& [class*='progressBar']": {
animationPlayState: "paused"
}
},
borderRadius: 4, borderRadius: 4,
paddingBottom: 15, paddingBottom: 15,
paddingRight: 45, paddingRight: 45,

View file

@ -1,11 +1,20 @@
import { IMessage } from "@saleor/components/messages"; 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 { function useNotifier(): UseNotifierResult {
const alert = useAlert(); const notificationContext = useContext(MessageContext);
const notification = useMemo(() => notificationContext.current, [
notificationContext
]);
const notify = (options: IMessage) => { const notify = (options: IMessage) => {
alert.show(options, options.autohide && { timeout: options.autohide }); notification.show(
options,
options.autohide && { timeout: options.autohide }
);
}; };
return notify; return notify;
} }

View file

@ -1,66 +1,65 @@
import { API_URI, APP_MOUNT_URI, GTM_ID } from "./config"; import Navigator from "@saleor/components/Navigator";
import Auth, { getAuthToken, removeAuthToken } from "./auth"; import useAppState from "@saleor/hooks/useAppState";
import { BrowserRouter, Route, Switch } from "react-router-dom"; import { defaultDataIdFromObject, InMemoryCache } from "apollo-cache-inmemory";
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 { ApolloClient } from "apollo-client"; import { ApolloClient } from "apollo-client";
import { ApolloLink } from "apollo-link"; 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 { 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 CategorySection from "./categories";
import CollectionSection from "./collections"; import CollectionSection from "./collections";
import { CustomerSection } from "./customers"; import AppLayout from "./components/AppLayout";
import { DateProvider } from "./components/Date"; 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 { 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 NavigationSection from "./navigation";
import Navigator from "@saleor/components/Navigator"; import { navigationSection } from "./navigation/urls";
import { NotFound } from "./NotFound"; import { NotFound } from "./NotFound";
import OrdersSection from "./orders"; import OrdersSection from "./orders";
import PageSection from "./pages"; import PageSection from "./pages";
import { PermissionEnum } from "./types/globalTypes";
import PermissionGroupSection from "./permissionGroups"; import PermissionGroupSection from "./permissionGroups";
import PluginsSection from "./plugins"; import PluginsSection from "./plugins";
import ProductSection from "./products"; import ProductSection from "./products";
import ProductTypesSection from "./productTypes"; import ProductTypesSection from "./productTypes";
import React from "react";
import SectionRoute from "./auth/components/SectionRoute";
import ServiceSection from "./services"; import ServiceSection from "./services";
import { serviceSection } from "./services/urls";
import ShippingSection from "./shipping"; import ShippingSection from "./shipping";
import { ShopProvider } from "./components/Shop";
import SiteSettingsSection from "./siteSettings"; import SiteSettingsSection from "./siteSettings";
import StaffSection from "./staff"; import StaffSection from "./staff";
import TagManager from "react-gtm-module";
import TaxesSection from "./taxes"; import TaxesSection from "./taxes";
import ThemeProvider from "./components/Theme";
import TranslationsSection from "./translations"; import TranslationsSection from "./translations";
import { PermissionEnum } from "./types/globalTypes";
import WarehouseSection from "./warehouses"; 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 { warehouseSection } from "./warehouses/urls";
import WebhooksSection from "./webhooks";
interface ResponseError extends ErrorResponse { interface ResponseError extends ErrorResponse {
networkError?: Error & { networkError?: Error & {
@ -136,7 +135,7 @@ const App: React.FC = () => {
<ThemeProvider isDefaultDark={isDark}> <ThemeProvider isDefaultDark={isDark}>
<DateProvider> <DateProvider>
<LocaleProvider> <LocaleProvider>
<AlertProvider {...notificationOptions} template={MessageManager}> <MessageManagerProvider>
<BackgroundTasksProvider> <BackgroundTasksProvider>
<AppStateProvider> <AppStateProvider>
<ShopProvider> <ShopProvider>
@ -144,7 +143,7 @@ const App: React.FC = () => {
</ShopProvider> </ShopProvider>
</AppStateProvider> </AppStateProvider>
</BackgroundTasksProvider> </BackgroundTasksProvider>
</AlertProvider> </MessageManagerProvider>
</LocaleProvider> </LocaleProvider>
</DateProvider> </DateProvider>
</ThemeProvider> </ThemeProvider>

View file

@ -1,10 +1,9 @@
import { Locale, RawLocaleProvider } from "@saleor/components/Locale"; import { Locale, RawLocaleProvider } from "@saleor/components/Locale";
import React from "react"; import React from "react";
import { Provider as AlertProvider } from "react-alert";
import { IntlProvider } from "react-intl"; import { IntlProvider } from "react-intl";
import { Provider as DateProvider } from "../components/Date/DateContext"; 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 ThemeProvider from "../components/Theme";
import { TimezoneProvider } from "../components/Timezone"; import { TimezoneProvider } from "../components/Timezone";
@ -19,7 +18,7 @@ export const Decorator = storyFn => (
<DateProvider value={+new Date("2018-08-07T14:30:44+00:00")}> <DateProvider value={+new Date("2018-08-07T14:30:44+00:00")}>
<TimezoneProvider value="America/New_York"> <TimezoneProvider value="America/New_York">
<ThemeProvider isDefaultDark={false}> <ThemeProvider isDefaultDark={false}>
<AlertProvider {...notificationOptions} template={MessageManager}> <MessageManagerProvider>
<div <div
style={{ style={{
padding: 24 padding: 24
@ -27,7 +26,7 @@ export const Decorator = storyFn => (
> >
{storyFn()} {storyFn()}
</div> </div>
</AlertProvider> </MessageManagerProvider>
</ThemeProvider> </ThemeProvider>
</TimezoneProvider> </TimezoneProvider>
</DateProvider> </DateProvider>