diff --git a/assets/images/logo-sidebar-light.svg b/assets/images/logo-sidebar-light.svg new file mode 100644 index 000000000..6d186cce1 --- /dev/null +++ b/assets/images/logo-sidebar-light.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/menu-apps-icon.svg b/assets/images/menu-apps-icon.svg index 3033691d1..85d36954a 100644 --- a/assets/images/menu-apps-icon.svg +++ b/assets/images/menu-apps-icon.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/images/menu-catalog-icon.svg b/assets/images/menu-catalog-icon.svg index 6b67b5aac..159804b6d 100644 --- a/assets/images/menu-catalog-icon.svg +++ b/assets/images/menu-catalog-icon.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/images/menu-configure-icon.svg b/assets/images/menu-configure-icon.svg index c1defe727..7a435af97 100644 --- a/assets/images/menu-configure-icon.svg +++ b/assets/images/menu-configure-icon.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/images/menu-customers-icon.svg b/assets/images/menu-customers-icon.svg index b46a2fc1e..989bc7b0d 100644 --- a/assets/images/menu-customers-icon.svg +++ b/assets/images/menu-customers-icon.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/images/menu-discounts-icon.svg b/assets/images/menu-discounts-icon.svg index 6f15dce79..30ca87a8e 100644 --- a/assets/images/menu-discounts-icon.svg +++ b/assets/images/menu-discounts-icon.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/images/menu-home-icon.svg b/assets/images/menu-home-icon.svg index f91bd00a1..2c90eedf3 100644 --- a/assets/images/menu-home-icon.svg +++ b/assets/images/menu-home-icon.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/images/menu-orders-icon.svg b/assets/images/menu-orders-icon.svg index 1e97d098d..74603fe7a 100644 --- a/assets/images/menu-orders-icon.svg +++ b/assets/images/menu-orders-icon.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/images/menu-translation-icon.svg b/assets/images/menu-translation-icon.svg index 7d034dd68..e869dedd1 100644 --- a/assets/images/menu-translation-icon.svg +++ b/assets/images/menu-translation-icon.svg @@ -1,3 +1,3 @@ - - + + diff --git a/cypress/elements/account/left-menu/left-menu-selectors.js b/cypress/elements/account/left-menu/left-menu-selectors.js index 453e686ca..fb525ee06 100644 --- a/cypress/elements/account/left-menu/left-menu-selectors.js +++ b/cypress/elements/account/left-menu/left-menu-selectors.js @@ -1,4 +1,4 @@ /* eslint-disable sort-keys */ export const LEFT_MENU_SELECTORS = { - catalog: "[data-testid='catalogue']" + catalog: "[data-test-id='catalogue']" }; diff --git a/cypress/elements/catalog/product-selectors.js b/cypress/elements/catalog/product-selectors.js index f2f584994..b52e0ca0d 100644 --- a/cypress/elements/catalog/product-selectors.js +++ b/cypress/elements/catalog/product-selectors.js @@ -1,6 +1,6 @@ /* eslint-disable sort-keys */ export const PRODUCTS_SELECTORS = { - products: "[href='/dashboard/products?']", + products: "[data-test='submenu-item-label'][data-test-id='products']", createProductBtn: "[data-test='add-product']", productNameInput: "[name='name']", productTypeInput: "[data-test='product-type']", diff --git a/cypress/integration/warehouse.js b/cypress/integration/warehouse.js index 7ed06c295..208321164 100644 --- a/cypress/integration/warehouse.js +++ b/cypress/integration/warehouse.js @@ -7,7 +7,7 @@ describe("Warehouse settings", () => { xit("Warehouse section visible in the configuration", () => { cy.visit("/configuration/") .loginUser() - .get("[data-testid=warehouses][data-test=settingsSubsection]") + .get("[data-test-id=warehouses][data-test=settingsSubsection]") .click(); cy.location("pathname").should("eq", "/warehouses/"); }); diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index be6b540ed..0ca873b4f 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -1444,14 +1444,6 @@ "src_dot_components_dot_AddressEdit_dot_944851093": { "string": "Country area" }, - "src_dot_components_dot_AppLayout_dot_21332146": { - "context": "button", - "string": "Log out" - }, - "src_dot_components_dot_AppLayout_dot_248888005": { - "context": "button", - "string": "Account Settings" - }, "src_dot_components_dot_AppStatus_dot_1624959454": { "string": "If you want to disable this App please uncheck the box below." }, @@ -2007,6 +1999,14 @@ "src_dot_components_dot_Timeline_dot_3028189627": { "string": "Leave your note here..." }, + "src_dot_components_dot_UserChip_dot_21332146": { + "context": "button", + "string": "Log out" + }, + "src_dot_components_dot_UserChip_dot_248888005": { + "context": "button", + "string": "Account Settings" + }, "src_dot_components_dot_VisibilityCard_dot_1311467573": { "string": "Show in product listings" }, diff --git a/src/components/AppHeader/AppHeader.tsx b/src/components/AppHeader/AppHeader.tsx index f4bdc4ab4..d38731269 100644 --- a/src/components/AppHeader/AppHeader.tsx +++ b/src/components/AppHeader/AppHeader.tsx @@ -34,7 +34,7 @@ const useStyles = makeStyles( marginTop: theme.spacing(0.5), transition: theme.transitions.duration.standard + "ms", [theme.breakpoints.down("sm")]: { - display: "none" + margin: theme.spacing(4, 0, 0, 0) } }, skeleton: { @@ -45,10 +45,7 @@ const useStyles = makeStyles( color: "inherit", flex: 1, marginLeft: theme.spacing(), - textTransform: "uppercase", - [theme.breakpoints.down("sm")]: { - display: "none" - } + textTransform: "uppercase" } }), { name: "AppHeader" } diff --git a/src/components/AppLayout/AppLayout.tsx b/src/components/AppLayout/AppLayout.tsx index 3b1b09def..fed6e6c41 100644 --- a/src/components/AppLayout/AppLayout.tsx +++ b/src/components/AppLayout/AppLayout.tsx @@ -1,41 +1,28 @@ -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 Avatar from "@material-ui/core/Avatar"; -import Chip from "@material-ui/core/Chip"; -import ClickAwayListener from "@material-ui/core/ClickAwayListener"; -import Grow from "@material-ui/core/Grow"; -import Hidden from "@material-ui/core/Hidden"; import LinearProgress from "@material-ui/core/LinearProgress"; -import MenuItem from "@material-ui/core/MenuItem"; -import Menu from "@material-ui/core/MenuList"; -import Paper from "@material-ui/core/Paper"; -import Popper from "@material-ui/core/Popper"; -import { makeStyles } from "@material-ui/core/styles"; +import { makeStyles, Theme } from "@material-ui/core/styles"; +import useMediaQuery from "@material-ui/core/useMediaQuery"; 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"; import useUser from "@saleor/hooks/useUser"; -import ArrowDropdown from "@saleor/icons/ArrowDropdown"; import { staffMemberDetailsUrl } from "@saleor/staff/urls"; import classNames from "classnames"; import React from "react"; -import SVG from "react-inlinesvg"; -import { FormattedMessage, useIntl } from "react-intl"; +import { useIntl } from "react-intl"; import useRouter from "use-react-router"; import Container from "../Container"; import ErrorPage from "../ErrorPage"; import Navigator from "../Navigator"; import NavigatorButton from "../NavigatorButton/NavigatorButton"; +import SideBar from "../SideBar"; +import SideBarDrawer from "../SideBarDrawer/SideBarDrawer"; +import UserChip from "../UserChip"; import AppActionContext from "./AppActionContext"; import AppHeaderContext from "./AppHeaderContext"; -import { appLoaderHeight, drawerWidth, drawerWidthExpanded } from "./consts"; -import MenuList from "./MenuList"; +import { appLoaderHeight } from "./consts"; import createMenuStructure from "./menuStructure"; -import ResponsiveDrawer from "./ResponsiveDrawer"; import ThemeSwitch from "./ThemeSwitch"; const useStyles = makeStyles( @@ -55,231 +42,58 @@ const useStyles = makeStyles( }, appLoader: { height: appLoaderHeight, - marginBottom: theme.spacing(2), + marginBottom: theme.spacing(4), zIndex: 1201 }, appLoaderPlaceholder: { height: appLoaderHeight, - marginBottom: theme.spacing(2) - }, - arrow: { - marginLeft: theme.spacing(2), - transition: theme.transitions.duration.standard + "ms" - }, - avatar: { - "&&": { - height: 32, - width: 32 - } + marginBottom: theme.spacing(4) }, + content: { - [theme.breakpoints.down("sm")]: { - paddingLeft: 0 - }, - paddingLeft: drawerWidthExpanded, - transition: "padding-left 0.5s ease", - width: "100%" - }, - contentToggle: { - [theme.breakpoints.down("sm")]: { - paddingLeft: 0 - }, - paddingLeft: drawerWidth + flex: 1 }, darkThemeSwitch: { [theme.breakpoints.down("sm")]: { - marginRight: -theme.spacing(1.5) + marginRight: theme.spacing(1) }, marginRight: theme.spacing(2) }, header: { + display: "grid", + gridTemplateAreas: `"headerAnchor headerToolbar"`, [theme.breakpoints.down("sm")]: { - height: 88, - marginBottom: 0 + gridTemplateAreas: `"headerToolbar" + "headerAnchor"` }, - display: "flex", - height: 40, marginBottom: theme.spacing(3) }, - isMenuSmall: { - "& path": { - fill: theme.palette.primary.main - }, - "& span": { - margin: "0 8px" - }, - "& svg": { - marginTop: 8, - transform: "rotate(180deg)" - }, - "&:hover": { - background: "#E6F3F3" - }, - background: theme.palette.background.paper, - border: `solid 1px #EAEAEA`, - borderRadius: "50%", - cursor: "pointer", - height: 32, - position: "absolute", - right: -16, - top: 65, - transition: `background ${theme.transitions.duration.shorter}ms`, - width: 32, - zIndex: 99 + headerAnchor: { + gridArea: "headerAnchor" }, - isMenuSmallDark: { - "&:hover": { - background: `linear-gradient(0deg, rgba(25, 195, 190, 0.1), rgba(25, 195, 190, 0.1)), ${theme.palette.background.paper}` - }, - border: `solid 1px #252728`, - transition: `background ${theme.transitions.duration.shorter}ms` - }, - isMenuSmallHide: { - "& svg": { - marginLeft: "3px", - transform: "rotate(0deg)" - } - }, - logo: { - "& svg": { - left: "50%", - position: "absolute", - top: "50%", - transform: "translate(-50%,-50%)" - }, - background: theme.palette.secondary.main, - display: "block", - height: 80, - position: "relative" - }, - logoDark: { - "& path": { - fill: theme.palette.common.white - }, - background: theme.palette.primary.main - }, - logoSmall: { - "& svg": { - margin: 0, - padding: 0, - width: "80px" - } - }, - menu: { - background: theme.palette.background.paper, - height: "100vh", - padding: "25px 20px" - }, - menuIcon: { - "& span": { - "&:nth-child(1)": { - top: 15 - }, - "&:nth-child(2), &:nth-child(3)": { - top: 20 - }, - "&:nth-child(4)": { - top: 25 - }, - background: theme.palette.secondary.light, - display: "block", - height: 1, - left: "20%", - opacity: 1, - position: "absolute", - transform: "rotate(0deg)", - transition: ".25s ease-in-out", - width: "60%" - }, - [theme.breakpoints.up("md")]: { - display: "none" - }, + headerToolbar: { + display: "flex", + gridArea: "headerToolbar", + height: 40, [theme.breakpoints.down("sm")]: { - left: 0 - }, - background: theme.palette.background.paper, - borderRadius: "50%", - cursor: "pointer", - height: 42, - left: theme.spacing(), - marginRight: theme.spacing(2), - position: "relative", - transform: "rotate(0deg)", - transition: `${theme.transitions.duration.shorter}ms ease-in-out`, - width: 42 - }, - menuIconDark: { - "& span": { - background: theme.palette.common.white + height: "auto" } }, - menuIconOpen: { - "& span": { - "&:nth-child(1), &:nth-child(4)": { - left: "50%", - top: 20, - width: 0 - }, - "&:nth-child(2)": { - transform: "rotate(45deg)" - }, - "&:nth-child(3)": { - transform: "rotate(-45deg)" - } - }, - left: 280, - position: "absolute", - zIndex: 1999 - }, - menuSmall: { - background: theme.palette.background.paper, - height: "100vh", - overflow: "hidden", - padding: 25 - }, - popover: { - zIndex: 2 - }, root: { - width: `100%` - }, - rotate: { - transform: "rotate(180deg)" - }, - sideBar: { - [theme.breakpoints.down("sm")]: { - padding: 0 + [theme.breakpoints.up("md")]: { + display: "flex" }, - background: theme.palette.background.paper, - padding: `0 ${theme.spacing(4)}px` + width: `100%` }, spacer: { flex: 1 }, userBar: { - [theme.breakpoints.down("sm")]: { - alignItems: "flex-end", - flexDirection: "column-reverse", - overflow: "hidden" - }, alignItems: "center", display: "flex" }, - userChip: { - backgroundColor: theme.palette.background.paper, - borderRadius: 24, - color: theme.palette.text.primary, - height: 40, - padding: theme.spacing(0.5) - }, - userMenuContainer: { - position: "relative" - }, - userMenuItem: { - textAlign: "right" - }, + view: { - backgroundColor: theme.palette.background.default, flex: 1, flexGrow: 1, marginLeft: 0, @@ -304,18 +118,15 @@ interface AppLayoutProps { 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 [isNavigatorVisible, setNavigatorVisibility] = React.useState(false); + const isMdUp = useMediaQuery((theme: Theme) => theme.breakpoints.up("md")); const [docked, setDocked] = React.useState(true); const menuStructure = createMenuStructure(intl); @@ -331,27 +142,6 @@ const AppLayout: React.FC = ({ children }) => { ) ); - const handleLogout = () => { - setMenuState(false); - logout(); - }; - - const handleViewerProfile = () => { - setMenuState(false); - navigate(staffMemberDetailsUrl(user.id)); - }; - - const handleMenuItemClick = (url: string, event: React.MouseEvent) => { - event.stopPropagation(); - event.preventDefault(); - setDrawerState(false); - navigate(url); - }; - - const handleIsMenuSmall = () => { - setMenuSmall(!isMenuSmall); - }; - const handleErrorBack = () => { navigate("/"); dispatchAppState({ @@ -377,49 +167,16 @@ const AppLayout: React.FC = ({ children }) => { }} >
-
- setDrawerState(false)} - open={isDrawerOpened} - small={!isMenuSmall} - > -
- -
- -
- -
-
- -
-
-
+ {isMdUp && ( + + )} +
{appState.loading ? ( ) : ( @@ -430,106 +187,39 @@ const AppLayout: React.FC = ({ children }) => {
setDrawerState(!isDrawerOpened)} - > - - - - -
-
-
-
- - + className={classes.headerAnchor} + ref={appHeaderAnchor} + /> +
+ {!isMdUp && ( + + )} +
+
+ setNavigatorVisibility(true)} /> - -
- - ) + + navigate(staffMemberDetailsUrl(user.id)) } - classes={{ - avatar: classes.avatar - }} - className={classes.userChip} - label={ - <> - {user.email} - - - } - onClick={() => setMenuState(!isMenuOpened)} - data-test="userMenu" + user={user} /> - - {({ TransitionProps, placement }) => ( - - - setMenuState(false)} - mouseEvent="onClick" - > - - - - - - - - - - - - )} -
diff --git a/src/components/Navigator/Navigator.tsx b/src/components/Navigator/Navigator.tsx index 569eede26..203c39b5e 100644 --- a/src/components/Navigator/Navigator.tsx +++ b/src/components/Navigator/Navigator.tsx @@ -49,6 +49,9 @@ const useStyles = makeStyles( overflow: "hidden" }, root: { + [theme.breakpoints.down("sm")]: { + height: "auto" + }, height: 500, maxWidth: 600, outline: 0, diff --git a/src/components/NavigatorButton/NavigatorButton.tsx b/src/components/NavigatorButton/NavigatorButton.tsx index ad3189280..5e3c07341 100644 --- a/src/components/NavigatorButton/NavigatorButton.tsx +++ b/src/components/NavigatorButton/NavigatorButton.tsx @@ -66,6 +66,10 @@ const useStyles = makeStyles( "&:not(:hover)": { backgroundColor: theme.palette.background.paper }, + [theme.breakpoints.down("sm")]: { + border: "none", + borderRadius: 16 + }, border: `1px solid ${theme.palette.divider}`, height: 40, marginRight: theme.spacing(2), diff --git a/src/components/SideBar/ExpandButton.tsx b/src/components/SideBar/ExpandButton.tsx new file mode 100644 index 000000000..379d6be95 --- /dev/null +++ b/src/components/SideBar/ExpandButton.tsx @@ -0,0 +1,42 @@ +import { ButtonProps } from "@material-ui/core/Button"; +import makeStyles from "@material-ui/core/styles/makeStyles"; +import ArrowIcon from "@material-ui/icons/ArrowBack"; +import classNames from "classnames"; +import React from "react"; + +import SquareButton from "../SquareButton"; + +const useStyles = makeStyles( + theme => ({ + arrow: { + transition: theme.transitions.duration.shortest + "ms" + }, + shrunk: { + transform: "scaleX(-1)" + } + }), + { + name: "ExpandButton" + } +); + +export interface ExpandButtonProps extends ButtonProps { + isShrunk: boolean; +} + +const ExpandButton: React.FC = ({ isShrunk, ...rest }) => { + const classes = useStyles({}); + + return ( + + + + ); +}; + +ExpandButton.displayName = "ExpandButton"; +export default ExpandButton; diff --git a/src/components/SideBar/MenuItem.tsx b/src/components/SideBar/MenuItem.tsx new file mode 100644 index 000000000..77462c507 --- /dev/null +++ b/src/components/SideBar/MenuItem.tsx @@ -0,0 +1,199 @@ +import ClickAwayListener from "@material-ui/core/ClickAwayListener"; +import Paper from "@material-ui/core/Paper"; +import Popper from "@material-ui/core/Popper"; +import { fade } from "@material-ui/core/styles/colorManipulator"; +import makeStyles from "@material-ui/core/styles/makeStyles"; +import Typography from "@material-ui/core/Typography"; +import { UseNavigatorResult } from "@saleor/hooks/useNavigator"; +import classNames from "classnames"; +import React from "react"; +import SVG from "react-inlinesvg"; + +import { IMenuItem } from "../AppLayout/menuStructure"; + +export interface MenuItemProps { + active: boolean; + isMenuShrunk: boolean; + menuItem: IMenuItem; + onClick: UseNavigatorResult; +} + +export const menuWidth = 210; +export const shrunkMenuWidth = 72; + +const useStyles = makeStyles( + theme => ({ + hideLabel: { + "&$label": { + opacity: 0 + } + }, + icon: { + "& svg": { + height: 24, + width: 24 + }, + marginRight: theme.spacing(1.5), + transition: theme.transitions.duration.shortest + "ms" + }, + label: { + cursor: "pointer", + display: "block", + fontSize: 16, + fontWeight: "bold", + opacity: 1, + transition: theme.transitions.duration.shortest + "ms" + }, + menuItemBtn: { + "&:focus": { + color: theme.palette.primary.main, + outline: 0 + }, + background: "none", + border: "none", + color: "inherit", + cursor: "pointer", + display: "inline-flex", + margin: 0, + padding: 0 + }, + paper: { + borderRadius: 16, + boxShadow: "0px 6px 30px rgba(0, 0, 0, 0.16)", + cursor: "default", + padding: theme.spacing(3), + textAlign: "left" + }, + popper: { + marginLeft: theme.spacing(3), + zIndex: 2 + }, + root: { + "&:hover, &:focus": { + color: theme.palette.primary.main, + outline: 0 + }, + borderBottomRightRadius: 100, + borderTopRightRadius: 100, + color: fade(theme.palette.text.primary, 0.6), + cursor: "pointer", + display: "flex", + height: 56, + marginBottom: theme.spacing(), + overflow: "hidden", + padding: theme.spacing(2, 3, 2, 3.5), + transition: theme.transitions.duration.shortest + "ms", + width: shrunkMenuWidth + }, + rootActive: { + "&$root": { + background: theme.palette.background.paper, + boxShadow: "0px 6px 30px rgba(0, 0, 0, 0.16)", + color: theme.palette.primary.main + } + }, + rootExpanded: { + width: menuWidth + }, + subMenuLabel: { + "&$label": { + "&:not(:last-child)": { + marginBottom: theme.spacing(2) + } + }, + "&:hover, &:focus": { + color: theme.palette.primary.main, + outline: 0 + }, + background: "none", + border: "none", + color: fade(theme.palette.text.primary, 0.6), + textAlign: "left", + whiteSpace: "nowrap" + } + }), + { + name: "MenuItem" + } +); + +const MenuItem: React.FC = ({ + active, + menuItem, + isMenuShrunk, + onClick +}) => { + const classes = useStyles({}); + const [open, setOpen] = React.useState(false); + const anchor = React.useRef(null); + + const handleClick = (event: React.MouseEvent, menuItem: IMenuItem) => { + event.stopPropagation(); + if (menuItem.children) { + setOpen(true); + } else { + onClick(menuItem.url); + setOpen(false); + } + }; + + return ( +
handleClick(event, menuItem)} + > + + {menuItem.children && ( + + setOpen(false)}> + + {menuItem.children.map(subMenuItem => ( + handleClick(event, subMenuItem)} + data-test="submenu-item-label" + data-test-id={subMenuItem.testingContextId} + variant="body2" + > + {subMenuItem.label} + + ))} + + + + )} +
+ ); +}; + +MenuItem.displayName = "MenuItem"; +export default MenuItem; diff --git a/src/components/SideBar/SideBar.tsx b/src/components/SideBar/SideBar.tsx new file mode 100644 index 000000000..77b4f3ee4 --- /dev/null +++ b/src/components/SideBar/SideBar.tsx @@ -0,0 +1,134 @@ +import logoLight from "@assets/images/logo-sidebar-light.svg"; +import configurationIcon from "@assets/images/menu-configure-icon.svg"; +import makeStyles from "@material-ui/core/styles/makeStyles"; +import { configurationMenuUrl } from "@saleor/configuration"; +import { User } from "@saleor/fragments/types/User"; +import useLocalStorage from "@saleor/hooks/useLocalStorage"; +import { UseNavigatorResult } from "@saleor/hooks/useNavigator"; +import { sectionNames } from "@saleor/intl"; +import classNames from "classnames"; +import React from "react"; +import SVG from "react-inlinesvg"; +import { IntlShape, useIntl } from "react-intl"; + +import { IMenuItem } from "../AppLayout/menuStructure"; +import ExpandButton from "./ExpandButton"; +import MenuItem, { menuWidth, shrunkMenuWidth } from "./MenuItem"; +import { isMenuActive } from "./utils"; + +const useStyles = makeStyles( + theme => ({ + expandButton: { + marginLeft: theme.spacing(2) + }, + float: { + position: "fixed" + }, + logo: { + margin: `36px 0 ${theme.spacing(3)}px ${theme.spacing(3)}px` + }, + root: { + transition: "width 0.5s ease", + width: menuWidth, + zIndex: 100 + }, + rootShrink: { + width: shrunkMenuWidth + } + }), + { + name: "SideBar" + } +); + +export interface SideBarProps { + className?: string; + menuItems: IMenuItem[]; + location: string; + user: User; + renderConfigure: boolean; + onMenuItemClick: UseNavigatorResult; +} + +export interface IActiveSubMenu { + isActive: boolean; + label: string | null; +} + +export const getConfigureMenuItem = (intl: IntlShape): IMenuItem => ({ + ariaLabel: "configure", + icon: configurationIcon, + label: intl.formatMessage(sectionNames.configuration), + testingContextId: "configure", + url: configurationMenuUrl +}); + +const SideBar: React.FC = ({ + location, + menuItems, + renderConfigure, + user, + onMenuItemClick +}) => { + const classes = useStyles({}); + const [isShrunk, setShrink] = useLocalStorage("isMenuSmall", false); + const intl = useIntl(); + const configureMenuItem = getConfigureMenuItem(intl); + + return ( +
+
+
+ +
+ {menuItems.map(menuItem => { + const isActive = isMenuActive(location, menuItem); + + if ( + menuItem.permission && + !user.userPermissions + .map(perm => perm.code) + .includes(menuItem.permission) + ) { + return null; + } + + return ( + + ); + })} + {renderConfigure && ( + acc || isMenuActive(location, menuItem), + false + ) + } + isMenuShrunk={isShrunk} + menuItem={configureMenuItem} + onClick={onMenuItemClick} + /> + )} + setShrink(!isShrunk)} + /> +
+
+ ); +}; + +SideBar.displayName = "SideBar"; +export default SideBar; diff --git a/src/components/SideBar/SubMenu.tsx b/src/components/SideBar/SubMenu.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/src/components/SideBar/index.ts b/src/components/SideBar/index.ts new file mode 100644 index 000000000..df08bddc7 --- /dev/null +++ b/src/components/SideBar/index.ts @@ -0,0 +1,2 @@ +export * from "./SideBar"; +export { default } from "./SideBar"; diff --git a/src/components/SideBar/utils.ts b/src/components/SideBar/utils.ts new file mode 100644 index 000000000..06827a7ac --- /dev/null +++ b/src/components/SideBar/utils.ts @@ -0,0 +1,24 @@ +import { orderDraftListUrl, orderListUrl } from "@saleor/orders/urls"; +import { matchPath } from "react-router"; + +import { IMenuItem } from "../AppLayout/menuStructure"; + +export function isMenuActive(location: string, menuItem: IMenuItem) { + if (menuItem.children) { + return menuItem.children.reduce( + (acc, subMenuItem) => acc || isMenuActive(location, subMenuItem), + false + ); + } + + const activeUrl = location.split("?")[0]; + const menuItemUrl = menuItem.url.split("?")[0]; + + return activeUrl === orderDraftListUrl().split("?")[0] && + menuItemUrl === orderListUrl().split("?")[0] + ? false + : !!matchPath(activeUrl, { + exact: menuItemUrl === "/", + path: menuItemUrl + }); +} diff --git a/src/components/SideBarDrawer/MenuItemBtn.tsx b/src/components/SideBarDrawer/MenuItemBtn.tsx new file mode 100644 index 000000000..9013d5f36 --- /dev/null +++ b/src/components/SideBarDrawer/MenuItemBtn.tsx @@ -0,0 +1,32 @@ +import Typography from "@material-ui/core/Typography"; +import { UseNavigatorResult } from "@saleor/hooks/useNavigator"; +import React from "react"; +import SVG from "react-inlinesvg"; + +import { IMenuItem } from "../AppLayout/menuStructure"; +import useStyles from "./styles"; + +export interface MenuItemBtnProps { + menuItem: IMenuItem; + onClick: UseNavigatorResult; +} +const MenuItemBtn: React.FC = ({ menuItem, onClick }) => { + const classes = useStyles({}); + + return ( + + ); +}; + +MenuItemBtn.displayName = "MenuItemBtn"; +export default MenuItemBtn; diff --git a/src/components/SideBarDrawer/SideBarDrawer.stories.tsx b/src/components/SideBarDrawer/SideBarDrawer.stories.tsx new file mode 100644 index 000000000..057ee7b90 --- /dev/null +++ b/src/components/SideBarDrawer/SideBarDrawer.stories.tsx @@ -0,0 +1,37 @@ +import { staffMember } from "@saleor/staff/fixtures"; +import Decorator from "@saleor/storybook/Decorator"; +import { storiesOf } from "@storybook/react"; +import { config } from "@test/intl"; +import React from "react"; +import { createIntl } from "react-intl"; + +import createMenuStructure from "../AppLayout/menuStructure"; +import SideBarDrawer from "./SideBarDrawer"; + +const intl = createIntl(config); +const user = { + __typename: staffMember.__typename, + avatar: { + __typename: staffMember.avatar.__typename, + url: staffMember.avatar.url + }, + email: staffMember.email, + firstName: "Adam Evan", + id: staffMember.id, + isStaff: true, + lastName: "Newton", + note: null, + userPermissions: staffMember.userPermissions +}; + +storiesOf("Generics / Mobile Side Menu", module) + .addDecorator(Decorator) + .add("default", () => ( + undefined} + renderConfigure={true} + user={user} + /> + )); diff --git a/src/components/SideBarDrawer/SideBarDrawer.tsx b/src/components/SideBarDrawer/SideBarDrawer.tsx new file mode 100644 index 000000000..e7963401c --- /dev/null +++ b/src/components/SideBarDrawer/SideBarDrawer.tsx @@ -0,0 +1,132 @@ +import logoLight from "@assets/images/logo-sidebar-light.svg"; +import { Typography } from "@material-ui/core"; +import Drawer from "@material-ui/core/Drawer"; +import ArrowLeftIcon from "@material-ui/icons/ArrowLeft"; +import MenuIcon from "@material-ui/icons/Menu"; +import classNames from "classnames"; +import React from "react"; +import SVG from "react-inlinesvg"; +import { useIntl } from "react-intl"; + +import { IMenuItem } from "../AppLayout/menuStructure"; +import { getConfigureMenuItem, SideBarProps } from "../SideBar/SideBar"; +import SquareButton from "../SquareButton"; +import MenuItemBtn from "./MenuItemBtn"; +import useStyles from "./styles"; + +export type SideBarDrawerProps = SideBarProps; + +const SideBarDrawer: React.FC = ({ + menuItems, + onMenuItemClick, + renderConfigure, + user +}) => { + const [isOpened, setOpened] = React.useState(false); + const classes = useStyles({}); + const intl = useIntl(); + const [activeMenu, setActiveMenu] = React.useState(null); + const [showSubmenu, setShowSubmenu] = React.useState(false); + const container = React.useRef(null); + + const configureMenuItem = getConfigureMenuItem(intl); + + const handleMenuItemClick = (url: string) => { + setOpened(false); + setShowSubmenu(false); + onMenuItemClick(url); + }; + + const handleMenuItemWithChildrenClick = (menuItem: IMenuItem) => { + setActiveMenu(menuItem); + setShowSubmenu(true); + container.current.scrollTo({ + top: 0 + }); + }; + + return ( + <> + setOpened(true)}> + + + setOpened(false)} + > +
+
+
+ + {menuItems.map(menuItem => { + if ( + menuItem.permission && + !user.userPermissions + .map(perm => perm.code) + .includes(menuItem.permission) + ) { + return null; + } + + return ( + handleMenuItemWithChildrenClick(menuItem) + : handleMenuItemClick + } + key={menuItem.ariaLabel} + /> + ); + })} + {renderConfigure && ( + + )} +
+ {activeMenu && ( +
+
+
+ + + {activeMenu.label} + +
+ setShowSubmenu(false)}> + + +
+ {activeMenu.children.map(subMenuItem => ( + + ))} +
+ )} +
+
+
+ + ); +}; + +SideBarDrawer.displayName = "SideBarDrawer"; +export default SideBarDrawer; diff --git a/src/components/SideBarDrawer/index.ts b/src/components/SideBarDrawer/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/components/SideBarDrawer/styles.ts b/src/components/SideBarDrawer/styles.ts new file mode 100644 index 000000000..92cda2a00 --- /dev/null +++ b/src/components/SideBarDrawer/styles.ts @@ -0,0 +1,68 @@ +import makeStyles from "@material-ui/core/styles/makeStyles"; + +const useStyles = makeStyles( + theme => ({ + activeMenuLabel: { + display: "flex" + }, + container: { + overflowX: "hidden", + width: "100%" + }, + containerSubMenu: { + "&$container": { + overflow: "hidden" + } + }, + content: { + width: "50%" + }, + icon: { + marginRight: theme.spacing(2) + }, + innerContainer: { + display: "flex", + position: "relative", + right: 0, + transition: theme.transitions.duration.short + "ms", + width: "200%" + }, + label: { + fontWeight: "bold" + }, + logo: { + display: "block", + marginBottom: theme.spacing(4) + }, + menuItemBtn: { + alignItems: "center", + background: "none", + border: "none", + color: theme.palette.text.secondary, + display: "flex", + marginBottom: theme.spacing(3), + padding: 0 + }, + root: { + background: theme.palette.background.default, + borderBottomRightRadius: 32, + borderTopRightRadius: 32, + padding: theme.spacing(3), + width: 260 + }, + secondaryContentActive: { + right: "100%" + }, + subMenuTopBar: { + alignItems: "center", + display: "flex", + justifyContent: "space-between", + marginBottom: theme.spacing(3) + } + }), + { + name: "SideBarDrawer" + } +); + +export default useStyles; diff --git a/src/components/SquareButton/SquareButton.stories.tsx b/src/components/SquareButton/SquareButton.stories.tsx new file mode 100644 index 000000000..2458c5d9f --- /dev/null +++ b/src/components/SquareButton/SquareButton.stories.tsx @@ -0,0 +1,14 @@ +import CloseIcon from "@material-ui/icons/Close"; +import Decorator from "@saleor/storybook/Decorator"; +import { storiesOf } from "@storybook/react"; +import React from "react"; + +import SquareButton from "./SquareButton"; + +storiesOf("Generics / Square Button", module) + .addDecorator(Decorator) + .add("default", () => ( + + + + )); diff --git a/src/components/SquareButton/SquareButton.tsx b/src/components/SquareButton/SquareButton.tsx new file mode 100644 index 000000000..5e69a2fd9 --- /dev/null +++ b/src/components/SquareButton/SquareButton.tsx @@ -0,0 +1,34 @@ +import ButtonBase, { ButtonBaseProps } from "@material-ui/core/ButtonBase"; +import makeStyles from "@material-ui/core/styles/makeStyles"; +import classNames from "classnames"; +import React from "react"; + +const useStyles = makeStyles( + theme => ({ + root: { + "&:hover, &:focus": { + background: "#daedeb" + }, + background: theme.palette.background.paper, + borderRadius: 16, + color: theme.palette.primary.main, + height: 48, + transition: theme.transitions.duration.shortest + "ms", + width: 48 + } + }), + { + name: "ExpandButton" + } +); + +const SquareButton: React.FC = ({ className, ...rest }) => { + const classes = useStyles({}); + + return ( + + ); +}; + +SquareButton.displayName = "SquareButton"; +export default SquareButton; diff --git a/src/components/SquareButton/index.ts b/src/components/SquareButton/index.ts new file mode 100644 index 000000000..7f406f96b --- /dev/null +++ b/src/components/SquareButton/index.ts @@ -0,0 +1,2 @@ +export * from "./SquareButton"; +export { default } from "./SquareButton"; diff --git a/src/components/UserChip/UserChip.tsx b/src/components/UserChip/UserChip.tsx new file mode 100644 index 000000000..165499ea7 --- /dev/null +++ b/src/components/UserChip/UserChip.tsx @@ -0,0 +1,183 @@ +import Avatar from "@material-ui/core/Avatar"; +import Chip from "@material-ui/core/Chip"; +import ClickAwayListener from "@material-ui/core/ClickAwayListener"; +import Grow from "@material-ui/core/Grow"; +import Hidden from "@material-ui/core/Hidden"; +import MenuItem from "@material-ui/core/MenuItem"; +import Menu from "@material-ui/core/MenuList"; +import Paper from "@material-ui/core/Paper"; +import Popper from "@material-ui/core/Popper"; +import makeStyles from "@material-ui/core/styles/makeStyles"; +import { User } from "@saleor/fragments/types/User"; +import ArrowDropdown from "@saleor/icons/ArrowDropdown"; +import { getUserInitials, getUserName } from "@saleor/misc"; +import classNames from "classnames"; +import React from "react"; +import { FormattedMessage } from "react-intl"; + +const useStyles = makeStyles( + theme => ({ + arrow: { + [theme.breakpoints.down("sm")]: { + marginLeft: 0 + }, + marginLeft: theme.spacing(2), + transition: theme.transitions.duration.standard + "ms" + }, + avatar: { + "&&": { + [theme.breakpoints.down("sm")]: { + height: 40, + width: 40 + }, + height: 32, + width: 32 + } + }, + avatarInitials: { + color: theme.palette.primary.contrastText + }, + avatarPlaceholder: { + alignItems: "center", + background: theme.palette.primary.main, + borderRadius: "100%", + display: "flex", + justifyContent: "center" + }, + popover: { + marginTop: theme.spacing(2), + zIndex: 1 + }, + rotate: { + transform: "rotate(180deg)" + }, + userChip: { + [theme.breakpoints.down("sm")]: { + height: 48 + }, + backgroundColor: theme.palette.background.paper, + borderRadius: 24, + color: theme.palette.text.primary, + height: 40, + padding: theme.spacing(0.5) + }, + userMenuContainer: { + position: "relative" + }, + userMenuItem: { + textAlign: "right" + } + }), + { + name: "UserChip" + } +); + +export interface UserChipProps { + user: User; + onLogout: () => void; + onProfileClick: () => void; +} + +const UserChip: React.FC = ({ + user, + onLogout, + onProfileClick +}) => { + const classes = useStyles({}); + const [isMenuOpened, setMenuState] = React.useState(false); + const anchor = React.useRef(); + + const handleLogout = () => { + setMenuState(false); + onLogout(); + }; + + const handleViewerProfile = () => { + setMenuState(false); + onProfileClick(); + }; + + return ( +
+ + ) : ( +
+
+ {getUserInitials(user)} +
+
+ ) + } + classes={{ + avatar: classes.avatar + }} + className={classes.userChip} + label={ + <> + {getUserName(user, true)} + + + } + onClick={() => setMenuState(!isMenuOpened)} + data-test="userMenu" + /> + + {({ TransitionProps, placement }) => ( + + + setMenuState(false)} + mouseEvent="onClick" + > + + + + + + + + + + + + )} + +
+ ); +}; +UserChip.displayName = "UserChip"; +export default UserChip; diff --git a/src/components/UserChip/index.ts b/src/components/UserChip/index.ts new file mode 100644 index 000000000..125a4544b --- /dev/null +++ b/src/components/UserChip/index.ts @@ -0,0 +1,2 @@ +export * from "./UserChip"; +export { default } from "./UserChip"; diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index 26e7a1955..566882817 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -4843,6 +4843,30 @@ exports[`Storyshots Generics / Metadata loading 1`] = `
`; +exports[`Storyshots Generics / Mobile Side Menu default 1`] = ` +
+ +
+`; + exports[`Storyshots Generics / Money formatting default 1`] = `
`; +exports[`Storyshots Generics / Square Button default 1`] = ` +
+ +
+`; + exports[`Storyshots Generics / StatusLabel when error 1`] = `