Add app state
This commit is contained in:
parent
0d51418f72
commit
b12f3c35fa
12 changed files with 551 additions and 442 deletions
|
@ -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<SectionRouteProps> = ({
|
|||
permissions,
|
||||
...props
|
||||
}) => {
|
||||
const navigate = useNavigator();
|
||||
const { user } = useUser();
|
||||
|
||||
const hasPermissions =
|
||||
|
@ -26,18 +21,7 @@ export const SectionRoute: React.FC<SectionRouteProps> = ({
|
|||
permissions
|
||||
.map(permission => hasPermission(permission, user))
|
||||
.reduce((prev, curr) => prev && curr);
|
||||
return hasPermissions ? (
|
||||
<AppLayout>
|
||||
<ErrorBoundary
|
||||
FallbackComponent={() => <ErrorPage onBack={() => navigate("/")} />}
|
||||
key={permissions ? permissions.join(":") : "home"}
|
||||
>
|
||||
<Route {...props} />
|
||||
</ErrorBoundary>
|
||||
</AppLayout>
|
||||
) : (
|
||||
<NotFound />
|
||||
);
|
||||
return hasPermissions ? <Route {...props} /> : <NotFound />;
|
||||
};
|
||||
SectionRoute.displayName = "Route";
|
||||
export default SectionRoute;
|
||||
|
|
|
@ -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<AppLayoutProps & RouteComponentProps<any>, any>(
|
||||
({ children, location }: AppLayoutProps & RouteComponentProps<any>) => {
|
||||
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<HTMLDivElement>();
|
||||
const appHeaderAnchor = React.useRef<HTMLDivElement>();
|
||||
const anchor = React.useRef<HTMLDivElement>();
|
||||
const { logout, user } = useUser();
|
||||
const navigate = useNavigator();
|
||||
const intl = useIntl();
|
||||
const AppLayout: React.FC<AppLayoutProps> = ({ 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<HTMLDivElement>();
|
||||
const appHeaderAnchor = React.useRef<HTMLDivElement>();
|
||||
const anchor = React.useRef<HTMLDivElement>();
|
||||
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<any>) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
setDrawerState(false);
|
||||
navigate(url);
|
||||
};
|
||||
const handleMenuItemClick = (url: string, event: React.MouseEvent<any>) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
setDrawerState(false);
|
||||
navigate(url);
|
||||
};
|
||||
|
||||
const handleIsMenuSmall = () => {
|
||||
setMenuSmall(!isMenuSmall);
|
||||
};
|
||||
const handleIsMenuSmall = () => {
|
||||
setMenuSmall(!isMenuSmall);
|
||||
};
|
||||
|
||||
return (
|
||||
<AppProgressProvider>
|
||||
{({ isProgress }) => (
|
||||
<AppHeaderContext.Provider value={appHeaderAnchor}>
|
||||
<AppActionContext.Provider value={appActionAnchor}>
|
||||
<div className={classes.root}>
|
||||
<div className={classes.sideBar}>
|
||||
<ResponsiveDrawer
|
||||
onClose={() => setDrawerState(false)}
|
||||
open={isDrawerOpened}
|
||||
small={!isMenuSmall}
|
||||
>
|
||||
<div
|
||||
className={classNames(classes.logo, {
|
||||
[classes.logoSmall]: isMenuSmall,
|
||||
[classes.logoDark]: isDark
|
||||
})}
|
||||
>
|
||||
<SVG
|
||||
src={isMenuSmall ? saleorDarkLogoSmall : saleorDarkLogo}
|
||||
/>
|
||||
</div>
|
||||
<Hidden smDown>
|
||||
<div
|
||||
className={classNames(classes.isMenuSmall, {
|
||||
[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
|
||||
})}
|
||||
>
|
||||
{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>
|
||||
const handleErrorBack = () => {
|
||||
navigate("/");
|
||||
dispatchAppState({
|
||||
payload: {
|
||||
error: null
|
||||
},
|
||||
type: "displayError"
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AppHeaderContext.Provider value={appHeaderAnchor}>
|
||||
<AppActionContext.Provider value={appActionAnchor}>
|
||||
<div className={classes.root}>
|
||||
<div className={classes.sideBar}>
|
||||
<ResponsiveDrawer
|
||||
onClose={() => setDrawerState(false)}
|
||||
open={isDrawerOpened}
|
||||
small={!isMenuSmall}
|
||||
>
|
||||
<div
|
||||
className={classNames(classes.logo, {
|
||||
[classes.logoSmall]: isMenuSmall,
|
||||
[classes.logoDark]: isDark
|
||||
})}
|
||||
>
|
||||
<SVG src={isMenuSmall ? saleorDarkLogoSmall : saleorDarkLogo} />
|
||||
</div>
|
||||
</AppActionContext.Provider>
|
||||
</AppHeaderContext.Provider>
|
||||
)}
|
||||
</AppProgressProvider>
|
||||
);
|
||||
}
|
||||
);
|
||||
<Hidden smDown>
|
||||
<div
|
||||
className={classNames(classes.isMenuSmall, {
|
||||
[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;
|
||||
|
|
|
@ -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;
|
|
@ -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
|
||||
|
|
|
@ -43,8 +43,7 @@ const useStyles = makeStyles(theme => ({
|
|||
root: {
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
height: "100vh",
|
||||
width: "100vw"
|
||||
height: "calc(100vh - 180px)"
|
||||
}
|
||||
}));
|
||||
|
||||
|
|
40
src/containers/AppState/AppState.tsx
Normal file
40
src/containers/AppState/AppState.tsx
Normal 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;
|
2
src/containers/AppState/index.ts
Normal file
2
src/containers/AppState/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./AppState";
|
||||
export * from "./AppState";
|
42
src/containers/AppState/reducer.ts
Normal file
42
src/containers/AppState/reducer.ts
Normal 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;
|
13
src/containers/AppState/state.ts
Normal file
13
src/containers/AppState/state.ts
Normal 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
11
src/hooks/useAppState.ts
Normal 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;
|
231
src/index.tsx
231
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 = () => {
|
|||
<DateProvider>
|
||||
<LocaleProvider>
|
||||
<MessageManager>
|
||||
<AppProgressProvider>
|
||||
<AppStateProvider>
|
||||
<ShopProvider>
|
||||
<Routes />
|
||||
</ShopProvider>
|
||||
</AppProgressProvider>
|
||||
</AppStateProvider>
|
||||
</MessageManager>
|
||||
</LocaleProvider>
|
||||
</DateProvider>
|
||||
|
@ -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 ? (
|
||||
<Switch>
|
||||
<SectionRoute exact path="/" component={HomePage} />
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_PRODUCTS]}
|
||||
path="/categories"
|
||||
component={CategorySection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_PRODUCTS]}
|
||||
path="/collections"
|
||||
component={CollectionSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_USERS]}
|
||||
path="/customers"
|
||||
component={CustomerSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_DISCOUNTS]}
|
||||
path="/discounts"
|
||||
component={DiscountSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_PAGES]}
|
||||
path="/pages"
|
||||
component={PageSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_PLUGINS]}
|
||||
path="/plugins"
|
||||
component={PluginsSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_ORDERS]}
|
||||
path="/orders"
|
||||
component={OrdersSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_PRODUCTS]}
|
||||
path="/products"
|
||||
component={ProductSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_PRODUCTS]}
|
||||
path="/product-types"
|
||||
component={ProductTypesSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_STAFF]}
|
||||
path="/staff"
|
||||
component={StaffSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_SETTINGS]}
|
||||
path="/site-settings"
|
||||
component={SiteSettingsSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_SETTINGS]}
|
||||
path="/taxes"
|
||||
component={TaxesSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_SHIPPING]}
|
||||
path="/shipping"
|
||||
component={ShippingSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_TRANSLATIONS]}
|
||||
path="/translations"
|
||||
component={TranslationsSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_WEBHOOKS]}
|
||||
path="/webhooks"
|
||||
component={WebhooksSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_MENUS]}
|
||||
path={navigationSection}
|
||||
component={NavigationSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_PRODUCTS]}
|
||||
path={attributeSection}
|
||||
component={AttributeSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_SERVICE_ACCOUNTS]}
|
||||
path={serviceSection}
|
||||
component={ServiceSection}
|
||||
/>
|
||||
{createConfigurationMenu(intl).filter(menu =>
|
||||
menu.menuItems.map(item => hasPermission(item.permission, user))
|
||||
).length > 0 && (
|
||||
<SectionRoute
|
||||
exact
|
||||
path="/configuration"
|
||||
component={ConfigurationSection}
|
||||
/>
|
||||
)}
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
<AppLayout>
|
||||
<ErrorBoundary
|
||||
onError={() =>
|
||||
dispatchAppState({
|
||||
payload: {
|
||||
error: "unhandled"
|
||||
},
|
||||
type: "displayError"
|
||||
})
|
||||
}
|
||||
>
|
||||
<Switch>
|
||||
<SectionRoute exact path="/" component={HomePage} />
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_PRODUCTS]}
|
||||
path="/categories"
|
||||
component={CategorySection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_PRODUCTS]}
|
||||
path="/collections"
|
||||
component={CollectionSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_USERS]}
|
||||
path="/customers"
|
||||
component={CustomerSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_DISCOUNTS]}
|
||||
path="/discounts"
|
||||
component={DiscountSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_PAGES]}
|
||||
path="/pages"
|
||||
component={PageSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_PLUGINS]}
|
||||
path="/plugins"
|
||||
component={PluginsSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_ORDERS]}
|
||||
path="/orders"
|
||||
component={OrdersSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_PRODUCTS]}
|
||||
path="/products"
|
||||
component={ProductSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_PRODUCTS]}
|
||||
path="/product-types"
|
||||
component={ProductTypesSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_STAFF]}
|
||||
path="/staff"
|
||||
component={StaffSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_SETTINGS]}
|
||||
path="/site-settings"
|
||||
component={SiteSettingsSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_SETTINGS]}
|
||||
path="/taxes"
|
||||
component={TaxesSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_SHIPPING]}
|
||||
path="/shipping"
|
||||
component={ShippingSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_TRANSLATIONS]}
|
||||
path="/translations"
|
||||
component={TranslationsSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_WEBHOOKS]}
|
||||
path="/webhooks"
|
||||
component={WebhooksSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_MENUS]}
|
||||
path={navigationSection}
|
||||
component={NavigationSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_PRODUCTS]}
|
||||
path={attributeSection}
|
||||
component={AttributeSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_SERVICE_ACCOUNTS]}
|
||||
path={serviceSection}
|
||||
component={ServiceSection}
|
||||
/>
|
||||
{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 ? (
|
||||
<LoginLoading />
|
||||
) : (
|
||||
|
|
171
src/queries.tsx
171
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<TData, TVariables>(
|
|||
query: DocumentNode
|
||||
): React.FC<TypedQueryInnerProps<TData, TVariables>> {
|
||||
return ({ children, displayLoader, skip, variables, require }) => {
|
||||
const navigate = useNavigator();
|
||||
const pushMessage = useNotifier();
|
||||
const [, dispatchAppState] = useAppState();
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<AppProgress>
|
||||
{({ setProgressState }) => (
|
||||
<Query
|
||||
fetchPolicy="cache-and-network"
|
||||
query={query}
|
||||
variables={variables}
|
||||
skip={skip}
|
||||
context={{ useBatching: true }}
|
||||
errorPolicy="all"
|
||||
>
|
||||
{(queryData: QueryResult<TData, TVariables>) => {
|
||||
if (queryData.error) {
|
||||
if (
|
||||
!queryData.error.graphQLErrors.every(
|
||||
err =>
|
||||
maybe(() => err.extensions.exception.code) ===
|
||||
"PermissionDenied"
|
||||
)
|
||||
) {
|
||||
pushMessage({
|
||||
text: intl.formatMessage(commonMessages.somethingWentWrong)
|
||||
});
|
||||
<Query
|
||||
fetchPolicy="cache-and-network"
|
||||
query={query}
|
||||
variables={variables}
|
||||
skip={skip}
|
||||
context={{ useBatching: true }}
|
||||
errorPolicy="all"
|
||||
>
|
||||
{(queryData: QueryResult<TData, TVariables>) => {
|
||||
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<TVariables>
|
||||
) =>
|
||||
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<TVariables>
|
||||
) =>
|
||||
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 (
|
||||
<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,
|
||||
loadMore
|
||||
});
|
||||
if (
|
||||
!queryData.loading &&
|
||||
require &&
|
||||
queryData.data &&
|
||||
!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>
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Query>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue