Add app state

This commit is contained in:
dominik-zeglen 2019-11-14 15:10:52 +01:00
parent 0d51418f72
commit b12f3c35fa
12 changed files with 551 additions and 442 deletions

View file

@ -1,10 +1,6 @@
import React from "react"; import React from "react";
import ErrorBoundary from "react-error-boundary";
import { Route, RouteProps } from "react-router-dom"; 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 useUser from "@saleor/hooks/useUser";
import NotFound from "../../NotFound"; import NotFound from "../../NotFound";
import { PermissionEnum } from "../../types/globalTypes"; import { PermissionEnum } from "../../types/globalTypes";
@ -18,7 +14,6 @@ export const SectionRoute: React.FC<SectionRouteProps> = ({
permissions, permissions,
...props ...props
}) => { }) => {
const navigate = useNavigator();
const { user } = useUser(); const { user } = useUser();
const hasPermissions = const hasPermissions =
@ -26,18 +21,7 @@ export const SectionRoute: React.FC<SectionRouteProps> = ({
permissions permissions
.map(permission => hasPermission(permission, user)) .map(permission => hasPermission(permission, user))
.reduce((prev, curr) => prev && curr); .reduce((prev, curr) => prev && curr);
return hasPermissions ? ( return hasPermissions ? <Route {...props} /> : <NotFound />;
<AppLayout>
<ErrorBoundary
FallbackComponent={() => <ErrorPage onBack={() => navigate("/")} />}
key={permissions ? permissions.join(":") : "home"}
>
<Route {...props} />
</ErrorBoundary>
</AppLayout>
) : (
<NotFound />
);
}; };
SectionRoute.displayName = "Route"; SectionRoute.displayName = "Route";
export default SectionRoute; export default SectionRoute;

View file

@ -13,13 +13,13 @@ import classNames from "classnames";
import React from "react"; import React from "react";
import SVG from "react-inlinesvg"; import SVG from "react-inlinesvg";
import { FormattedMessage, useIntl } from "react-intl"; 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 saleorDarkLogoSmall from "@assets/images/logo-dark-small.svg";
import saleorDarkLogo from "@assets/images/logo-dark.svg"; import saleorDarkLogo from "@assets/images/logo-dark.svg";
import menuArrowIcon from "@assets/images/menu-arrow-icon.svg"; import menuArrowIcon from "@assets/images/menu-arrow-icon.svg";
import AppProgressProvider from "@saleor/components/AppProgress";
import { createConfigurationMenu } from "@saleor/configuration"; import { createConfigurationMenu } from "@saleor/configuration";
import useAppState from "@saleor/hooks/useAppState";
import useLocalStorage from "@saleor/hooks/useLocalStorage"; import useLocalStorage from "@saleor/hooks/useLocalStorage";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useTheme from "@saleor/hooks/useTheme"; import useTheme from "@saleor/hooks/useTheme";
@ -28,6 +28,8 @@ import ArrowDropdown from "@saleor/icons/ArrowDropdown";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
import { staffMemberDetailsUrl } from "@saleor/staff/urls"; import { staffMemberDetailsUrl } from "@saleor/staff/urls";
import Container from "../Container"; import Container from "../Container";
import ErrorPage from "../ErrorPage";
import NotFoundPage from "../NotFoundPage";
import AppActionContext from "./AppActionContext"; import AppActionContext from "./AppActionContext";
import AppHeaderContext from "./AppHeaderContext"; import AppHeaderContext from "./AppHeaderContext";
import { appLoaderHeight, drawerWidth, drawerWidthExpanded } from "./consts"; import { appLoaderHeight, drawerWidth, drawerWidthExpanded } from "./consts";
@ -273,222 +275,230 @@ interface AppLayoutProps {
children: React.ReactNode; children: React.ReactNode;
} }
const AppLayout = withRouter<AppLayoutProps & RouteComponentProps<any>, any>( const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
({ children, location }: AppLayoutProps & RouteComponentProps<any>) => { const classes = useStyles({});
const classes = useStyles({}); const { isDark, toggleTheme } = useTheme();
const { isDark, toggleTheme } = useTheme(); const [isMenuSmall, setMenuSmall] = useLocalStorage("isMenuSmall", false);
const [isMenuSmall, setMenuSmall] = useLocalStorage("isMenuSmall", false); const [isDrawerOpened, setDrawerState] = React.useState(false);
const [isDrawerOpened, setDrawerState] = React.useState(false); const [isMenuOpened, setMenuState] = React.useState(false);
const [isMenuOpened, setMenuState] = React.useState(false); const appActionAnchor = React.useRef<HTMLDivElement>();
const appActionAnchor = React.useRef<HTMLDivElement>(); const appHeaderAnchor = React.useRef<HTMLDivElement>();
const appHeaderAnchor = React.useRef<HTMLDivElement>(); const anchor = React.useRef<HTMLDivElement>();
const anchor = React.useRef<HTMLDivElement>(); const { logout, user } = useUser();
const { logout, user } = useUser(); const navigate = useNavigator();
const navigate = useNavigator(); const intl = useIntl();
const intl = useIntl(); const [appState, dispatchAppState] = useAppState();
const { location } = useRouter();
const menuStructure = createMenuStructure(intl); const menuStructure = createMenuStructure(intl);
const configurationMenu = createConfigurationMenu(intl); const configurationMenu = createConfigurationMenu(intl);
const userPermissions = maybe(() => user.permissions, []); const userPermissions = maybe(() => user.permissions, []);
const renderConfigure = configurationMenu.some(section => const renderConfigure = configurationMenu.some(section =>
section.menuItems.some( section.menuItems.some(
menuItem => menuItem =>
!!userPermissions.find( !!userPermissions.find(
userPermission => userPermission.code === menuItem.permission userPermission => userPermission.code === menuItem.permission
) )
) )
); );
const handleLogout = () => { const handleLogout = () => {
setMenuState(false); setMenuState(false);
logout(); logout();
}; };
const handleViewerProfile = () => { const handleViewerProfile = () => {
setMenuState(false); setMenuState(false);
navigate(staffMemberDetailsUrl(user.id)); navigate(staffMemberDetailsUrl(user.id));
}; };
const handleMenuItemClick = (url: string, event: React.MouseEvent<any>) => { const handleMenuItemClick = (url: string, event: React.MouseEvent<any>) => {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
setDrawerState(false); setDrawerState(false);
navigate(url); navigate(url);
}; };
const handleIsMenuSmall = () => { const handleIsMenuSmall = () => {
setMenuSmall(!isMenuSmall); setMenuSmall(!isMenuSmall);
}; };
return ( const handleErrorBack = () => {
<AppProgressProvider> navigate("/");
{({ isProgress }) => ( dispatchAppState({
<AppHeaderContext.Provider value={appHeaderAnchor}> payload: {
<AppActionContext.Provider value={appActionAnchor}> error: null
<div className={classes.root}> },
<div className={classes.sideBar}> type: "displayError"
<ResponsiveDrawer });
onClose={() => setDrawerState(false)} };
open={isDrawerOpened}
small={!isMenuSmall} return (
> <AppHeaderContext.Provider value={appHeaderAnchor}>
<div <AppActionContext.Provider value={appActionAnchor}>
className={classNames(classes.logo, { <div className={classes.root}>
[classes.logoSmall]: isMenuSmall, <div className={classes.sideBar}>
[classes.logoDark]: isDark <ResponsiveDrawer
})} onClose={() => setDrawerState(false)}
> open={isDrawerOpened}
<SVG small={!isMenuSmall}
src={isMenuSmall ? saleorDarkLogoSmall : saleorDarkLogo} >
/> <div
</div> className={classNames(classes.logo, {
<Hidden smDown> [classes.logoSmall]: isMenuSmall,
<div [classes.logoDark]: isDark
className={classNames(classes.isMenuSmall, { })}
[classes.isMenuSmallHide]: isMenuSmall, >
[classes.isMenuSmallDark]: isDark <SVG src={isMenuSmall ? saleorDarkLogoSmall : saleorDarkLogo} />
})}
onClick={handleIsMenuSmall}
>
<SVG src={menuArrowIcon} />
</div>
</Hidden>
<MenuList
className={isMenuSmall ? classes.menuSmall : classes.menu}
menuItems={menuStructure}
isMenuSmall={!isMenuSmall}
location={location.pathname}
user={user}
renderConfigure={renderConfigure}
onMenuItemClick={handleMenuItemClick}
/>
</ResponsiveDrawer>
</div>
<div
className={classNames(classes.content, {
[classes.contentToggle]: isMenuSmall
})}
>
{isProgress ? (
<LinearProgress
className={classes.appLoader}
color="primary"
/>
) : (
<div className={classes.appLoaderPlaceholder} />
)}
<div className={classes.viewContainer}>
<div>
<Container>
<div className={classes.header}>
<div
className={classNames(classes.menuIcon, {
[classes.menuIconOpen]: isDrawerOpened,
[classes.menuIconDark]: isDark
})}
onClick={() => setDrawerState(!isDrawerOpened)}
>
<span />
<span />
<span />
<span />
</div>
<div ref={appHeaderAnchor} />
<div className={classes.spacer} />
<div className={classes.userBar}>
<ThemeSwitch
className={classes.darkThemeSwitch}
checked={isDark}
onClick={toggleTheme}
/>
<div
className={classes.userMenuContainer}
ref={anchor}
>
<Chip
avatar={
user.avatar && (
<Avatar alt="user" src={user.avatar.url} />
)
}
className={classes.userChip}
label={
<>
{user.email}
<ArrowDropdown
className={classNames(classes.arrow, {
[classes.rotate]: isMenuOpened
})}
/>
</>
}
onClick={() => setMenuState(!isMenuOpened)}
/>
<Popper
className={classes.popover}
open={isMenuOpened}
anchorEl={anchor.current}
transition
disablePortal
placement="bottom-end"
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin:
placement === "bottom"
? "right top"
: "right bottom"
}}
>
<Paper>
<ClickAwayListener
onClickAway={() => setMenuState(false)}
mouseEvent="onClick"
>
<Menu>
<MenuItem
className={classes.userMenuItem}
onClick={handleViewerProfile}
>
<FormattedMessage
defaultMessage="Account Settings"
description="button"
/>
</MenuItem>
<MenuItem
className={classes.userMenuItem}
onClick={handleLogout}
>
<FormattedMessage
defaultMessage="Log out"
description="button"
/>
</MenuItem>
</Menu>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</div>
</div>
</div>
</Container>
</div>
<main className={classes.view}>{children}</main>
</div>
<div className={classes.appAction} ref={appActionAnchor} />
</div>
</div> </div>
</AppActionContext.Provider> <Hidden smDown>
</AppHeaderContext.Provider> <div
)} className={classNames(classes.isMenuSmall, {
</AppProgressProvider> [classes.isMenuSmallHide]: isMenuSmall,
); [classes.isMenuSmallDark]: isDark
} })}
); onClick={handleIsMenuSmall}
>
<SVG src={menuArrowIcon} />
</div>
</Hidden>
<MenuList
className={isMenuSmall ? classes.menuSmall : classes.menu}
menuItems={menuStructure}
isMenuSmall={!isMenuSmall}
location={location.pathname}
user={user}
renderConfigure={renderConfigure}
onMenuItemClick={handleMenuItemClick}
/>
</ResponsiveDrawer>
</div>
<div
className={classNames(classes.content, {
[classes.contentToggle]: isMenuSmall
})}
>
{appState.loading ? (
<LinearProgress className={classes.appLoader} color="primary" />
) : (
<div className={classes.appLoaderPlaceholder} />
)}
<div className={classes.viewContainer}>
<div>
<Container>
<div className={classes.header}>
<div
className={classNames(classes.menuIcon, {
[classes.menuIconOpen]: isDrawerOpened,
[classes.menuIconDark]: isDark
})}
onClick={() => setDrawerState(!isDrawerOpened)}
>
<span />
<span />
<span />
<span />
</div>
<div ref={appHeaderAnchor} />
<div className={classes.spacer} />
<div className={classes.userBar}>
<ThemeSwitch
className={classes.darkThemeSwitch}
checked={isDark}
onClick={toggleTheme}
/>
<div className={classes.userMenuContainer} ref={anchor}>
<Chip
avatar={
user.avatar && (
<Avatar alt="user" src={user.avatar.url} />
)
}
className={classes.userChip}
label={
<>
{user.email}
<ArrowDropdown
className={classNames(classes.arrow, {
[classes.rotate]: isMenuOpened
})}
/>
</>
}
onClick={() => setMenuState(!isMenuOpened)}
/>
<Popper
className={classes.popover}
open={isMenuOpened}
anchorEl={anchor.current}
transition
disablePortal
placement="bottom-end"
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin:
placement === "bottom"
? "right top"
: "right bottom"
}}
>
<Paper>
<ClickAwayListener
onClickAway={() => setMenuState(false)}
mouseEvent="onClick"
>
<Menu>
<MenuItem
className={classes.userMenuItem}
onClick={handleViewerProfile}
>
<FormattedMessage
defaultMessage="Account Settings"
description="button"
/>
</MenuItem>
<MenuItem
className={classes.userMenuItem}
onClick={handleLogout}
>
<FormattedMessage
defaultMessage="Log out"
description="button"
/>
</MenuItem>
</Menu>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</div>
</div>
</div>
</Container>
</div>
<main className={classes.view}>
{appState.error ? (
appState.error === "not-found" ? (
<NotFoundPage onBack={handleErrorBack} />
) : (
<ErrorPage onBack={handleErrorBack} />
)
) : (
children
)}
</main>
</div>
<div className={classes.appAction} ref={appActionAnchor} />
</div>
</div>
</AppActionContext.Provider>
</AppHeaderContext.Provider>
);
};
export default AppLayout; export default AppLayout;

View file

@ -1,28 +0,0 @@
import React from "react";
interface IAppProgressContext {
isProgress: boolean;
setProgressState: (isOpened: boolean) => void;
}
export const AppProgressContext = React.createContext<IAppProgressContext>(
undefined
);
export const AppProgressProvider: React.FC<{}> = ({ children }) => {
const [isProgress, setProgressState] = React.useState(false);
return (
<AppProgressContext.Provider
value={{
isProgress,
setProgressState
}}
>
{children}
</AppProgressContext.Provider>
);
};
export const AppProgress = AppProgressContext.Consumer;
export default AppProgress;

View file

@ -48,7 +48,7 @@ const useStyles = makeStyles(theme => ({
root: { root: {
alignItems: "center", alignItems: "center",
display: "flex", display: "flex",
height: "calc(100vh - 88px)" height: "calc(100vh - 180px)"
}, },
upperHeader: { upperHeader: {
fontWeight: 600 as 600 fontWeight: 600 as 600

View file

@ -43,8 +43,7 @@ const useStyles = makeStyles(theme => ({
root: { root: {
alignItems: "center", alignItems: "center",
display: "flex", display: "flex",
height: "100vh", height: "calc(100vh - 180px)"
width: "100vw"
} }
})); }));

View file

@ -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<AppStateReducerAction>
];
export const AppStateContext = React.createContext<AppStateContextType>([
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 (
<AppStateContext.Provider value={stateAndDispatch}>
{children}
</AppStateContext.Provider>
);
};
export const { Consumer } = AppStateContext;
export default AppStateProvider;

View file

@ -0,0 +1,2 @@
export { default } from "./AppState";
export * from "./AppState";

View file

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

View file

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

11
src/hooks/useAppState.ts Normal file
View file

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

View file

@ -8,9 +8,11 @@ import { createUploadLink } from "apollo-upload-client";
import React from "react"; import React from "react";
import { ApolloProvider } from "react-apollo"; import { ApolloProvider } from "react-apollo";
import { render } from "react-dom"; import { render } from "react-dom";
import ErrorBoundary from "react-error-boundary";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { BrowserRouter, Route, Switch } from "react-router-dom"; import { BrowserRouter, Route, Switch } from "react-router-dom";
import useAppState from "@saleor/hooks/useAppState";
import AttributeSection from "./attributes"; import AttributeSection from "./attributes";
import { attributeSection } from "./attributes/urls"; import { attributeSection } from "./attributes/urls";
import Auth, { getAuthToken, removeAuthToken } from "./auth"; import Auth, { getAuthToken, removeAuthToken } from "./auth";
@ -20,7 +22,7 @@ import SectionRoute from "./auth/components/SectionRoute";
import { hasPermission } from "./auth/misc"; import { hasPermission } from "./auth/misc";
import CategorySection from "./categories"; import CategorySection from "./categories";
import CollectionSection from "./collections"; import CollectionSection from "./collections";
import { AppProgressProvider } from "./components/AppProgress"; import AppLayout from "./components/AppLayout";
import { DateProvider } from "./components/Date"; import { DateProvider } from "./components/Date";
import { LocaleProvider } from "./components/Locale"; import { LocaleProvider } from "./components/Locale";
import { MessageManager } from "./components/messages"; import { MessageManager } from "./components/messages";
@ -29,6 +31,7 @@ import ThemeProvider from "./components/Theme";
import { WindowTitle } from "./components/WindowTitle"; import { WindowTitle } from "./components/WindowTitle";
import { API_URI, APP_MOUNT_URI } from "./config"; import { API_URI, APP_MOUNT_URI } from "./config";
import ConfigurationSection, { createConfigurationMenu } from "./configuration"; import ConfigurationSection, { createConfigurationMenu } from "./configuration";
import AppStateProvider from "./containers/AppState";
import { CustomerSection } from "./customers"; import { CustomerSection } from "./customers";
import DiscountSection from "./discounts"; import DiscountSection from "./discounts";
import HomePage from "./home"; import HomePage from "./home";
@ -119,11 +122,11 @@ const App: React.FC = () => {
<DateProvider> <DateProvider>
<LocaleProvider> <LocaleProvider>
<MessageManager> <MessageManager>
<AppProgressProvider> <AppStateProvider>
<ShopProvider> <ShopProvider>
<Routes /> <Routes />
</ShopProvider> </ShopProvider>
</AppProgressProvider> </AppStateProvider>
</MessageManager> </MessageManager>
</LocaleProvider> </LocaleProvider>
</DateProvider> </DateProvider>
@ -135,6 +138,7 @@ const App: React.FC = () => {
const Routes: React.FC = () => { const Routes: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const [, dispatchAppState] = useAppState();
return ( return (
<> <>
@ -148,109 +152,124 @@ const Routes: React.FC = () => {
user user
}) => }) =>
isAuthenticated && !tokenAuthLoading && !tokenVerifyLoading ? ( isAuthenticated && !tokenAuthLoading && !tokenVerifyLoading ? (
<Switch> <AppLayout>
<SectionRoute exact path="/" component={HomePage} /> <ErrorBoundary
<SectionRoute onError={() =>
permissions={[PermissionEnum.MANAGE_PRODUCTS]} dispatchAppState({
path="/categories" payload: {
component={CategorySection} error: "unhandled"
/> },
<SectionRoute type: "displayError"
permissions={[PermissionEnum.MANAGE_PRODUCTS]} })
path="/collections" }
component={CollectionSection} >
/> <Switch>
<SectionRoute <SectionRoute exact path="/" component={HomePage} />
permissions={[PermissionEnum.MANAGE_USERS]} <SectionRoute
path="/customers" permissions={[PermissionEnum.MANAGE_PRODUCTS]}
component={CustomerSection} path="/categories"
/> component={CategorySection}
<SectionRoute />
permissions={[PermissionEnum.MANAGE_DISCOUNTS]} <SectionRoute
path="/discounts" permissions={[PermissionEnum.MANAGE_PRODUCTS]}
component={DiscountSection} path="/collections"
/> component={CollectionSection}
<SectionRoute />
permissions={[PermissionEnum.MANAGE_PAGES]} <SectionRoute
path="/pages" permissions={[PermissionEnum.MANAGE_USERS]}
component={PageSection} path="/customers"
/> component={CustomerSection}
<SectionRoute />
permissions={[PermissionEnum.MANAGE_PLUGINS]} <SectionRoute
path="/plugins" permissions={[PermissionEnum.MANAGE_DISCOUNTS]}
component={PluginsSection} path="/discounts"
/> component={DiscountSection}
<SectionRoute />
permissions={[PermissionEnum.MANAGE_ORDERS]} <SectionRoute
path="/orders" permissions={[PermissionEnum.MANAGE_PAGES]}
component={OrdersSection} path="/pages"
/> component={PageSection}
<SectionRoute />
permissions={[PermissionEnum.MANAGE_PRODUCTS]} <SectionRoute
path="/products" permissions={[PermissionEnum.MANAGE_PLUGINS]}
component={ProductSection} path="/plugins"
/> component={PluginsSection}
<SectionRoute />
permissions={[PermissionEnum.MANAGE_PRODUCTS]} <SectionRoute
path="/product-types" permissions={[PermissionEnum.MANAGE_ORDERS]}
component={ProductTypesSection} path="/orders"
/> component={OrdersSection}
<SectionRoute />
permissions={[PermissionEnum.MANAGE_STAFF]} <SectionRoute
path="/staff" permissions={[PermissionEnum.MANAGE_PRODUCTS]}
component={StaffSection} path="/products"
/> component={ProductSection}
<SectionRoute />
permissions={[PermissionEnum.MANAGE_SETTINGS]} <SectionRoute
path="/site-settings" permissions={[PermissionEnum.MANAGE_PRODUCTS]}
component={SiteSettingsSection} path="/product-types"
/> component={ProductTypesSection}
<SectionRoute />
permissions={[PermissionEnum.MANAGE_SETTINGS]} <SectionRoute
path="/taxes" permissions={[PermissionEnum.MANAGE_STAFF]}
component={TaxesSection} path="/staff"
/> component={StaffSection}
<SectionRoute />
permissions={[PermissionEnum.MANAGE_SHIPPING]} <SectionRoute
path="/shipping" permissions={[PermissionEnum.MANAGE_SETTINGS]}
component={ShippingSection} path="/site-settings"
/> component={SiteSettingsSection}
<SectionRoute />
permissions={[PermissionEnum.MANAGE_TRANSLATIONS]} <SectionRoute
path="/translations" permissions={[PermissionEnum.MANAGE_SETTINGS]}
component={TranslationsSection} path="/taxes"
/> component={TaxesSection}
<SectionRoute />
permissions={[PermissionEnum.MANAGE_WEBHOOKS]} <SectionRoute
path="/webhooks" permissions={[PermissionEnum.MANAGE_SHIPPING]}
component={WebhooksSection} path="/shipping"
/> component={ShippingSection}
<SectionRoute />
permissions={[PermissionEnum.MANAGE_MENUS]} <SectionRoute
path={navigationSection} permissions={[PermissionEnum.MANAGE_TRANSLATIONS]}
component={NavigationSection} path="/translations"
/> component={TranslationsSection}
<SectionRoute />
permissions={[PermissionEnum.MANAGE_PRODUCTS]} <SectionRoute
path={attributeSection} permissions={[PermissionEnum.MANAGE_WEBHOOKS]}
component={AttributeSection} path="/webhooks"
/> component={WebhooksSection}
<SectionRoute />
permissions={[PermissionEnum.MANAGE_SERVICE_ACCOUNTS]} <SectionRoute
path={serviceSection} permissions={[PermissionEnum.MANAGE_MENUS]}
component={ServiceSection} path={navigationSection}
/> component={NavigationSection}
{createConfigurationMenu(intl).filter(menu => />
menu.menuItems.map(item => hasPermission(item.permission, user)) <SectionRoute
).length > 0 && ( permissions={[PermissionEnum.MANAGE_PRODUCTS]}
<SectionRoute path={attributeSection}
exact component={AttributeSection}
path="/configuration" />
component={ConfigurationSection} <SectionRoute
/> permissions={[PermissionEnum.MANAGE_SERVICE_ACCOUNTS]}
)} path={serviceSection}
<Route component={NotFound} /> component={ServiceSection}
</Switch> />
{createConfigurationMenu(intl).filter(menu =>
menu.menuItems.map(item =>
hasPermission(item.permission, user)
)
).length > 0 && (
<SectionRoute
exact
path="/configuration"
component={ConfigurationSection}
/>
)}
<Route component={NotFound} />
</Switch>
</ErrorBoundary>
</AppLayout>
) : hasToken && tokenVerifyLoading ? ( ) : hasToken && tokenVerifyLoading ? (
<LoginLoading /> <LoginLoading />
) : ( ) : (

View file

@ -5,9 +5,7 @@ import React from "react";
import { Query, QueryResult } from "react-apollo"; import { Query, QueryResult } from "react-apollo";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import AppProgress from "./components/AppProgress"; import useAppState from "./hooks/useAppState";
import NotFoundPage from "./components/NotFoundPage";
import useNavigator from "./hooks/useNavigator";
import useNotifier from "./hooks/useNotifier"; import useNotifier from "./hooks/useNotifier";
import { commonMessages } from "./intl"; import { commonMessages } from "./intl";
import { maybe, RequireAtLeastOne } from "./misc"; import { maybe, RequireAtLeastOne } from "./misc";
@ -68,89 +66,108 @@ export function TypedQuery<TData, TVariables>(
query: DocumentNode query: DocumentNode
): React.FC<TypedQueryInnerProps<TData, TVariables>> { ): React.FC<TypedQueryInnerProps<TData, TVariables>> {
return ({ children, displayLoader, skip, variables, require }) => { return ({ children, displayLoader, skip, variables, require }) => {
const navigate = useNavigator();
const pushMessage = useNotifier(); const pushMessage = useNotifier();
const [, dispatchAppState] = useAppState();
const intl = useIntl(); const intl = useIntl();
return ( return (
<AppProgress> <Query
{({ setProgressState }) => ( fetchPolicy="cache-and-network"
<Query query={query}
fetchPolicy="cache-and-network" variables={variables}
query={query} skip={skip}
variables={variables} context={{ useBatching: true }}
skip={skip} errorPolicy="all"
context={{ useBatching: true }} >
errorPolicy="all" {(queryData: QueryResult<TData, TVariables>) => {
> if (queryData.error) {
{(queryData: QueryResult<TData, TVariables>) => { if (
if (queryData.error) { !queryData.error.graphQLErrors.every(
if ( err =>
!queryData.error.graphQLErrors.every( maybe(() => err.extensions.exception.code) ===
err => "PermissionDenied"
maybe(() => err.extensions.exception.code) === )
"PermissionDenied" ) {
) pushMessage({
) { text: intl.formatMessage(commonMessages.somethingWentWrong)
pushMessage({ });
text: intl.formatMessage(commonMessages.somethingWentWrong) }
}); }
const loadMore = (
mergeFunc: (
previousResults: TData,
fetchMoreResult: TData
) => TData,
extraVariables: RequireAtLeastOne<TVariables>
) =>
queryData.fetchMore({
query,
updateQuery: (previousResults, { fetchMoreResult }) => {
if (!fetchMoreResult) {
return previousResults;
} }
} return mergeFunc(previousResults, fetchMoreResult);
},
variables: { ...variables, ...extraVariables }
});
const loadMore = ( if (
mergeFunc: ( !queryData.loading &&
previousResults: TData, require &&
fetchMoreResult: TData queryData.data &&
) => TData, !require.reduce(
extraVariables: RequireAtLeastOne<TVariables> (acc, key) => acc && queryData.data[key] !== null,
) => true
queryData.fetchMore({ )
query, ) {
updateQuery: (previousResults, { fetchMoreResult }) => { dispatchAppState({
if (!fetchMoreResult) { payload: {
return previousResults; error: "not-found"
} },
return mergeFunc(previousResults, fetchMoreResult); type: "displayError"
}, });
variables: { ...variables, ...extraVariables } }
});
let childrenOrNotFound = children({ if (displayLoader) {
return (
<QueryProgress
loading={queryData.loading}
onCompleted={() =>
dispatchAppState({
payload: {
value: false
},
type: "displayLoader"
})
}
onLoading={() =>
dispatchAppState({
payload: {
value: true
},
type: "displayLoader"
})
}
>
{children({
...queryData,
loadMore
})}
</QueryProgress>
);
}
return (
<>
{children({
...queryData, ...queryData,
loadMore loadMore
}); })}
if ( </>
!queryData.loading && );
require && }}
queryData.data && </Query>
!require.reduce(
(acc, key) => acc && queryData.data[key] !== null,
true
)
) {
childrenOrNotFound = (
<NotFoundPage onBack={() => navigate("/")} />
);
}
if (displayLoader) {
return (
<QueryProgress
loading={queryData.loading}
onCompleted={() => setProgressState(false)}
onLoading={() => setProgressState(true)}
>
{childrenOrNotFound}
</QueryProgress>
);
}
return <>{childrenOrNotFound}</>;
}}
</Query>
)}
</AppProgress>
); );
}; };
} }