Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix logout button appears in two different menus #6230

Merged
merged 3 commits into from
May 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 109 additions & 47 deletions docs/Theming.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<Resource>` components passed as children of `<Admin>` to build a menu to each resource with a `list` component.
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`.

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 `<DashboardMenuItem>` and `<MenuItemLink>` 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 = () => (
<div>
<DashboardMenuItem />
<MenuItemLink to="/posts" primaryText="Posts" leftIcon={<BookIcon />}/>
<MenuItemLink to="/comments" primaryText="Comments" leftIcon={<ChatBubbleIcon />}/>
<MenuItemLink to="/users" primaryText="Users" leftIcon={<PeopleIcon />}/>
<MenuItemLink to="/custom-route" primaryText="Miscellaneous" leftIcon={<LabelIcon />}/>
</div>
);
```

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) => <Layout {...props} menu={Menu} />;
```

Then, use this layout in the `<Admin>` `layout` prop:

```jsx
// in src/App.js
import { Layout } from './Layout';

const App = () => (
<Admin layout={Layout} dataProvider={simpleRestProvider('http://path.to.my.api')}>
// ...
</Admin>
);
```

**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
Expand All @@ -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 (
<div>
<DashboardMenuItem onClick={onMenuClick} sidebarIsOpen={open} />
<DashboardMenuItem />
{resources.map(resource => (
<MenuItemLink
key={resource.name}
Expand All @@ -843,71 +888,88 @@ const Menu = ({ onMenuClick, logout }) => {
sidebarIsOpen={open}
/>
))}
<MenuItemLink
to="/custom-route"
primaryText="Miscellaneous"
leftIcon={<LabelIcon />}
onClick={onMenuClick}
sidebarIsOpen={open}
/>
{isXSmall && logout}
{/* add your custom menus here */}
</div>
);
};

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`<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))

**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';
### `<MenuItemLink>`

<MenuItemLink to="/custom-route" primaryText={
<Badge badgeContent={4} color="primary">
Notifications
</Badge>
} onClick={onMenuClick} />
```
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.

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) => <Layout {...props} menu={MyMenu} />;

export default MyLayout;
<MenuItemLink to="/custom-route" primaryText={
<Badge badgeContent={4} color="primary">
Notifications
</Badge>
} />
```

Then, use this layout in the `<Admin>` `layout` prop:
The `letfIcon` 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 `<MenuItem>` component](https://material-ui.com/api/menu-item/#menuitem-api).

const App = () => (
<Admin layout={MyLayout} dataProvider={simpleRestProvider('http://path.to.my.api')}>
**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:

```jsx
export const theme = {
palette: {
// ...
</Admin>
);
},
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`<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))
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
<MenuItemLink
to={{
pathname: '/posts',
search: `filter=${JSON.stringify({ is_published: true })}`,
}}
primaryText="Posts"
leftIcon={<BookIcon />}
/>
```

![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 `<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.

Just use an empty `filter` query parameter to force empty filters:

```jsx
<MenuItemLink
to="/posts?filter=%7B%7D" // %7B%7D is JSON.stringify({})
primaryText="Posts"
leftIcon={<BookIcon />}
/>
```

## Using a Custom Login Page

Expand Down
52 changes: 13 additions & 39 deletions examples/demo/src/layout/Menu.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import * as React from 'react';
import { FC, useState } from 'react';
import { 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 { makeStyles } from '@material-ui/core/styles';
import {
useTranslate,
DashboardMenuItem,
Expand All @@ -22,31 +21,27 @@ import { AppState } from '../types';

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

const Menu: FC<MenuProps> = ({ onMenuClick, logout, dense = false }) => {
const Menu = ({ dense = false }: MenuProps) => {
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 classes = useStyles();

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

return (
<Box mt={1}>
<div className={classes.root}>
{' '}
<DashboardMenuItem onClick={onMenuClick} sidebarIsOpen={open} />
<DashboardMenuItem />
<SubMenu
handleToggle={() => handleToggle('menuSales')}
isOpen={state.menuSales}
sidebarIsOpen={open}
name="pos.menu.sales"
icon={<orders.icon />}
dense={dense}
Expand All @@ -57,8 +52,6 @@ const Menu: FC<MenuProps> = ({ onMenuClick, logout, dense = false }) => {
smart_count: 2,
})}
leftIcon={<orders.icon />}
onClick={onMenuClick}
sidebarIsOpen={open}
dense={dense}
/>
<MenuItemLink
Expand All @@ -67,15 +60,12 @@ const Menu: FC<MenuProps> = ({ onMenuClick, logout, dense = false }) => {
smart_count: 2,
})}
leftIcon={<invoices.icon />}
onClick={onMenuClick}
sidebarIsOpen={open}
dense={dense}
/>
</SubMenu>
<SubMenu
handleToggle={() => handleToggle('menuCatalog')}
isOpen={state.menuCatalog}
sidebarIsOpen={open}
name="pos.menu.catalog"
icon={<products.icon />}
dense={dense}
Expand All @@ -86,8 +76,6 @@ const Menu: FC<MenuProps> = ({ onMenuClick, logout, dense = false }) => {
smart_count: 2,
})}
leftIcon={<products.icon />}
onClick={onMenuClick}
sidebarIsOpen={open}
dense={dense}
/>
<MenuItemLink
Expand All @@ -96,15 +84,12 @@ const Menu: FC<MenuProps> = ({ onMenuClick, logout, dense = false }) => {
smart_count: 2,
})}
leftIcon={<categories.icon />}
onClick={onMenuClick}
sidebarIsOpen={open}
dense={dense}
/>
</SubMenu>
<SubMenu
handleToggle={() => handleToggle('menuCustomers')}
isOpen={state.menuCustomers}
sidebarIsOpen={open}
name="pos.menu.customers"
icon={<visitors.icon />}
dense={dense}
Expand All @@ -115,8 +100,6 @@ const Menu: FC<MenuProps> = ({ onMenuClick, logout, dense = false }) => {
smart_count: 2,
})}
leftIcon={<visitors.icon />}
onClick={onMenuClick}
sidebarIsOpen={open}
dense={dense}
/>
<MenuItemLink
Expand All @@ -125,8 +108,6 @@ const Menu: FC<MenuProps> = ({ onMenuClick, logout, dense = false }) => {
smart_count: 2,
})}
leftIcon={<LabelIcon />}
onClick={onMenuClick}
sidebarIsOpen={open}
dense={dense}
/>
</SubMenu>
Expand All @@ -136,23 +117,16 @@ const Menu: FC<MenuProps> = ({ onMenuClick, logout, dense = false }) => {
smart_count: 2,
})}
leftIcon={<reviews.icon />}
onClick={onMenuClick}
sidebarIsOpen={open}
dense={dense}
/>
{isXSmall && (
<MenuItemLink
to="/configuration"
primaryText={translate('pos.configuration')}
leftIcon={<SettingsIcon />}
onClick={onMenuClick}
sidebarIsOpen={open}
dense={dense}
/>
)}
{isXSmall && logout}
</Box>
</div>
);
};

const useStyles = makeStyles(theme => ({
root: {
marginTop: theme.spacing(1),
},
}));

export default Menu;
Loading