diff --git a/src/auth/components/SectionRoute.tsx b/src/auth/components/SectionRoute.tsx index 840ae2f89..f1b2d1546 100644 --- a/src/auth/components/SectionRoute.tsx +++ b/src/auth/components/SectionRoute.tsx @@ -1,10 +1,6 @@ import React from "react"; -import ErrorBoundary from "react-error-boundary"; import { Route, RouteProps } from "react-router-dom"; -import AppLayout from "@saleor/components/AppLayout"; -import ErrorPage from "@saleor/components/ErrorPage"; -import useNavigator from "@saleor/hooks/useNavigator"; import useUser from "@saleor/hooks/useUser"; import NotFound from "../../NotFound"; import { PermissionEnum } from "../../types/globalTypes"; @@ -18,7 +14,6 @@ export const SectionRoute: React.FC = ({ permissions, ...props }) => { - const navigate = useNavigator(); const { user } = useUser(); const hasPermissions = @@ -26,18 +21,7 @@ export const SectionRoute: React.FC = ({ permissions .map(permission => hasPermission(permission, user)) .reduce((prev, curr) => prev && curr); - return hasPermissions ? ( - - navigate("/")} />} - key={permissions ? permissions.join(":") : "home"} - > - - - - ) : ( - - ); + return hasPermissions ? : ; }; SectionRoute.displayName = "Route"; export default SectionRoute; diff --git a/src/components/AppLayout/AppLayout.tsx b/src/components/AppLayout/AppLayout.tsx index bacdc3954..0362dc578 100644 --- a/src/components/AppLayout/AppLayout.tsx +++ b/src/components/AppLayout/AppLayout.tsx @@ -13,13 +13,13 @@ import classNames from "classnames"; import React from "react"; import SVG from "react-inlinesvg"; import { FormattedMessage, useIntl } from "react-intl"; -import { RouteComponentProps, withRouter } from "react-router"; +import useRouter from "use-react-router"; import saleorDarkLogoSmall from "@assets/images/logo-dark-small.svg"; import saleorDarkLogo from "@assets/images/logo-dark.svg"; import menuArrowIcon from "@assets/images/menu-arrow-icon.svg"; -import AppProgressProvider from "@saleor/components/AppProgress"; import { createConfigurationMenu } from "@saleor/configuration"; +import useAppState from "@saleor/hooks/useAppState"; import useLocalStorage from "@saleor/hooks/useLocalStorage"; import useNavigator from "@saleor/hooks/useNavigator"; import useTheme from "@saleor/hooks/useTheme"; @@ -28,6 +28,8 @@ import ArrowDropdown from "@saleor/icons/ArrowDropdown"; import { maybe } from "@saleor/misc"; import { staffMemberDetailsUrl } from "@saleor/staff/urls"; import Container from "../Container"; +import ErrorPage from "../ErrorPage"; +import NotFoundPage from "../NotFoundPage"; import AppActionContext from "./AppActionContext"; import AppHeaderContext from "./AppHeaderContext"; import { appLoaderHeight, drawerWidth, drawerWidthExpanded } from "./consts"; @@ -273,222 +275,230 @@ interface AppLayoutProps { children: React.ReactNode; } -const AppLayout = withRouter, any>( - ({ children, location }: AppLayoutProps & RouteComponentProps) => { - const classes = useStyles({}); - const { isDark, toggleTheme } = useTheme(); - const [isMenuSmall, setMenuSmall] = useLocalStorage("isMenuSmall", false); - const [isDrawerOpened, setDrawerState] = React.useState(false); - const [isMenuOpened, setMenuState] = React.useState(false); - const appActionAnchor = React.useRef(); - const appHeaderAnchor = React.useRef(); - const anchor = React.useRef(); - const { logout, user } = useUser(); - const navigate = useNavigator(); - const intl = useIntl(); +const AppLayout: React.FC = ({ children }) => { + const classes = useStyles({}); + const { isDark, toggleTheme } = useTheme(); + const [isMenuSmall, setMenuSmall] = useLocalStorage("isMenuSmall", false); + const [isDrawerOpened, setDrawerState] = React.useState(false); + const [isMenuOpened, setMenuState] = React.useState(false); + const appActionAnchor = React.useRef(); + const appHeaderAnchor = React.useRef(); + const anchor = React.useRef(); + const { logout, user } = useUser(); + const navigate = useNavigator(); + const intl = useIntl(); + const [appState, dispatchAppState] = useAppState(); + const { location } = useRouter(); - const menuStructure = createMenuStructure(intl); - const configurationMenu = createConfigurationMenu(intl); - const userPermissions = maybe(() => user.permissions, []); + const menuStructure = createMenuStructure(intl); + const configurationMenu = createConfigurationMenu(intl); + const userPermissions = maybe(() => user.permissions, []); - const renderConfigure = configurationMenu.some(section => - section.menuItems.some( - menuItem => - !!userPermissions.find( - userPermission => userPermission.code === menuItem.permission - ) - ) - ); + const renderConfigure = configurationMenu.some(section => + section.menuItems.some( + menuItem => + !!userPermissions.find( + userPermission => userPermission.code === menuItem.permission + ) + ) + ); - const handleLogout = () => { - setMenuState(false); - logout(); - }; + const handleLogout = () => { + setMenuState(false); + logout(); + }; - const handleViewerProfile = () => { - setMenuState(false); - navigate(staffMemberDetailsUrl(user.id)); - }; + const handleViewerProfile = () => { + setMenuState(false); + navigate(staffMemberDetailsUrl(user.id)); + }; - const handleMenuItemClick = (url: string, event: React.MouseEvent) => { - event.stopPropagation(); - event.preventDefault(); - setDrawerState(false); - navigate(url); - }; + const handleMenuItemClick = (url: string, event: React.MouseEvent) => { + event.stopPropagation(); + event.preventDefault(); + setDrawerState(false); + navigate(url); + }; - const handleIsMenuSmall = () => { - setMenuSmall(!isMenuSmall); - }; + const handleIsMenuSmall = () => { + setMenuSmall(!isMenuSmall); + }; - return ( - - {({ isProgress }) => ( - - -
-
- setDrawerState(false)} - open={isDrawerOpened} - small={!isMenuSmall} - > -
- -
- -
- -
-
- -
-
-
- {isProgress ? ( - - ) : ( -
- )} -
-
- -
-
setDrawerState(!isDrawerOpened)} - > - - - - -
-
-
-
- -
- - ) - } - className={classes.userChip} - label={ - <> - {user.email} - - - } - onClick={() => setMenuState(!isMenuOpened)} - /> - - {({ TransitionProps, placement }) => ( - - - setMenuState(false)} - mouseEvent="onClick" - > - - - - - - - - - - - - )} - -
-
-
- -
-
{children}
-
-
-
+ const handleErrorBack = () => { + navigate("/"); + dispatchAppState({ + payload: { + error: null + }, + type: "displayError" + }); + }; + + return ( + + +
+
+ setDrawerState(false)} + open={isDrawerOpened} + small={!isMenuSmall} + > +
+
- - - )} - - ); - } -); + +
+ +
+
+ +
+
+
+ {appState.loading ? ( + + ) : ( +
+ )} +
+
+ +
+
setDrawerState(!isDrawerOpened)} + > + + + + +
+
+
+
+ +
+ + ) + } + className={classes.userChip} + label={ + <> + {user.email} + + + } + onClick={() => setMenuState(!isMenuOpened)} + /> + + {({ TransitionProps, placement }) => ( + + + setMenuState(false)} + mouseEvent="onClick" + > + + + + + + + + + + + + )} + +
+
+
+ +
+
+ {appState.error ? ( + appState.error === "not-found" ? ( + + ) : ( + + ) + ) : ( + children + )} +
+
+
+
+
+ + + ); +}; export default AppLayout; diff --git a/src/components/AppProgress/index.tsx b/src/components/AppProgress/index.tsx deleted file mode 100644 index 4ff8f199b..000000000 --- a/src/components/AppProgress/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from "react"; - -interface IAppProgressContext { - isProgress: boolean; - setProgressState: (isOpened: boolean) => void; -} - -export const AppProgressContext = React.createContext( - undefined -); - -export const AppProgressProvider: React.FC<{}> = ({ children }) => { - const [isProgress, setProgressState] = React.useState(false); - - return ( - - {children} - - ); -}; - -export const AppProgress = AppProgressContext.Consumer; -export default AppProgress; diff --git a/src/components/ErrorPage/ErrorPage.tsx b/src/components/ErrorPage/ErrorPage.tsx index 77b6ba041..51810fb5f 100644 --- a/src/components/ErrorPage/ErrorPage.tsx +++ b/src/components/ErrorPage/ErrorPage.tsx @@ -48,7 +48,7 @@ const useStyles = makeStyles(theme => ({ root: { alignItems: "center", display: "flex", - height: "calc(100vh - 88px)" + height: "calc(100vh - 180px)" }, upperHeader: { fontWeight: 600 as 600 diff --git a/src/components/NotFoundPage/NotFoundPage.tsx b/src/components/NotFoundPage/NotFoundPage.tsx index 3dc5d923f..a71160355 100644 --- a/src/components/NotFoundPage/NotFoundPage.tsx +++ b/src/components/NotFoundPage/NotFoundPage.tsx @@ -43,8 +43,7 @@ const useStyles = makeStyles(theme => ({ root: { alignItems: "center", display: "flex", - height: "100vh", - width: "100vw" + height: "calc(100vh - 180px)" } })); diff --git a/src/containers/AppState/AppState.tsx b/src/containers/AppState/AppState.tsx new file mode 100644 index 000000000..9f3b5a2cc --- /dev/null +++ b/src/containers/AppState/AppState.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import useRouter from "use-react-router"; + +import appStateReducer, { AppStateReducerAction } from "./reducer"; +import IAppState, { initialAppState } from "./state"; + +export type AppStateContextType = [ + IAppState, + React.Dispatch +]; +export const AppStateContext = React.createContext([ + initialAppState, + () => undefined +]); +const AppStateProvider: React.FC = ({ children }) => { + const { location } = useRouter(); + const stateAndDispatch = React.useReducer(appStateReducer, initialAppState); + const [state, dispatch] = stateAndDispatch; + + React.useEffect(() => { + if (!!state.error) { + dispatch({ + payload: { + error: null + }, + type: "displayError" + }); + } + }, [location]); + + return ( + + {children} + + ); +}; + +export const { Consumer } = AppStateContext; + +export default AppStateProvider; diff --git a/src/containers/AppState/index.ts b/src/containers/AppState/index.ts new file mode 100644 index 000000000..04b1b0a50 --- /dev/null +++ b/src/containers/AppState/index.ts @@ -0,0 +1,2 @@ +export { default } from "./AppState"; +export * from "./AppState"; diff --git a/src/containers/AppState/reducer.ts b/src/containers/AppState/reducer.ts new file mode 100644 index 000000000..1ed4654d4 --- /dev/null +++ b/src/containers/AppState/reducer.ts @@ -0,0 +1,42 @@ +import IAppState, { AppError } from "./state"; + +export type AppStateReducerActionType = "displayError" | "displayLoader"; + +export interface AppStateReducerAction { + payload: Partial<{ + error: AppError; + value: boolean; + }>; + type: AppStateReducerActionType; +} + +function displayError(prevState: IAppState, error: AppError): IAppState { + return { + ...prevState, + error, + loading: false + }; +} + +function displayLoader(prevState: IAppState, value: boolean): IAppState { + return { + ...prevState, + loading: value + }; +} + +function reduceAppState( + prevState: IAppState, + action: AppStateReducerAction +): IAppState { + switch (action.type) { + case "displayError": + return displayError(prevState, action.payload.error); + case "displayLoader": + return displayLoader(prevState, action.payload.value); + default: + return prevState; + } +} + +export default reduceAppState; diff --git a/src/containers/AppState/state.ts b/src/containers/AppState/state.ts new file mode 100644 index 000000000..efea07b71 --- /dev/null +++ b/src/containers/AppState/state.ts @@ -0,0 +1,13 @@ +export type AppError = "unhandled" | "not-found"; + +interface IAppState { + error: AppError | null; + loading: boolean; +} + +export const initialAppState: IAppState = { + error: null, + loading: false +}; + +export default IAppState; diff --git a/src/hooks/useAppState.ts b/src/hooks/useAppState.ts new file mode 100644 index 000000000..83abfa181 --- /dev/null +++ b/src/hooks/useAppState.ts @@ -0,0 +1,11 @@ +import React from "react"; + +import { AppStateContext } from "../containers/AppState"; + +function useAppState() { + const stateAndDispatch = React.useContext(AppStateContext); + + return stateAndDispatch; +} + +export default useAppState; diff --git a/src/index.tsx b/src/index.tsx index 7c193a445..b229ec5d1 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,9 +8,11 @@ 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 { useIntl } from "react-intl"; import { BrowserRouter, Route, Switch } from "react-router-dom"; +import useAppState from "@saleor/hooks/useAppState"; import AttributeSection from "./attributes"; import { attributeSection } from "./attributes/urls"; import Auth, { getAuthToken, removeAuthToken } from "./auth"; @@ -20,7 +22,7 @@ import SectionRoute from "./auth/components/SectionRoute"; import { hasPermission } from "./auth/misc"; import CategorySection from "./categories"; import CollectionSection from "./collections"; -import { AppProgressProvider } from "./components/AppProgress"; +import AppLayout from "./components/AppLayout"; import { DateProvider } from "./components/Date"; import { LocaleProvider } from "./components/Locale"; import { MessageManager } from "./components/messages"; @@ -29,6 +31,7 @@ import ThemeProvider from "./components/Theme"; import { WindowTitle } from "./components/WindowTitle"; import { API_URI, APP_MOUNT_URI } from "./config"; import ConfigurationSection, { createConfigurationMenu } from "./configuration"; +import AppStateProvider from "./containers/AppState"; import { CustomerSection } from "./customers"; import DiscountSection from "./discounts"; import HomePage from "./home"; @@ -119,11 +122,11 @@ const App: React.FC = () => { - + - + @@ -135,6 +138,7 @@ const App: React.FC = () => { const Routes: React.FC = () => { const intl = useIntl(); + const [, dispatchAppState] = useAppState(); return ( <> @@ -148,109 +152,124 @@ const Routes: React.FC = () => { user }) => isAuthenticated && !tokenAuthLoading && !tokenVerifyLoading ? ( - - - - - - - - - - - - - - - - - - - - - {createConfigurationMenu(intl).filter(menu => - menu.menuItems.map(item => hasPermission(item.permission, user)) - ).length > 0 && ( - - )} - - + + + dispatchAppState({ + payload: { + error: "unhandled" + }, + type: "displayError" + }) + } + > + + + + + + + + + + + + + + + + + + + + + {createConfigurationMenu(intl).filter(menu => + menu.menuItems.map(item => + hasPermission(item.permission, user) + ) + ).length > 0 && ( + + )} + + + + ) : hasToken && tokenVerifyLoading ? ( ) : ( diff --git a/src/queries.tsx b/src/queries.tsx index bd71dc233..1428f5db4 100644 --- a/src/queries.tsx +++ b/src/queries.tsx @@ -5,9 +5,7 @@ import React from "react"; import { Query, QueryResult } from "react-apollo"; import { useIntl } from "react-intl"; -import AppProgress from "./components/AppProgress"; -import NotFoundPage from "./components/NotFoundPage"; -import useNavigator from "./hooks/useNavigator"; +import useAppState from "./hooks/useAppState"; import useNotifier from "./hooks/useNotifier"; import { commonMessages } from "./intl"; import { maybe, RequireAtLeastOne } from "./misc"; @@ -68,89 +66,108 @@ export function TypedQuery( query: DocumentNode ): React.FC> { return ({ children, displayLoader, skip, variables, require }) => { - const navigate = useNavigator(); const pushMessage = useNotifier(); + const [, dispatchAppState] = useAppState(); const intl = useIntl(); return ( - - {({ setProgressState }) => ( - - {(queryData: QueryResult) => { - if (queryData.error) { - if ( - !queryData.error.graphQLErrors.every( - err => - maybe(() => err.extensions.exception.code) === - "PermissionDenied" - ) - ) { - pushMessage({ - text: intl.formatMessage(commonMessages.somethingWentWrong) - }); + + {(queryData: QueryResult) => { + if (queryData.error) { + if ( + !queryData.error.graphQLErrors.every( + err => + maybe(() => err.extensions.exception.code) === + "PermissionDenied" + ) + ) { + pushMessage({ + text: intl.formatMessage(commonMessages.somethingWentWrong) + }); + } + } + + const loadMore = ( + mergeFunc: ( + previousResults: TData, + fetchMoreResult: TData + ) => TData, + extraVariables: RequireAtLeastOne + ) => + queryData.fetchMore({ + query, + updateQuery: (previousResults, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return previousResults; } - } + return mergeFunc(previousResults, fetchMoreResult); + }, + variables: { ...variables, ...extraVariables } + }); - const loadMore = ( - mergeFunc: ( - previousResults: TData, - fetchMoreResult: TData - ) => TData, - extraVariables: RequireAtLeastOne - ) => - queryData.fetchMore({ - query, - updateQuery: (previousResults, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return previousResults; - } - return mergeFunc(previousResults, fetchMoreResult); - }, - variables: { ...variables, ...extraVariables } - }); + if ( + !queryData.loading && + require && + queryData.data && + !require.reduce( + (acc, key) => acc && queryData.data[key] !== null, + true + ) + ) { + dispatchAppState({ + payload: { + error: "not-found" + }, + type: "displayError" + }); + } - let childrenOrNotFound = children({ + if (displayLoader) { + return ( + + dispatchAppState({ + payload: { + value: false + }, + type: "displayLoader" + }) + } + onLoading={() => + dispatchAppState({ + payload: { + value: true + }, + type: "displayLoader" + }) + } + > + {children({ + ...queryData, + loadMore + })} + + ); + } + + return ( + <> + {children({ ...queryData, loadMore - }); - if ( - !queryData.loading && - require && - queryData.data && - !require.reduce( - (acc, key) => acc && queryData.data[key] !== null, - true - ) - ) { - childrenOrNotFound = ( - navigate("/")} /> - ); - } - - if (displayLoader) { - return ( - setProgressState(false)} - onLoading={() => setProgressState(true)} - > - {childrenOrNotFound} - - ); - } - - return <>{childrenOrNotFound}; - }} - - )} - + })} + + ); + }} + ); }; }