From 9c965a8d725977d2b48bf755ffe57274a4e6731b Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Fri, 30 Apr 2021 18:45:28 +0200 Subject: [PATCH 1/3] Fix logout button is displayed in two menus Closes #6228 --- docs/Theming.md | 156 ++++++++++++------ examples/demo/src/layout/Menu.tsx | 39 +---- examples/demo/src/layout/SubMenu.tsx | 24 +-- .../src/layout/DashboardMenuItem.tsx | 7 +- packages/ra-ui-materialui/src/layout/Menu.tsx | 20 +-- .../src/layout/MenuItemLink.tsx | 36 ++-- .../ra-ui-materialui/src/layout/Sidebar.tsx | 15 +- 7 files changed, 165 insertions(+), 132 deletions(-) diff --git a/docs/Theming.md b/docs/Theming.md index 70f3eeb61bc..f277df3f3ac 100644 --- a/docs/Theming.md +++ b/docs/Theming.md @@ -807,9 +807,56 @@ Check [the `ra-preferences` documentation](https://marmelab.com/ra-enterprise/mo ## Using a Custom Menu -By default, React-admin uses the list of `` components passed as children of `` to build a menu to each resource with a `list` component. +By default, React-admin uses the list of `` components passed as children of `` to build a menu to each resource with a `list` component. If you want to reorder, add or remove menu items, for instance to link to non-resources pages, you have to provide a custom `` component to your `Layout`. -If you want to add or remove menu items, for instance to link to non-resources pages, you can create your own menu component: +### Custom Menu Example + +You can create a custom menu component using the `` and `` components: + +```jsx +// in src/Menu.js +import * as React from 'react'; +import { DashboardMenuItem, MenuItemLink } from 'react-admin'; +import BookIcon from '@material-ui/icons/Book'; +import ChatBubbleIcon from '@material-ui/icons/ChatBubble'; +import PeopleIcon from '@material-ui/icons/People'; +import LabelIcon from '@material-ui/icons/Label'; + +export const Menu = () => ( +
+ + }/> + }/> + }/> + }/> +
+); +``` + +To use this custom menu component, pass it to a custom Layout, as explained above: + +```jsx +// in src/Layout.js +import { Layout } from 'react-admin'; +import { Menu } from './Menu'; + +export const Layout = (props) => ; +``` + +Then, use this layout in the `` `layout` prop: + +```jsx +// in src/App.js +import { Layout } from './Layout'; + +const App = () => ( + + // ... + +); +``` + +**Tip**: You can generate the menu items for each of the resourc emages by reading the Resource configurations from the Redux store: ```jsx // in src/Menu.js @@ -821,13 +868,11 @@ import { DashboardMenuItem, MenuItemLink, getResources } from 'react-admin'; import DefaultIcon from '@material-ui/icons/ViewList'; import LabelIcon from '@material-ui/icons/Label'; -const Menu = ({ onMenuClick, logout }) => { - const isXSmall = useMediaQuery(theme => theme.breakpoints.down('xs')); - const open = useSelector(state => state.admin.ui.sidebarOpen); +export const Menu = () => { const resources = useSelector(getResources); return (
- + {resources.map(resource => ( { sidebarIsOpen={open} /> ))} - } - onClick={onMenuClick} - sidebarIsOpen={open} - /> - {isXSmall && logout} + {/* add your custom menus here */}
); }; - -export default Menu; ``` -**Tip**: Note the `MenuItemLink` component. It must be used to avoid unwanted side effects in mobile views. +**Tip**: If you need a multi-level menu, or a Mega Menu opening panels with custom content, check out [the `ra-navigation` module](https://marmelab.com/ra-enterprise/modules/ra-navigation) (part of the [Enterprise Edition](https://marmelab.com/ra-enterprise)) -**Tip**: Note that we include the `logout` item only on small devices. Indeed, the `logout` button is already displayed in the AppBar on larger devices. +![multi-level menu](https://marmelab.com/ra-enterprise/modules/assets/ra-multilevelmenu-item.gif) -**Tip**: The `primaryText` prop accepts a React node. You can pass a custom element in it. For example: +![MegaMenu and Breadcrumb](https://marmelab.com/ra-enterprise/modules/assets/ra-multilevelmenu-categories.gif) -```jsx - import Badge from '@material-ui/core/Badge'; +### `` - - Notifications - - } onClick={onMenuClick} /> -``` +The `` component displays a menu item with a label and an icon - or only the icon with a tooltip when the sidebar is minimized. It also handles the automatic closing of the menu on tap on mobile. -To use this custom menu component, pass it to a custom Layout, as explained above: +The `primaryText` prop accepts a string or a React node. You can use it e.g. to display a badge on top of the menu item: ```jsx -// in src/MyLayout.js -import { Layout } from 'react-admin'; -import MyMenu from './MyMenu'; +import Badge from '@material-ui/core/Badge'; -const MyLayout = (props) => ; - -export default MyLayout; + + Notifications + +} /> ``` -Then, use this layout in the `` `layout` prop: +The `icon` prop allows to set the menu left icon. -```jsx -// in src/App.js -import MyLayout from './MyLayout'; +Additional props are passed down to [the underling material-ui `` component](https://material-ui.com/api/menu-item/#menuitem-api). -const App = () => ( - +**Tip**: The `` component makes use of the React Router [NavLink](https://reacttraining.com/react-router/web/api/NavLink) component, hence allowing to customize the active menu style. For instance, here is how to use a custom theme to show a left border for the active menu: + +```jsx +export const theme = { + palette: { // ... - -); + }, + overrides: { + RaMenuItemLink: { + active: { + borderLeft: '3px solid #4f3cc9', + }, + root: { + borderLeft: '3px solid #fff', // invisible menu when not active, to avoid scrolling the text when selecting the menu + }, + }, + }, +}; ``` -**Tip**: If you use authentication, don't forget to render the `logout` prop in your custom menu component. Also, the `onMenuClick` function passed as prop is used to close the sidebar on mobile. +### Menu To A Filtered List -The `MenuItemLink` component make use of the React Router [NavLink](https://reacttraining.com/react-router/web/api/NavLink) component, hence allowing to customize its style when it targets the current page. +As the filter values are taken from the URL, you can link to a pre-filtered list by setting the `filter` query parameter. -**Tip**: If you need a multi-level menu, or a Mega Menu opening panels with custom content, check out [the `ra-navigation` module](https://marmelab.com/ra-enterprise/modules/ra-navigation) (part of the [Enterprise Edition](https://marmelab.com/ra-enterprise)) +For instance, to include a menu to a list of published posts: -![multi-level menu](https://marmelab.com/ra-enterprise/modules/assets/ra-multilevelmenu-item.gif) +```jsx +} +/> +``` -![MegaMenu and Breadcrumb](https://marmelab.com/ra-enterprise/modules/assets/ra-multilevelmenu-categories.gif) +### Menu To A List Without Filters + +By default, a click on `` for a list page opens the list with the same filters as they were applied the last time the user saw them. This is usually the expected behavior, but your users may prefer that clicking on a menu item resets the list filters. + +Just use an empty `filter` query parameter to force empty filters: + +```jsx +} +/> +``` ## Using a Custom Login Page diff --git a/examples/demo/src/layout/Menu.tsx b/examples/demo/src/layout/Menu.tsx index 252240881c1..ac3ee849cb2 100644 --- a/examples/demo/src/layout/Menu.tsx +++ b/examples/demo/src/layout/Menu.tsx @@ -1,9 +1,8 @@ import * as React from 'react'; import { FC, useState } from 'react'; import { useSelector } from 'react-redux'; -import SettingsIcon from '@material-ui/icons/Settings'; import LabelIcon from '@material-ui/icons/Label'; -import { useMediaQuery, Theme, Box } from '@material-ui/core'; +import { Box } from '@material-ui/core'; import { useTranslate, DashboardMenuItem, @@ -22,17 +21,13 @@ import { AppState } from '../types'; type MenuName = 'menuCatalog' | 'menuSales' | 'menuCustomers'; -const Menu: FC = ({ onMenuClick, logout, dense = false }) => { +const Menu: FC = ({ dense = false }) => { const [state, setState] = useState({ menuCatalog: true, menuSales: true, menuCustomers: true, }); const translate = useTranslate(); - const isXSmall = useMediaQuery((theme: Theme) => - theme.breakpoints.down('xs') - ); - const open = useSelector((state: AppState) => state.admin.ui.sidebarOpen); useSelector((state: AppState) => state.theme); // force rerender on theme change const handleToggle = (menu: MenuName) => { @@ -42,11 +37,10 @@ const Menu: FC = ({ onMenuClick, logout, dense = false }) => { return ( {' '} - + handleToggle('menuSales')} isOpen={state.menuSales} - sidebarIsOpen={open} name="pos.menu.sales" icon={} dense={dense} @@ -57,8 +51,6 @@ const Menu: FC = ({ onMenuClick, logout, dense = false }) => { smart_count: 2, })} leftIcon={} - onClick={onMenuClick} - sidebarIsOpen={open} dense={dense} /> = ({ onMenuClick, logout, dense = false }) => { smart_count: 2, })} leftIcon={} - onClick={onMenuClick} - sidebarIsOpen={open} dense={dense} /> handleToggle('menuCatalog')} isOpen={state.menuCatalog} - sidebarIsOpen={open} name="pos.menu.catalog" icon={} dense={dense} @@ -86,8 +75,6 @@ const Menu: FC = ({ onMenuClick, logout, dense = false }) => { smart_count: 2, })} leftIcon={} - onClick={onMenuClick} - sidebarIsOpen={open} dense={dense} /> = ({ onMenuClick, logout, dense = false }) => { smart_count: 2, })} leftIcon={} - onClick={onMenuClick} - sidebarIsOpen={open} dense={dense} /> handleToggle('menuCustomers')} isOpen={state.menuCustomers} - sidebarIsOpen={open} name="pos.menu.customers" icon={} dense={dense} @@ -115,8 +99,6 @@ const Menu: FC = ({ onMenuClick, logout, dense = false }) => { smart_count: 2, })} leftIcon={} - onClick={onMenuClick} - sidebarIsOpen={open} dense={dense} /> = ({ onMenuClick, logout, dense = false }) => { smart_count: 2, })} leftIcon={} - onClick={onMenuClick} - sidebarIsOpen={open} dense={dense} /> @@ -136,21 +116,8 @@ const Menu: FC = ({ onMenuClick, logout, dense = false }) => { smart_count: 2, })} leftIcon={} - onClick={onMenuClick} - sidebarIsOpen={open} dense={dense} /> - {isXSmall && ( - } - onClick={onMenuClick} - sidebarIsOpen={open} - dense={dense} - /> - )} - {isXSmall && logout} ); }; diff --git a/examples/demo/src/layout/SubMenu.tsx b/examples/demo/src/layout/SubMenu.tsx index bb8fe965fac..99b5c05c4b8 100644 --- a/examples/demo/src/layout/SubMenu.tsx +++ b/examples/demo/src/layout/SubMenu.tsx @@ -1,14 +1,17 @@ import * as React from 'react'; import { FC, Fragment, ReactElement } from 'react'; -import ExpandMore from '@material-ui/icons/ExpandMore'; -import List from '@material-ui/core/List'; -import MenuItem from '@material-ui/core/MenuItem'; -import ListItemIcon from '@material-ui/core/ListItemIcon'; -import Typography from '@material-ui/core/Typography'; -import Collapse from '@material-ui/core/Collapse'; -import Tooltip from '@material-ui/core/Tooltip'; +import { useSelector } from 'react-redux'; +import { + List, + MenuItem, + ListItemIcon, + Typography, + Collapse, + Tooltip, +} from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; -import { useTranslate } from 'react-admin'; +import ExpandMore from '@material-ui/icons/ExpandMore'; +import { useTranslate, ReduxState } from 'react-admin'; const useStyles = makeStyles(theme => ({ icon: { minWidth: theme.spacing(5) }, @@ -32,12 +35,10 @@ interface Props { icon: ReactElement; isOpen: boolean; name: string; - sidebarIsOpen: boolean; } const SubMenu: FC = ({ handleToggle, - sidebarIsOpen, isOpen, name, icon, @@ -46,6 +47,9 @@ const SubMenu: FC = ({ }) => { const translate = useTranslate(); const classes = useStyles(); + const sidebarIsOpen = useSelector( + state => state.admin.ui.sidebarOpen + ); const header = ( diff --git a/packages/ra-ui-materialui/src/layout/DashboardMenuItem.tsx b/packages/ra-ui-materialui/src/layout/DashboardMenuItem.tsx index 189d462af7e..f295db3e09c 100644 --- a/packages/ra-ui-materialui/src/layout/DashboardMenuItem.tsx +++ b/packages/ra-ui-materialui/src/layout/DashboardMenuItem.tsx @@ -8,13 +8,11 @@ import MenuItemLink from './MenuItemLink'; const DashboardMenuItem: FC = ({ locale, - onClick, ...props }) => { const translate = useTranslate(); return ( } @@ -29,7 +27,10 @@ export interface DashboardMenuItemProps { locale?: string; onClick?: () => void; dense?: boolean; - sidebarIsOpen: boolean; + /** + * @deprcated + */ + sidebarIsOpen?: boolean; } DashboardMenuItem.propTypes = { diff --git a/packages/ra-ui-materialui/src/layout/Menu.tsx b/packages/ra-ui-materialui/src/layout/Menu.tsx index 927f49ce2fc..0a8290d2dd5 100644 --- a/packages/ra-ui-materialui/src/layout/Menu.tsx +++ b/packages/ra-ui-materialui/src/layout/Menu.tsx @@ -51,9 +51,6 @@ const Menu: FC = props => { ...rest } = props; const classes = useStyles(props); - const isXSmall = useMediaQuery((theme: Theme) => - theme.breakpoints.down('xs') - ); const open = useSelector((state: ReduxState) => state.admin.ui.sidebarOpen); const resources = useSelector(getResources, shallowEqual) as Array; const getResourceLabel = useGetResourceLabel(); @@ -69,13 +66,7 @@ const Menu: FC = props => { )} {...rest} > - {hasDashboard && ( - - )} + {hasDashboard && } {resources .filter(r => r.hasList) .map(resource => ( @@ -89,12 +80,9 @@ const Menu: FC = props => { leftIcon={ resource.icon ? : } - onClick={onMenuClick} dense={dense} - sidebarIsOpen={open} /> ))} - {isXSmall && logout} ); }; @@ -104,7 +92,13 @@ export interface MenuProps { className?: string; dense?: boolean; hasDashboard?: boolean; + /** + * @deprecated + */ logout?: ReactNode; + /** + * @deprecated + */ onMenuClick?: () => void; } diff --git a/packages/ra-ui-materialui/src/layout/MenuItemLink.tsx b/packages/ra-ui-materialui/src/layout/MenuItemLink.tsx index c32dae6bbdc..d4452f0eae3 100644 --- a/packages/ra-ui-materialui/src/layout/MenuItemLink.tsx +++ b/packages/ra-ui-materialui/src/layout/MenuItemLink.tsx @@ -8,11 +8,19 @@ import React, { } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; +import { useDispatch, useSelector } from 'react-redux'; import { StaticContext } from 'react-router'; import { NavLink, NavLinkProps } from 'react-router-dom'; -import MenuItem, { MenuItemProps } from '@material-ui/core/MenuItem'; -import ListItemIcon from '@material-ui/core/ListItemIcon'; -import Tooltip, { TooltipProps } from '@material-ui/core/Tooltip'; +import { ReduxState, setSidebarVisibility } from 'ra-core'; +import { + MenuItem, + MenuItemProps, + ListItemIcon, + Tooltip, + TooltipProps, + useMediaQuery, + Theme, +} from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; const NavLinkRef = forwardRef((props, ref) => ( @@ -44,12 +52,17 @@ const MenuItemLink: FC = forwardRef((props, ref) => { ...rest } = props; const classes = useStyles(props); - + const dispatch = useDispatch(); + const isSmall = useMediaQuery(theme => theme.breakpoints.down('sm')); + const open = useSelector((state: ReduxState) => state.admin.ui.sidebarOpen); const handleMenuTap = useCallback( e => { + if (isSmall) { + dispatch(setSidebarVisibility(false)); + } onClick && onClick(e); }, - [onClick] + [dispatch, isSmall, onClick] ); const renderMenuItem = () => { @@ -75,11 +88,9 @@ const MenuItemLink: FC = forwardRef((props, ref) => { ); }; - if (sidebarIsOpen) { - return renderMenuItem(); - } - - return ( + return open ? ( + renderMenuItem() + ) : ( {renderMenuItem()} @@ -90,7 +101,10 @@ interface Props { leftIcon?: ReactElement; primaryText?: ReactNode; staticContext?: StaticContext; - sidebarIsOpen: boolean; + /** + * @deprecated + */ + sidebarIsOpen?: boolean; tooltipProps?: TooltipProps; } diff --git a/packages/ra-ui-materialui/src/layout/Sidebar.tsx b/packages/ra-ui-materialui/src/layout/Sidebar.tsx index 1b17a6e4cf4..37cd3797414 100644 --- a/packages/ra-ui-materialui/src/layout/Sidebar.tsx +++ b/packages/ra-ui-materialui/src/layout/Sidebar.tsx @@ -28,7 +28,6 @@ const Sidebar = (props: SidebarProps) => { state => state.admin.ui.sidebarOpen ); useLocale(); // force redraw on locale change - const handleClose = () => dispatch(setSidebarVisibility(false)); const toggleSidebar = () => dispatch(setSidebarVisibility(!open)); const { drawerPaper, ...classes } = useStyles({ ...props, open }); @@ -43,9 +42,7 @@ const Sidebar = (props: SidebarProps) => { classes={classes} {...rest} > - {cloneElement(Children.only(children), { - onMenuClick: handleClose, - })} + {children} ) : isSmall ? ( { classes={classes} {...rest} > - {cloneElement(Children.only(children), { - onMenuClick: handleClose, - })} + {children} ) : ( { classes={classes} {...rest} > - {cloneElement(Children.only(children), { - onMenuClick: defaultOnMenuClick, - })} + {children} ); }; @@ -84,8 +77,6 @@ Sidebar.propTypes = { children: PropTypes.node.isRequired, }; -const defaultOnMenuClick = () => null; - const useStyles = makeStyles( theme => ({ root: {}, From d4256a5b8d8b27497c5a7f28b14e45bad94d76ce Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Thu, 6 May 2021 16:07:27 +0200 Subject: [PATCH 2/3] Fix warnings --- docs/Theming.md | 2 +- packages/ra-ui-materialui/src/layout/Menu.tsx | 1 - packages/ra-ui-materialui/src/layout/Sidebar.tsx | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/Theming.md b/docs/Theming.md index f277df3f3ac..601e742e461 100644 --- a/docs/Theming.md +++ b/docs/Theming.md @@ -916,7 +916,7 @@ import Badge from '@material-ui/core/Badge'; } /> ``` -The `icon` prop allows to set the menu left icon. +The `letfIcon` prop allows to set the menu left icon. Additional props are passed down to [the underling material-ui `` component](https://material-ui.com/api/menu-item/#menuitem-api). diff --git a/packages/ra-ui-materialui/src/layout/Menu.tsx b/packages/ra-ui-materialui/src/layout/Menu.tsx index 0a8290d2dd5..e803e734a49 100644 --- a/packages/ra-ui-materialui/src/layout/Menu.tsx +++ b/packages/ra-ui-materialui/src/layout/Menu.tsx @@ -4,7 +4,6 @@ import PropTypes from 'prop-types'; import { shallowEqual, useSelector } from 'react-redux'; import lodashGet from 'lodash/get'; // @ts-ignore -import { useMediaQuery, Theme } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; import DefaultIcon from '@material-ui/icons/ViewList'; import classnames from 'classnames'; diff --git a/packages/ra-ui-materialui/src/layout/Sidebar.tsx b/packages/ra-ui-materialui/src/layout/Sidebar.tsx index 37cd3797414..62f2a9dcc7e 100644 --- a/packages/ra-ui-materialui/src/layout/Sidebar.tsx +++ b/packages/ra-ui-materialui/src/layout/Sidebar.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Children, cloneElement, ReactElement } from 'react'; +import { ReactElement } from 'react'; import PropTypes from 'prop-types'; import { useDispatch, useSelector } from 'react-redux'; import { Drawer, DrawerProps, useMediaQuery, Theme } from '@material-ui/core'; From fe5cf34bacc397988f1c32eb33ae9efed732b5ce Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Fri, 7 May 2021 06:17:22 +0200 Subject: [PATCH 3/3] Review --- docs/Theming.md | 2 +- examples/demo/src/layout/Menu.tsx | 17 ++++++++++++----- packages/ra-ui-materialui/src/layout/Menu.tsx | 4 ++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/docs/Theming.md b/docs/Theming.md index 601e742e461..9d80e34b423 100644 --- a/docs/Theming.md +++ b/docs/Theming.md @@ -856,7 +856,7 @@ const App = () => ( ); ``` -**Tip**: You can generate the menu items for each of the resourc emages by reading the Resource configurations from the Redux store: +**Tip**: You can generate the menu items for each of the resources by reading the Resource configurations from the Redux store: ```jsx // in src/Menu.js diff --git a/examples/demo/src/layout/Menu.tsx b/examples/demo/src/layout/Menu.tsx index ac3ee849cb2..571b067c386 100644 --- a/examples/demo/src/layout/Menu.tsx +++ b/examples/demo/src/layout/Menu.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; -import { FC, useState } from 'react'; +import { useState } from 'react'; import { useSelector } from 'react-redux'; import LabelIcon from '@material-ui/icons/Label'; -import { Box } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; import { useTranslate, DashboardMenuItem, @@ -21,7 +21,7 @@ import { AppState } from '../types'; type MenuName = 'menuCatalog' | 'menuSales' | 'menuCustomers'; -const Menu: FC = ({ dense = false }) => { +const Menu = ({ dense = false }: MenuProps) => { const [state, setState] = useState({ menuCatalog: true, menuSales: true, @@ -29,13 +29,14 @@ const Menu: FC = ({ dense = false }) => { }); const translate = useTranslate(); useSelector((state: AppState) => state.theme); // force rerender on theme change + const classes = useStyles(); const handleToggle = (menu: MenuName) => { setState(state => ({ ...state, [menu]: !state[menu] })); }; return ( - +
{' '} = ({ dense = false }) => { leftIcon={} dense={dense} /> - +
); }; +const useStyles = makeStyles(theme => ({ + root: { + marginTop: theme.spacing(1), + }, +})); + export default Menu; diff --git a/packages/ra-ui-materialui/src/layout/Menu.tsx b/packages/ra-ui-materialui/src/layout/Menu.tsx index e803e734a49..b74692ac7fc 100644 --- a/packages/ra-ui-materialui/src/layout/Menu.tsx +++ b/packages/ra-ui-materialui/src/layout/Menu.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { FC, ReactNode } from 'react'; +import { ReactNode } from 'react'; import PropTypes from 'prop-types'; import { shallowEqual, useSelector } from 'react-redux'; import lodashGet from 'lodash/get'; @@ -39,7 +39,7 @@ const useStyles = makeStyles( { name: 'RaMenu' } ); -const Menu: FC = props => { +const Menu = (props: MenuProps) => { const { classes: classesOverride, className,