Skip to content

Commit f5645ef

Browse files
authored
Merge pull request #6230 from marmelab/simplify-menu-customization
Fix logout button appears in two different menus
2 parents 14c6bbc + fe5cf34 commit f5645ef

File tree

7 files changed

+178
-139
lines changed

7 files changed

+178
-139
lines changed

docs/Theming.md

+109-47
Original file line numberDiff line numberDiff line change
@@ -807,9 +807,56 @@ Check [the `ra-preferences` documentation](https://marmelab.com/ra-enterprise/mo
807807

808808
## Using a Custom Menu
809809

810-
By default, React-admin uses the list of `<Resource>` components passed as children of `<Admin>` to build a menu to each resource with a `list` component.
810+
By default, React-admin uses the list of `<Resource>` components passed as children of `<Admin>` 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 `<Menu>` component to your `Layout`.
811811

812-
If you want to add or remove menu items, for instance to link to non-resources pages, you can create your own menu component:
812+
### Custom Menu Example
813+
814+
You can create a custom menu component using the `<DashboardMenuItem>` and `<MenuItemLink>` components:
815+
816+
```jsx
817+
// in src/Menu.js
818+
import * as React from 'react';
819+
import { DashboardMenuItem, MenuItemLink } from 'react-admin';
820+
import BookIcon from '@material-ui/icons/Book';
821+
import ChatBubbleIcon from '@material-ui/icons/ChatBubble';
822+
import PeopleIcon from '@material-ui/icons/People';
823+
import LabelIcon from '@material-ui/icons/Label';
824+
825+
export const Menu = () => (
826+
<div>
827+
<DashboardMenuItem />
828+
<MenuItemLink to="/posts" primaryText="Posts" leftIcon={<BookIcon />}/>
829+
<MenuItemLink to="/comments" primaryText="Comments" leftIcon={<ChatBubbleIcon />}/>
830+
<MenuItemLink to="/users" primaryText="Users" leftIcon={<PeopleIcon />}/>
831+
<MenuItemLink to="/custom-route" primaryText="Miscellaneous" leftIcon={<LabelIcon />}/>
832+
</div>
833+
);
834+
```
835+
836+
To use this custom menu component, pass it to a custom Layout, as explained above:
837+
838+
```jsx
839+
// in src/Layout.js
840+
import { Layout } from 'react-admin';
841+
import { Menu } from './Menu';
842+
843+
export const Layout = (props) => <Layout {...props} menu={Menu} />;
844+
```
845+
846+
Then, use this layout in the `<Admin>` `layout` prop:
847+
848+
```jsx
849+
// in src/App.js
850+
import { Layout } from './Layout';
851+
852+
const App = () => (
853+
<Admin layout={Layout} dataProvider={simpleRestProvider('http://path.to.my.api')}>
854+
// ...
855+
</Admin>
856+
);
857+
```
858+
859+
**Tip**: You can generate the menu items for each of the resources by reading the Resource configurations from the Redux store:
813860

814861
```jsx
815862
// in src/Menu.js
@@ -821,13 +868,11 @@ import { DashboardMenuItem, MenuItemLink, getResources } from 'react-admin';
821868
import DefaultIcon from '@material-ui/icons/ViewList';
822869
import LabelIcon from '@material-ui/icons/Label';
823870

824-
const Menu = ({ onMenuClick, logout }) => {
825-
const isXSmall = useMediaQuery(theme => theme.breakpoints.down('xs'));
826-
const open = useSelector(state => state.admin.ui.sidebarOpen);
871+
export const Menu = () => {
827872
const resources = useSelector(getResources);
828873
return (
829874
<div>
830-
<DashboardMenuItem onClick={onMenuClick} sidebarIsOpen={open} />
875+
<DashboardMenuItem />
831876
{resources.map(resource => (
832877
<MenuItemLink
833878
key={resource.name}
@@ -843,71 +888,88 @@ const Menu = ({ onMenuClick, logout }) => {
843888
sidebarIsOpen={open}
844889
/>
845890
))}
846-
<MenuItemLink
847-
to="/custom-route"
848-
primaryText="Miscellaneous"
849-
leftIcon={<LabelIcon />}
850-
onClick={onMenuClick}
851-
sidebarIsOpen={open}
852-
/>
853-
{isXSmall && logout}
891+
{/* add your custom menus here */}
854892
</div>
855893
);
856894
};
857-
858-
export default Menu;
859895
```
860896

861-
**Tip**: Note the `MenuItemLink` component. It must be used to avoid unwanted side effects in mobile views.
897+
**Tip**: If you need a multi-level menu, or a Mega Menu opening panels with custom content, check out [the `ra-navigation`<img class="icon" src="./img/premium.svg" /> module](https://marmelab.com/ra-enterprise/modules/ra-navigation) (part of the [Enterprise Edition](https://marmelab.com/ra-enterprise))
862898

863-
**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.
899+
![multi-level menu](https://marmelab.com/ra-enterprise/modules/assets/ra-multilevelmenu-item.gif)
864900

865-
**Tip**: The `primaryText` prop accepts a React node. You can pass a custom element in it. For example:
901+
![MegaMenu and Breadcrumb](https://marmelab.com/ra-enterprise/modules/assets/ra-multilevelmenu-categories.gif)
866902

867-
```jsx
868-
import Badge from '@material-ui/core/Badge';
903+
### `<MenuItemLink>`
869904

870-
<MenuItemLink to="/custom-route" primaryText={
871-
<Badge badgeContent={4} color="primary">
872-
Notifications
873-
</Badge>
874-
} onClick={onMenuClick} />
875-
```
905+
The `<MenuItemLink>` 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.
876906

877-
To use this custom menu component, pass it to a custom Layout, as explained above:
907+
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:
878908

879909
```jsx
880-
// in src/MyLayout.js
881-
import { Layout } from 'react-admin';
882-
import MyMenu from './MyMenu';
910+
import Badge from '@material-ui/core/Badge';
883911

884-
const MyLayout = (props) => <Layout {...props} menu={MyMenu} />;
885-
886-
export default MyLayout;
912+
<MenuItemLink to="/custom-route" primaryText={
913+
<Badge badgeContent={4} color="primary">
914+
Notifications
915+
</Badge>
916+
} />
887917
```
888918

889-
Then, use this layout in the `<Admin>` `layout` prop:
919+
The `letfIcon` prop allows to set the menu left icon.
890920

891-
```jsx
892-
// in src/App.js
893-
import MyLayout from './MyLayout';
921+
Additional props are passed down to [the underling material-ui `<MenuItem>` component](https://material-ui.com/api/menu-item/#menuitem-api).
894922

895-
const App = () => (
896-
<Admin layout={MyLayout} dataProvider={simpleRestProvider('http://path.to.my.api')}>
923+
**Tip**: The `<MenuItemLink>` 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:
924+
925+
```jsx
926+
export const theme = {
927+
palette: {
897928
// ...
898-
</Admin>
899-
);
929+
},
930+
overrides: {
931+
RaMenuItemLink: {
932+
active: {
933+
borderLeft: '3px solid #4f3cc9',
934+
},
935+
root: {
936+
borderLeft: '3px solid #fff', // invisible menu when not active, to avoid scrolling the text when selecting the menu
937+
},
938+
},
939+
},
940+
};
900941
```
901942

902-
**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.
943+
### Menu To A Filtered List
903944

904-
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.
945+
As the filter values are taken from the URL, you can link to a pre-filtered list by setting the `filter` query parameter.
905946

906-
**Tip**: If you need a multi-level menu, or a Mega Menu opening panels with custom content, check out [the `ra-navigation`<img class="icon" src="./img/premium.svg" /> module](https://marmelab.com/ra-enterprise/modules/ra-navigation) (part of the [Enterprise Edition](https://marmelab.com/ra-enterprise))
947+
For instance, to include a menu to a list of published posts:
907948

908-
![multi-level menu](https://marmelab.com/ra-enterprise/modules/assets/ra-multilevelmenu-item.gif)
949+
```jsx
950+
<MenuItemLink
951+
to={{
952+
pathname: '/posts',
953+
search: `filter=${JSON.stringify({ is_published: true })}`,
954+
}}
955+
primaryText="Posts"
956+
leftIcon={<BookIcon />}
957+
/>
958+
```
909959

910-
![MegaMenu and Breadcrumb](https://marmelab.com/ra-enterprise/modules/assets/ra-multilevelmenu-categories.gif)
960+
### Menu To A List Without Filters
961+
962+
By default, a click on `<MenuItemLink >` 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.
963+
964+
Just use an empty `filter` query parameter to force empty filters:
965+
966+
```jsx
967+
<MenuItemLink
968+
to="/posts?filter=%7B%7D" // %7B%7D is JSON.stringify({})
969+
primaryText="Posts"
970+
leftIcon={<BookIcon />}
971+
/>
972+
```
911973

912974
## Using a Custom Login Page
913975

examples/demo/src/layout/Menu.tsx

+13-39
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import * as React from 'react';
2-
import { FC, useState } from 'react';
2+
import { useState } from 'react';
33
import { useSelector } from 'react-redux';
4-
import SettingsIcon from '@material-ui/icons/Settings';
54
import LabelIcon from '@material-ui/icons/Label';
6-
import { useMediaQuery, Theme, Box } from '@material-ui/core';
5+
import { makeStyles } from '@material-ui/core/styles';
76
import {
87
useTranslate,
98
DashboardMenuItem,
@@ -22,31 +21,27 @@ import { AppState } from '../types';
2221

2322
type MenuName = 'menuCatalog' | 'menuSales' | 'menuCustomers';
2423

25-
const Menu: FC<MenuProps> = ({ onMenuClick, logout, dense = false }) => {
24+
const Menu = ({ dense = false }: MenuProps) => {
2625
const [state, setState] = useState({
2726
menuCatalog: true,
2827
menuSales: true,
2928
menuCustomers: true,
3029
});
3130
const translate = useTranslate();
32-
const isXSmall = useMediaQuery((theme: Theme) =>
33-
theme.breakpoints.down('xs')
34-
);
35-
const open = useSelector((state: AppState) => state.admin.ui.sidebarOpen);
3631
useSelector((state: AppState) => state.theme); // force rerender on theme change
32+
const classes = useStyles();
3733

3834
const handleToggle = (menu: MenuName) => {
3935
setState(state => ({ ...state, [menu]: !state[menu] }));
4036
};
4137

4238
return (
43-
<Box mt={1}>
39+
<div className={classes.root}>
4440
{' '}
45-
<DashboardMenuItem onClick={onMenuClick} sidebarIsOpen={open} />
41+
<DashboardMenuItem />
4642
<SubMenu
4743
handleToggle={() => handleToggle('menuSales')}
4844
isOpen={state.menuSales}
49-
sidebarIsOpen={open}
5045
name="pos.menu.sales"
5146
icon={<orders.icon />}
5247
dense={dense}
@@ -57,8 +52,6 @@ const Menu: FC<MenuProps> = ({ onMenuClick, logout, dense = false }) => {
5752
smart_count: 2,
5853
})}
5954
leftIcon={<orders.icon />}
60-
onClick={onMenuClick}
61-
sidebarIsOpen={open}
6255
dense={dense}
6356
/>
6457
<MenuItemLink
@@ -67,15 +60,12 @@ const Menu: FC<MenuProps> = ({ onMenuClick, logout, dense = false }) => {
6760
smart_count: 2,
6861
})}
6962
leftIcon={<invoices.icon />}
70-
onClick={onMenuClick}
71-
sidebarIsOpen={open}
7263
dense={dense}
7364
/>
7465
</SubMenu>
7566
<SubMenu
7667
handleToggle={() => handleToggle('menuCatalog')}
7768
isOpen={state.menuCatalog}
78-
sidebarIsOpen={open}
7969
name="pos.menu.catalog"
8070
icon={<products.icon />}
8171
dense={dense}
@@ -86,8 +76,6 @@ const Menu: FC<MenuProps> = ({ onMenuClick, logout, dense = false }) => {
8676
smart_count: 2,
8777
})}
8878
leftIcon={<products.icon />}
89-
onClick={onMenuClick}
90-
sidebarIsOpen={open}
9179
dense={dense}
9280
/>
9381
<MenuItemLink
@@ -96,15 +84,12 @@ const Menu: FC<MenuProps> = ({ onMenuClick, logout, dense = false }) => {
9684
smart_count: 2,
9785
})}
9886
leftIcon={<categories.icon />}
99-
onClick={onMenuClick}
100-
sidebarIsOpen={open}
10187
dense={dense}
10288
/>
10389
</SubMenu>
10490
<SubMenu
10591
handleToggle={() => handleToggle('menuCustomers')}
10692
isOpen={state.menuCustomers}
107-
sidebarIsOpen={open}
10893
name="pos.menu.customers"
10994
icon={<visitors.icon />}
11095
dense={dense}
@@ -115,8 +100,6 @@ const Menu: FC<MenuProps> = ({ onMenuClick, logout, dense = false }) => {
115100
smart_count: 2,
116101
})}
117102
leftIcon={<visitors.icon />}
118-
onClick={onMenuClick}
119-
sidebarIsOpen={open}
120103
dense={dense}
121104
/>
122105
<MenuItemLink
@@ -125,8 +108,6 @@ const Menu: FC<MenuProps> = ({ onMenuClick, logout, dense = false }) => {
125108
smart_count: 2,
126109
})}
127110
leftIcon={<LabelIcon />}
128-
onClick={onMenuClick}
129-
sidebarIsOpen={open}
130111
dense={dense}
131112
/>
132113
</SubMenu>
@@ -136,23 +117,16 @@ const Menu: FC<MenuProps> = ({ onMenuClick, logout, dense = false }) => {
136117
smart_count: 2,
137118
})}
138119
leftIcon={<reviews.icon />}
139-
onClick={onMenuClick}
140-
sidebarIsOpen={open}
141120
dense={dense}
142121
/>
143-
{isXSmall && (
144-
<MenuItemLink
145-
to="/configuration"
146-
primaryText={translate('pos.configuration')}
147-
leftIcon={<SettingsIcon />}
148-
onClick={onMenuClick}
149-
sidebarIsOpen={open}
150-
dense={dense}
151-
/>
152-
)}
153-
{isXSmall && logout}
154-
</Box>
122+
</div>
155123
);
156124
};
157125

126+
const useStyles = makeStyles(theme => ({
127+
root: {
128+
marginTop: theme.spacing(1),
129+
},
130+
}));
131+
158132
export default Menu;

0 commit comments

Comments
 (0)