Merge pull request #635 from mirumee/add/navigator-btn

Add navigator button
This commit is contained in:
Dominik Żegleń 2020-08-11 15:35:23 +02:00 committed by GitHub
commit 9a86c6beb5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 409 additions and 160 deletions

View file

@ -15,6 +15,7 @@ All notable, unreleased changes to this project will be documented in this file.
- create Apps - #599 by @AlicjaSzu - create Apps - #599 by @AlicjaSzu
- Refactor authorization - #624 by @dominik-zeglen - Refactor authorization - #624 by @dominik-zeglen
- Fix minor bugs - #628 by @dominik-zeglen - Fix minor bugs - #628 by @dominik-zeglen
- Add navigator button - #635 by @dominik-zeglen
## 2.10.1 ## 2.10.1

View file

@ -0,0 +1 @@
<svg width="12" height="18" viewBox="0 0 12 18" fill="none" xmlns="http://www.w3.org/2000/svg" ><path d="M0 11.2324L6.99609 0L6.1875 6.78516H11.6895L4.79883 18L5.51953 11.2324H0Z" fill="currentColor" /></svg>

After

Width:  |  Height:  |  Size: 208 B

View file

@ -1512,6 +1512,9 @@
"src_dot_components_dot_MultiSelectField_dot_4205644805": { "src_dot_components_dot_MultiSelectField_dot_4205644805": {
"string": "No results found" "string": "No results found"
}, },
"src_dot_components_dot_NavigatorButton_dot_154826674": {
"string": "Navigator"
},
"src_dot_components_dot_Navigator_dot_1116468870": { "src_dot_components_dot_Navigator_dot_1116468870": {
"context": "navigator placeholder", "context": "navigator placeholder",
"string": "Order Number" "string": "Order Number"

View file

@ -28,6 +28,8 @@ import useRouter from "use-react-router";
import Container from "../Container"; import Container from "../Container";
import ErrorPage from "../ErrorPage"; import ErrorPage from "../ErrorPage";
import Navigator from "../Navigator";
import NavigatorButton from "../NavigatorButton/NavigatorButton";
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";
@ -310,6 +312,7 @@ const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
const intl = useIntl(); const intl = useIntl();
const [appState, dispatchAppState] = useAppState(); const [appState, dispatchAppState] = useAppState();
const { location } = useRouter(); const { location } = useRouter();
const [isNavigatorVisible, setNavigatorVisibility] = React.useState(false);
const menuStructure = createMenuStructure(intl); const menuStructure = createMenuStructure(intl);
const configurationMenu = createConfigurationMenu(intl); const configurationMenu = createConfigurationMenu(intl);
@ -356,170 +359,184 @@ const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
}; };
return ( return (
<AppHeaderContext.Provider value={appHeaderAnchor}> <>
<AppActionContext.Provider value={appActionAnchor}> <Navigator
<div className={classes.root}> visible={isNavigatorVisible}
<div className={classes.sideBar}> setVisibility={setNavigatorVisibility}
<ResponsiveDrawer />
onClose={() => setDrawerState(false)} <AppHeaderContext.Provider value={appHeaderAnchor}>
open={isDrawerOpened} <AppActionContext.Provider value={appActionAnchor}>
small={!isMenuSmall} <div className={classes.root}>
> <div className={classes.sideBar}>
<div <ResponsiveDrawer
className={classNames(classes.logo, { onClose={() => setDrawerState(false)}
[classes.logoSmall]: isMenuSmall, open={isDrawerOpened}
[classes.logoDark]: isDark small={!isMenuSmall}
})}
> >
<SVG src={isMenuSmall ? saleorDarkLogoSmall : saleorDarkLogo} />
</div>
<Hidden smDown>
<div <div
className={classNames(classes.isMenuSmall, { className={classNames(classes.logo, {
[classes.isMenuSmallHide]: isMenuSmall, [classes.logoSmall]: isMenuSmall,
[classes.isMenuSmallDark]: isDark [classes.logoDark]: isDark
})} })}
onClick={handleIsMenuSmall}
> >
<SVG src={menuArrowIcon} /> <SVG
src={isMenuSmall ? saleorDarkLogoSmall : saleorDarkLogo}
/>
</div> </div>
</Hidden> <Hidden smDown>
<MenuList <div
className={isMenuSmall ? classes.menuSmall : classes.menu} className={classNames(classes.isMenuSmall, {
menuItems={menuStructure} [classes.isMenuSmallHide]: isMenuSmall,
isMenuSmall={!isMenuSmall} [classes.isMenuSmallDark]: isDark
location={location.pathname} })}
user={user} onClick={handleIsMenuSmall}
renderConfigure={renderConfigure} >
onMenuItemClick={handleMenuItemClick} <SVG src={menuArrowIcon} />
/> </div>
</ResponsiveDrawer> </Hidden>
</div> <MenuList
<div className={isMenuSmall ? classes.menuSmall : classes.menu}
className={classNames(classes.content, { menuItems={menuStructure}
[classes.contentToggle]: isMenuSmall isMenuSmall={!isMenuSmall}
})} location={location.pathname}
> user={user}
{appState.loading ? ( renderConfigure={renderConfigure}
<LinearProgress className={classes.appLoader} color="primary" /> onMenuItemClick={handleMenuItemClick}
) : ( />
<div className={classes.appLoaderPlaceholder} /> </ResponsiveDrawer>
)} </div>
<div className={classes.viewContainer}> <div
<div> className={classNames(classes.content, {
<Container> [classes.contentToggle]: isMenuSmall
<div className={classes.header}> })}
<div >
className={classNames(classes.menuIcon, { {appState.loading ? (
[classes.menuIconOpen]: isDrawerOpened, <LinearProgress className={classes.appLoader} color="primary" />
[classes.menuIconDark]: isDark ) : (
})} <div className={classes.appLoaderPlaceholder} />
onClick={() => setDrawerState(!isDrawerOpened)} )}
> <div className={classes.viewContainer}>
<span /> <div>
<span /> <Container>
<span /> <div className={classes.header}>
<span /> <div
</div> className={classNames(classes.menuIcon, {
<div ref={appHeaderAnchor} /> [classes.menuIconOpen]: isDrawerOpened,
<div className={classes.spacer} /> [classes.menuIconDark]: isDark
<div className={classes.userBar}> })}
<ThemeSwitch onClick={() => setDrawerState(!isDrawerOpened)}
className={classes.darkThemeSwitch} >
checked={isDark} <span />
onClick={toggleTheme} <span />
/> <span />
<div className={classes.userMenuContainer} ref={anchor}> <span />
<Chip </div>
avatar={ <div ref={appHeaderAnchor} />
user.avatar && ( <div className={classes.spacer} />
<Avatar alt="user" src={user.avatar.url} /> <div className={classes.userBar}>
) <ThemeSwitch
} className={classes.darkThemeSwitch}
classes={{ checked={isDark}
avatar: classes.avatar onClick={toggleTheme}
}}
className={classes.userChip}
label={
<>
{user.email}
<ArrowDropdown
className={classNames(classes.arrow, {
[classes.rotate]: isMenuOpened
})}
/>
</>
}
onClick={() => setMenuState(!isMenuOpened)}
data-test="userMenu"
/> />
<Popper <NavigatorButton
className={classes.popover} isMac={navigator.platform
open={isMenuOpened} .toLowerCase()
anchorEl={anchor.current} .includes("mac")}
transition onClick={() => setNavigatorVisibility(true)}
placement="bottom-end" />
> <div className={classes.userMenuContainer} ref={anchor}>
{({ TransitionProps, placement }) => ( <Chip
<Grow avatar={
{...TransitionProps} user.avatar && (
style={{ <Avatar alt="user" src={user.avatar.url} />
transformOrigin: )
placement === "bottom" }
? "right top" classes={{
: "right bottom" avatar: classes.avatar
}} }}
> className={classes.userChip}
<Paper> label={
<ClickAwayListener <>
onClickAway={() => setMenuState(false)} {user.email}
mouseEvent="onClick" <ArrowDropdown
> className={classNames(classes.arrow, {
<Menu> [classes.rotate]: isMenuOpened
<MenuItem })}
className={classes.userMenuItem} />
onClick={handleViewerProfile} </>
data-test="accountSettingsButton" }
> onClick={() => setMenuState(!isMenuOpened)}
<FormattedMessage data-test="userMenu"
defaultMessage="Account Settings" />
description="button" <Popper
/> className={classes.popover}
</MenuItem> open={isMenuOpened}
<MenuItem anchorEl={anchor.current}
className={classes.userMenuItem} transition
onClick={handleLogout} placement="bottom-end"
data-test="logOutButton" >
> {({ TransitionProps, placement }) => (
<FormattedMessage <Grow
defaultMessage="Log out" {...TransitionProps}
description="button" style={{
/> transformOrigin:
</MenuItem> placement === "bottom"
</Menu> ? "right top"
</ClickAwayListener> : "right bottom"
</Paper> }}
</Grow> >
)} <Paper>
</Popper> <ClickAwayListener
onClickAway={() => setMenuState(false)}
mouseEvent="onClick"
>
<Menu>
<MenuItem
className={classes.userMenuItem}
onClick={handleViewerProfile}
data-test="accountSettingsButton"
>
<FormattedMessage
defaultMessage="Account Settings"
description="button"
/>
</MenuItem>
<MenuItem
className={classes.userMenuItem}
onClick={handleLogout}
data-test="logOutButton"
>
<FormattedMessage
defaultMessage="Log out"
description="button"
/>
</MenuItem>
</Menu>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</div>
</div> </div>
</div> </div>
</div> </Container>
</Container> </div>
<main className={classes.view}>
{appState.error
? appState.error === "unhandled" && (
<ErrorPage onBack={handleErrorBack} />
)
: children}
</main>
</div> </div>
<main className={classes.view}> <div className={classes.appAction} ref={appActionAnchor} />
{appState.error
? appState.error === "unhandled" && (
<ErrorPage onBack={handleErrorBack} />
)
: children}
</main>
</div> </div>
<div className={classes.appAction} ref={appActionAnchor} />
</div> </div>
</div> </AppActionContext.Provider>
</AppActionContext.Provider> </AppHeaderContext.Provider>
</AppHeaderContext.Provider> </>
); );
}; };

View file

@ -60,8 +60,12 @@ const useStyles = makeStyles(
} }
); );
const Navigator: React.FC = () => { export interface NavigatorProps {
const [visible, setVisible] = React.useState(false); visible: boolean;
setVisibility: (state: boolean) => void;
}
const Navigator: React.FC<NavigatorProps> = ({ visible, setVisibility }) => {
const input = React.useRef(null); const input = React.useRef(null);
const [query, mode, change, actions] = useQuickSearch(visible, input); const [query, mode, change, actions] = useQuickSearch(visible, input);
const intl = useIntl(); const intl = useIntl();
@ -76,7 +80,7 @@ const Navigator: React.FC = () => {
React.useEffect(() => { React.useEffect(() => {
hotkeys(navigatorHotkey, event => { hotkeys(navigatorHotkey, event => {
event.preventDefault(); event.preventDefault();
setVisible(!visible); setVisibility(!visible);
}); });
if (cmp(APP_VERSION, "2.1.0") !== 1 && !notifiedAboutNavigator) { if (cmp(APP_VERSION, "2.1.0") !== 1 && !notifiedAboutNavigator) {
@ -110,7 +114,7 @@ const Navigator: React.FC = () => {
<Modal <Modal
className={classes.modal} className={classes.modal}
open={visible} open={visible}
onClose={() => setVisible(false)} onClose={() => setVisibility(false)}
> >
<Fade appear in={visible} timeout={theme.transitions.duration.short}> <Fade appear in={visible} timeout={theme.transitions.duration.short}>
<div className={classes.root}> <div className={classes.root}>
@ -122,7 +126,7 @@ const Navigator: React.FC = () => {
onSelect={(item: QuickSearchAction) => { onSelect={(item: QuickSearchAction) => {
const shouldRemainVisible = item.onClick(); const shouldRemainVisible = item.onClick();
if (!shouldRemainVisible) { if (!shouldRemainVisible) {
setVisible(false); setVisibility(false);
} }
}} }}
onInputValueChange={value => onInputValueChange={value =>

View file

@ -0,0 +1,12 @@
import CardDecorator from "@saleor/storybook/CardDecorator";
import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import NavigatorButton from "./NavigatorButton";
storiesOf("Generics / NavigatorButton", module)
.addDecorator(Decorator)
.addDecorator(CardDecorator)
.add("mac", () => <NavigatorButton isMac={true} />)
.add("other", () => <NavigatorButton isMac={false} />);

View file

@ -0,0 +1,153 @@
import navigatorIcon from "@assets/images/navigator.svg";
import Grow from "@material-ui/core/Grow";
import IconButton, { IconButtonProps } from "@material-ui/core/IconButton";
import Paper from "@material-ui/core/Paper";
import Popper from "@material-ui/core/Popper";
import makeStyles from "@material-ui/core/styles/makeStyles";
import classNames from "classnames";
import React from "react";
import ReactSVG from "react-inlinesvg";
import { FormattedMessage } from "react-intl";
const useStyles = makeStyles(
theme => {
const triangle = (color: string, width: number) => ({
borderBottom: `${width}px solid ${color}`,
borderLeft: `${width}px solid transparent`,
borderRight: `${width}px solid transparent`,
height: 0,
width: 0
});
return {
keyTile: {
"&:first-child": {
marginLeft: theme.spacing()
},
alignItems: "center",
background: theme.palette.background.default,
borderRadius: 8,
display: "inline-flex",
height: 32,
justifyContent: "center",
marginLeft: theme.spacing(0.5),
padding: theme.spacing(),
width: 32
},
keyTileLabel: {
verticalAlign: "middle"
},
paper: {
"&:after": {
...triangle(theme.palette.background.paper, 7),
content: "''",
left: theme.spacing(2) + 2,
position: "absolute",
top: -theme.spacing() + 1
},
"&:before": {
...triangle(theme.palette.divider, 8),
content: "''",
left: theme.spacing(2) + 1,
position: "absolute",
top: -theme.spacing()
},
border: `1px solid ${theme.palette.divider}`,
borderRadius: 6,
marginTop: theme.spacing(2),
padding: theme.spacing(2),
position: "relative"
},
root: {
"& path": {
color: theme.palette.primary.main
},
"&:not(:hover)": {
backgroundColor: theme.palette.background.paper
},
border: `1px solid ${theme.palette.divider}`,
height: 40,
marginRight: theme.spacing(2),
width: 40
}
};
},
{
name: "NavigatorButton"
}
);
export interface NavigatorButtonProps extends IconButtonProps {
isMac: boolean;
}
const NavigatorButton: React.FC<NavigatorButtonProps> = ({
className,
isMac,
...props
}) => {
const classes = useStyles({});
const helperTimer = React.useRef(null);
const [helperVisibility, setHelperVisibility] = React.useState(false);
const anchor = React.useRef<HTMLButtonElement>();
const setHelper = () => {
helperTimer.current = setTimeout(() => setHelperVisibility(true), 2 * 1000);
};
const clearHelper = () => {
if (helperTimer.current) {
clearTimeout(helperTimer.current);
helperTimer.current = null;
}
setHelperVisibility(false);
};
return (
<>
<IconButton
className={classNames(className, classes.root)}
data-test="navigator"
onMouseEnter={setHelper}
onMouseLeave={clearHelper}
{...props}
ref={anchor}
>
<ReactSVG src={navigatorIcon} />
</IconButton>
<Popper
open={helperVisibility}
anchorEl={anchor.current}
transition
disablePortal
placement="bottom-start"
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin:
placement === "bottom" ? "right top" : "right bottom"
}}
>
<Paper className={classes.paper} elevation={0}>
<FormattedMessage defaultMessage="Navigator" />
<div className={classes.keyTile}>
<span className={classes.keyTileLabel}>
{isMac ? "⌘" : "Ctrl"}
</span>
</div>
<div className={classes.keyTile}>
<span className={classes.keyTileLabel}>K</span>
</div>
</Paper>
</Grow>
)}
</Popper>
</>
);
};
NavigatorButton.displayName = "NavigatorButton";
export default NavigatorButton;

View file

@ -1,4 +1,3 @@
import Navigator from "@saleor/components/Navigator";
import useAppState from "@saleor/hooks/useAppState"; import useAppState from "@saleor/hooks/useAppState";
import { defaultDataIdFromObject, InMemoryCache } from "apollo-cache-inmemory"; import { defaultDataIdFromObject, InMemoryCache } from "apollo-cache-inmemory";
import { ApolloClient } from "apollo-client"; import { ApolloClient } from "apollo-client";
@ -139,7 +138,6 @@ const Routes: React.FC = () => {
<WindowTitle title={intl.formatMessage(commonMessages.dashboard)} /> <WindowTitle title={intl.formatMessage(commonMessages.dashboard)} />
{isAuthenticated && !tokenAuthLoading && !tokenVerifyLoading ? ( {isAuthenticated && !tokenAuthLoading && !tokenVerifyLoading ? (
<AppLayout> <AppLayout>
<Navigator />
<ErrorBoundary <ErrorBoundary
onError={() => onError={() =>
dispatchAppState({ dispatchAppState({

View file

@ -4670,6 +4670,66 @@ exports[`Storyshots Generics / Multiple select with autocomplete no data 1`] = `
</div> </div>
`; `;
exports[`Storyshots Generics / NavigatorButton mac 1`] = `
<div
class="MuiPaper-root-id MuiPaper-elevation0-id MuiCard-root-id MuiPaper-rounded-id"
style="margin:auto;overflow:visible;position:relative;width:400px"
>
<div
class="MuiCardContent-root-id"
>
<div
style="padding:24px"
>
<button
class="MuiButtonBase-root-id MuiIconButton-root-id NavigatorButton-root-id"
data-test="navigator"
tabindex="0"
type="button"
>
<span
class="MuiIconButton-label-id"
>
<span
class="isvg pending"
/>
</span>
</button>
</div>
</div>
</div>
`;
exports[`Storyshots Generics / NavigatorButton other 1`] = `
<div
class="MuiPaper-root-id MuiPaper-elevation0-id MuiCard-root-id MuiPaper-rounded-id"
style="margin:auto;overflow:visible;position:relative;width:400px"
>
<div
class="MuiCardContent-root-id"
>
<div
style="padding:24px"
>
<button
class="MuiButtonBase-root-id MuiIconButton-root-id NavigatorButton-root-id"
data-test="navigator"
tabindex="0"
type="button"
>
<span
class="MuiIconButton-label-id"
>
<span
class="isvg pending"
/>
</span>
</button>
</div>
</div>
</div>
`;
exports[`Storyshots Generics / PageHeader with title 1`] = ` exports[`Storyshots Generics / PageHeader with title 1`] = `
<div <div
style="padding:24px" style="padding:24px"