mirror of
https://github.com/strapi/strapi.git
synced 2025-08-24 16:49:28 +00:00
feat(main-nav): Main nav refactoring, change links order (#20275)
* feat(main-nav): change links order in main nav * feat(main-nav): add the position property to order links in the main nav * feat(main-nav): refactor the sorting of the nav links * feat(main-nav): add useCollator and format message to sort by name the links
This commit is contained in:
parent
8c5105d949
commit
c734c14d5f
@ -64,6 +64,7 @@ interface MenuItem {
|
|||||||
Component: React.LazyExoticComponent<React.ComponentType>;
|
Component: React.LazyExoticComponent<React.ComponentType>;
|
||||||
exact?: boolean;
|
exact?: boolean;
|
||||||
lockIcon?: boolean;
|
lockIcon?: boolean;
|
||||||
|
position?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StrapiAppSettingLink extends Omit<MenuItem, 'icon' | 'notificationCount'> {
|
interface StrapiAppSettingLink extends Omit<MenuItem, 'icon' | 'notificationCount'> {
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { Divider, Flex, FlexComponent } from '@strapi/design-system';
|
import { Divider, Flex, FlexComponent, useCollator } from '@strapi/design-system';
|
||||||
import { Feather, Lock, House } from '@strapi/icons';
|
import { Lock } from '@strapi/icons';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { styled } from 'styled-components';
|
import { styled } from 'styled-components';
|
||||||
|
|
||||||
import { useAuth } from '../features/Auth';
|
import { useAuth } from '../features/Auth';
|
||||||
import { useTracking } from '../features/Tracking';
|
import { useTracking } from '../features/Tracking';
|
||||||
import { Menu } from '../hooks/useMenu';
|
import { Menu, MenuItem } from '../hooks/useMenu';
|
||||||
import { getDisplayName } from '../utils/users';
|
import { getDisplayName } from '../utils/users';
|
||||||
|
|
||||||
import { MainNav } from './MainNav/MainNav';
|
import { MainNav } from './MainNav/MainNav';
|
||||||
@ -16,12 +16,30 @@ import { NavBrand } from './MainNav/NavBrand';
|
|||||||
import { NavLink } from './MainNav/NavLink';
|
import { NavLink } from './MainNav/NavLink';
|
||||||
import { NavUser } from './MainNav/NavUser';
|
import { NavUser } from './MainNav/NavUser';
|
||||||
|
|
||||||
const NewNavLinkBadge = styled(NavLink.Badge)`
|
const sortLinks = (links: MenuItem[]) => {
|
||||||
|
return links.sort((a, b) => {
|
||||||
|
// if no position is defined, we put the link in the position of the external plugins, before the plugins list
|
||||||
|
const positionA = a.position ?? 6;
|
||||||
|
const positionB = b.position ?? 6;
|
||||||
|
|
||||||
|
if (positionA < positionB) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const NavLinkBadgeCounter = styled(NavLink.Badge)`
|
||||||
span {
|
span {
|
||||||
color: ${({ theme }) => theme.colors.neutral0};
|
color: ${({ theme }) => theme.colors.neutral0};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const NavLinkBadgeLock = styled(NavLink.Badge)`
|
||||||
|
background-color: transparent;
|
||||||
|
`;
|
||||||
|
|
||||||
const NavListWrapper = styled<FlexComponent<'ul'>>(Flex)`
|
const NavListWrapper = styled<FlexComponent<'ul'>>(Flex)`
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
`;
|
`;
|
||||||
@ -30,10 +48,13 @@ interface LeftMenuProps extends Pick<Menu, 'generalSectionLinks' | 'pluginsSecti
|
|||||||
|
|
||||||
const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) => {
|
const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) => {
|
||||||
const user = useAuth('AuthenticatedApp', (state) => state.user);
|
const user = useAuth('AuthenticatedApp', (state) => state.user);
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
const { trackUsage } = useTracking();
|
const { trackUsage } = useTracking();
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const userDisplayName = getDisplayName(user);
|
const userDisplayName = getDisplayName(user);
|
||||||
|
const { formatMessage, locale } = useIntl();
|
||||||
|
const formatter = useCollator(locale, {
|
||||||
|
sensitivity: 'base',
|
||||||
|
});
|
||||||
|
|
||||||
const initials = userDisplayName
|
const initials = userDisplayName
|
||||||
.split(' ')
|
.split(' ')
|
||||||
@ -45,6 +66,11 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) =
|
|||||||
trackUsage('willNavigate', { from: pathname, to: destination });
|
trackUsage('willNavigate', { from: pathname, to: destination });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const listLinksAlphabeticallySorted = [...pluginsSectionLinks, ...generalSectionLinks].sort(
|
||||||
|
(a, b) => formatter.compare(formatMessage(a.intlLabel), formatMessage(b.intlLabel))
|
||||||
|
);
|
||||||
|
const listLinks = sortLinks(listLinksAlphabeticallySorted);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainNav>
|
<MainNav>
|
||||||
<NavBrand />
|
<NavBrand />
|
||||||
@ -52,108 +78,47 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) =
|
|||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<NavListWrapper tag="ul" gap={3} direction="column" flex={1} paddingTop={3} paddingBottom={3}>
|
<NavListWrapper tag="ul" gap={3} direction="column" flex={1} paddingTop={3} paddingBottom={3}>
|
||||||
<Flex tag="li">
|
{listLinks.length > 0
|
||||||
<NavLink.Tooltip label={formatMessage({ id: 'global.home', defaultMessage: 'Home' })}>
|
? listLinks.map((link) => {
|
||||||
<NavLink.Link
|
|
||||||
to="/"
|
|
||||||
onClick={() => handleClickOnLink('/')}
|
|
||||||
aria-label={formatMessage({ id: 'global.home', defaultMessage: 'Home' })}
|
|
||||||
>
|
|
||||||
<NavLink.Icon label={formatMessage({ id: 'global.home', defaultMessage: 'Home' })}>
|
|
||||||
<House width="2rem" height="2rem" fill="neutral500" />
|
|
||||||
</NavLink.Icon>
|
|
||||||
</NavLink.Link>
|
|
||||||
</NavLink.Tooltip>
|
|
||||||
</Flex>
|
|
||||||
<Flex tag="li">
|
|
||||||
<NavLink.Tooltip
|
|
||||||
label={formatMessage({
|
|
||||||
id: 'global.content-manager',
|
|
||||||
defaultMessage: 'Content Manager',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<NavLink.Link
|
|
||||||
to="/content-manager"
|
|
||||||
onClick={() => handleClickOnLink('/content-manager')}
|
|
||||||
aria-label={formatMessage({
|
|
||||||
id: 'global.content-manager',
|
|
||||||
defaultMessage: 'Content Manager',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<NavLink.Icon
|
|
||||||
label={formatMessage({
|
|
||||||
id: 'global.content-manager',
|
|
||||||
defaultMessage: 'Content Manager',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Feather width="2rem" height="2rem" fill="neutral500" />
|
|
||||||
</NavLink.Icon>
|
|
||||||
</NavLink.Link>
|
|
||||||
</NavLink.Tooltip>
|
|
||||||
</Flex>
|
|
||||||
{pluginsSectionLinks.length > 0
|
|
||||||
? pluginsSectionLinks.map((link) => {
|
|
||||||
if (link.to === 'content-manager') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LinkIcon = link.icon;
|
const LinkIcon = link.icon;
|
||||||
const badgeContent = link?.lockIcon ? <Lock /> : undefined;
|
const badgeContentLock = link?.lockIcon ? <Lock /> : undefined;
|
||||||
|
|
||||||
const labelValue = formatMessage(link.intlLabel);
|
const badgeContentNumeric =
|
||||||
return (
|
|
||||||
<Flex tag="li" key={link.to}>
|
|
||||||
<NavLink.Tooltip label={labelValue}>
|
|
||||||
<NavLink.Link
|
|
||||||
to={link.to}
|
|
||||||
onClick={() => handleClickOnLink(link.to)}
|
|
||||||
aria-label={labelValue}
|
|
||||||
>
|
|
||||||
<NavLink.Icon label={labelValue}>
|
|
||||||
<LinkIcon width="2rem" height="2rem" fill="neutral500" />
|
|
||||||
</NavLink.Icon>
|
|
||||||
{badgeContent && (
|
|
||||||
<NavLink.Badge
|
|
||||||
label="locked"
|
|
||||||
background="transparent"
|
|
||||||
textColor="neutral500"
|
|
||||||
>
|
|
||||||
{badgeContent}
|
|
||||||
</NavLink.Badge>
|
|
||||||
)}
|
|
||||||
</NavLink.Link>
|
|
||||||
</NavLink.Tooltip>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: null}
|
|
||||||
{generalSectionLinks.length > 0
|
|
||||||
? generalSectionLinks.map((link) => {
|
|
||||||
const LinkIcon = link.icon;
|
|
||||||
|
|
||||||
const badgeContent =
|
|
||||||
link.notificationsCount && link.notificationsCount > 0
|
link.notificationsCount && link.notificationsCount > 0
|
||||||
? link.notificationsCount.toString()
|
? link.notificationsCount.toString()
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const labelValue = formatMessage(link.intlLabel);
|
const labelValue = formatMessage(link.intlLabel);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex tag="li" key={link.to}>
|
<Flex tag="li" key={link.to}>
|
||||||
<NavLink.Tooltip label={labelValue}>
|
<NavLink.Tooltip label={labelValue}>
|
||||||
<NavLink.Link
|
<NavLink.Link
|
||||||
aria-label={labelValue}
|
|
||||||
to={link.to}
|
to={link.to}
|
||||||
onClick={() => handleClickOnLink(link.to)}
|
onClick={() => handleClickOnLink(link.to)}
|
||||||
|
aria-label={labelValue}
|
||||||
>
|
>
|
||||||
<NavLink.Icon label={labelValue}>
|
<NavLink.Icon label={labelValue}>
|
||||||
<LinkIcon width="2rem" height="2rem" fill="neutral500" />
|
<LinkIcon width="2rem" height="2rem" fill="neutral500" />
|
||||||
</NavLink.Icon>
|
</NavLink.Icon>
|
||||||
{badgeContent && (
|
{badgeContentLock ? (
|
||||||
<NewNavLinkBadge label={badgeContent} backgroundColor="primary600">
|
<NavLinkBadgeLock
|
||||||
{badgeContent}
|
label="locked"
|
||||||
</NewNavLinkBadge>
|
textColor="neutral500"
|
||||||
)}
|
paddingLeft={0}
|
||||||
|
paddingRight={0}
|
||||||
|
>
|
||||||
|
{badgeContentLock}
|
||||||
|
</NavLinkBadgeLock>
|
||||||
|
) : badgeContentNumeric ? (
|
||||||
|
<NavLinkBadgeCounter
|
||||||
|
label={badgeContentNumeric}
|
||||||
|
backgroundColor="primary600"
|
||||||
|
width="2.3rem"
|
||||||
|
color="neutral0"
|
||||||
|
>
|
||||||
|
{badgeContentNumeric}
|
||||||
|
</NavLinkBadgeCounter>
|
||||||
|
) : null}
|
||||||
</NavLink.Link>
|
</NavLink.Link>
|
||||||
</NavLink.Tooltip>
|
</NavLink.Tooltip>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -81,7 +81,6 @@ const BadgeImpl = ({ children, label, ...props }: NavLink.NavBadgeProps) => {
|
|||||||
return (
|
return (
|
||||||
<CustomBadge
|
<CustomBadge
|
||||||
position="absolute"
|
position="absolute"
|
||||||
width="2.3rem"
|
|
||||||
top="-0.8rem"
|
top="-0.8rem"
|
||||||
left="1.7rem"
|
left="1.7rem"
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { Cog, PuzzlePiece, ShoppingCart } from '@strapi/icons';
|
import { Cog, PuzzlePiece, ShoppingCart, House } from '@strapi/icons';
|
||||||
import cloneDeep from 'lodash/cloneDeep';
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ import { selectAdminPermissions } from '../selectors';
|
|||||||
* useMenu
|
* useMenu
|
||||||
* -----------------------------------------------------------------------------------------------*/
|
* -----------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
type MenuItem = Omit<StrapiAppContextValue['menu'][number], 'Component'>;
|
export type MenuItem = Omit<StrapiAppContextValue['menu'][number], 'Component'>;
|
||||||
|
|
||||||
export interface Menu {
|
export interface Menu {
|
||||||
generalSectionLinks: MenuItem[];
|
generalSectionLinks: MenuItem[];
|
||||||
@ -26,6 +26,16 @@ const useMenu = (shouldUpdateStrapi: boolean) => {
|
|||||||
const permissions = useSelector(selectAdminPermissions);
|
const permissions = useSelector(selectAdminPermissions);
|
||||||
const [menuWithUserPermissions, setMenuWithUserPermissions] = React.useState<Menu>({
|
const [menuWithUserPermissions, setMenuWithUserPermissions] = React.useState<Menu>({
|
||||||
generalSectionLinks: [
|
generalSectionLinks: [
|
||||||
|
{
|
||||||
|
icon: House,
|
||||||
|
intlLabel: {
|
||||||
|
id: 'global.home',
|
||||||
|
defaultMessage: 'Home',
|
||||||
|
},
|
||||||
|
to: '/',
|
||||||
|
permissions: [],
|
||||||
|
position: 0,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: PuzzlePiece,
|
icon: PuzzlePiece,
|
||||||
intlLabel: {
|
intlLabel: {
|
||||||
@ -34,6 +44,7 @@ const useMenu = (shouldUpdateStrapi: boolean) => {
|
|||||||
},
|
},
|
||||||
to: '/list-plugins',
|
to: '/list-plugins',
|
||||||
permissions: permissions.marketplace?.main ?? [],
|
permissions: permissions.marketplace?.main ?? [],
|
||||||
|
position: 7,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: ShoppingCart,
|
icon: ShoppingCart,
|
||||||
@ -43,6 +54,7 @@ const useMenu = (shouldUpdateStrapi: boolean) => {
|
|||||||
},
|
},
|
||||||
to: '/marketplace',
|
to: '/marketplace',
|
||||||
permissions: permissions.marketplace?.main ?? [],
|
permissions: permissions.marketplace?.main ?? [],
|
||||||
|
position: 8,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Cog,
|
icon: Cog,
|
||||||
@ -55,6 +67,7 @@ const useMenu = (shouldUpdateStrapi: boolean) => {
|
|||||||
// using the settings menu
|
// using the settings menu
|
||||||
permissions: [],
|
permissions: [],
|
||||||
notificationsCount: 0,
|
notificationsCount: 0,
|
||||||
|
position: 10,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
pluginsSectionLinks: [],
|
pluginsSectionLinks: [],
|
||||||
|
@ -27,6 +27,7 @@ export default {
|
|||||||
},
|
},
|
||||||
permissions: [],
|
permissions: [],
|
||||||
Component: () => import('./layout').then((mod) => ({ default: mod.Layout })),
|
Component: () => import('./layout').then((mod) => ({ default: mod.Layout })),
|
||||||
|
position: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
app.registerPlugin(cm.config);
|
app.registerPlugin(cm.config);
|
||||||
|
@ -32,6 +32,7 @@ const admin: Plugin.Config.AdminInput = {
|
|||||||
},
|
},
|
||||||
Component: () => import('./pages/App').then((mod) => ({ default: mod.App })),
|
Component: () => import('./pages/App').then((mod) => ({ default: mod.App })),
|
||||||
permissions: PERMISSIONS.main,
|
permissions: PERMISSIONS.main,
|
||||||
|
position: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -77,6 +78,7 @@ const admin: Plugin.Config.AdminInput = {
|
|||||||
return { default: PurchaseContentReleases };
|
return { default: PurchaseContentReleases };
|
||||||
},
|
},
|
||||||
lockIcon: true,
|
lockIcon: true,
|
||||||
|
position: 2,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -23,6 +23,7 @@ export default {
|
|||||||
},
|
},
|
||||||
permissions: PERMISSIONS.main,
|
permissions: PERMISSIONS.main,
|
||||||
Component: () => import('./pages/App'),
|
Component: () => import('./pages/App'),
|
||||||
|
position: 5,
|
||||||
});
|
});
|
||||||
|
|
||||||
app.registerPlugin({
|
app.registerPlugin({
|
||||||
|
@ -22,6 +22,7 @@ export default {
|
|||||||
},
|
},
|
||||||
permissions: PERMISSIONS.main,
|
permissions: PERMISSIONS.main,
|
||||||
Component: () => import('./pages/App'),
|
Component: () => import('./pages/App'),
|
||||||
|
position: 4,
|
||||||
});
|
});
|
||||||
|
|
||||||
app.addSettingsLink('global', {
|
app.addSettingsLink('global', {
|
||||||
|
@ -20,6 +20,7 @@ export default {
|
|||||||
const { App } = await import('./pages/App');
|
const { App } = await import('./pages/App');
|
||||||
return App;
|
return App;
|
||||||
},
|
},
|
||||||
|
position: 9,
|
||||||
});
|
});
|
||||||
|
|
||||||
app.addMiddlewares([() => api.middleware]);
|
app.addMiddlewares([() => api.middleware]);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user