mirror of
https://github.com/strapi/strapi.git
synced 2025-08-23 16:19:37 +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>;
|
||||
exact?: boolean;
|
||||
lockIcon?: boolean;
|
||||
position?: number;
|
||||
}
|
||||
|
||||
interface StrapiAppSettingLink extends Omit<MenuItem, 'icon' | 'notificationCount'> {
|
||||
|
@ -1,14 +1,14 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Divider, Flex, FlexComponent } from '@strapi/design-system';
|
||||
import { Feather, Lock, House } from '@strapi/icons';
|
||||
import { Divider, Flex, FlexComponent, useCollator } from '@strapi/design-system';
|
||||
import { Lock } from '@strapi/icons';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { styled } from 'styled-components';
|
||||
|
||||
import { useAuth } from '../features/Auth';
|
||||
import { useTracking } from '../features/Tracking';
|
||||
import { Menu } from '../hooks/useMenu';
|
||||
import { Menu, MenuItem } from '../hooks/useMenu';
|
||||
import { getDisplayName } from '../utils/users';
|
||||
|
||||
import { MainNav } from './MainNav/MainNav';
|
||||
@ -16,12 +16,30 @@ import { NavBrand } from './MainNav/NavBrand';
|
||||
import { NavLink } from './MainNav/NavLink';
|
||||
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 {
|
||||
color: ${({ theme }) => theme.colors.neutral0};
|
||||
}
|
||||
`;
|
||||
|
||||
const NavLinkBadgeLock = styled(NavLink.Badge)`
|
||||
background-color: transparent;
|
||||
`;
|
||||
|
||||
const NavListWrapper = styled<FlexComponent<'ul'>>(Flex)`
|
||||
overflow-y: auto;
|
||||
`;
|
||||
@ -30,10 +48,13 @@ interface LeftMenuProps extends Pick<Menu, 'generalSectionLinks' | 'pluginsSecti
|
||||
|
||||
const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) => {
|
||||
const user = useAuth('AuthenticatedApp', (state) => state.user);
|
||||
const { formatMessage } = useIntl();
|
||||
const { trackUsage } = useTracking();
|
||||
const { pathname } = useLocation();
|
||||
const userDisplayName = getDisplayName(user);
|
||||
const { formatMessage, locale } = useIntl();
|
||||
const formatter = useCollator(locale, {
|
||||
sensitivity: 'base',
|
||||
});
|
||||
|
||||
const initials = userDisplayName
|
||||
.split(' ')
|
||||
@ -45,6 +66,11 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) =
|
||||
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 (
|
||||
<MainNav>
|
||||
<NavBrand />
|
||||
@ -52,108 +78,47 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) =
|
||||
<Divider />
|
||||
|
||||
<NavListWrapper tag="ul" gap={3} direction="column" flex={1} paddingTop={3} paddingBottom={3}>
|
||||
<Flex tag="li">
|
||||
<NavLink.Tooltip label={formatMessage({ id: 'global.home', defaultMessage: 'Home' })}>
|
||||
<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;
|
||||
}
|
||||
|
||||
{listLinks.length > 0
|
||||
? listLinks.map((link) => {
|
||||
const LinkIcon = link.icon;
|
||||
const badgeContent = link?.lockIcon ? <Lock /> : undefined;
|
||||
const badgeContentLock = link?.lockIcon ? <Lock /> : undefined;
|
||||
|
||||
const labelValue = formatMessage(link.intlLabel);
|
||||
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 =
|
||||
const badgeContentNumeric =
|
||||
link.notificationsCount && link.notificationsCount > 0
|
||||
? link.notificationsCount.toString()
|
||||
: undefined;
|
||||
|
||||
const labelValue = formatMessage(link.intlLabel);
|
||||
|
||||
return (
|
||||
<Flex tag="li" key={link.to}>
|
||||
<NavLink.Tooltip label={labelValue}>
|
||||
<NavLink.Link
|
||||
aria-label={labelValue}
|
||||
to={link.to}
|
||||
onClick={() => handleClickOnLink(link.to)}
|
||||
aria-label={labelValue}
|
||||
>
|
||||
<NavLink.Icon label={labelValue}>
|
||||
<LinkIcon width="2rem" height="2rem" fill="neutral500" />
|
||||
</NavLink.Icon>
|
||||
{badgeContent && (
|
||||
<NewNavLinkBadge label={badgeContent} backgroundColor="primary600">
|
||||
{badgeContent}
|
||||
</NewNavLinkBadge>
|
||||
)}
|
||||
{badgeContentLock ? (
|
||||
<NavLinkBadgeLock
|
||||
label="locked"
|
||||
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.Tooltip>
|
||||
</Flex>
|
||||
|
@ -81,7 +81,6 @@ const BadgeImpl = ({ children, label, ...props }: NavLink.NavBadgeProps) => {
|
||||
return (
|
||||
<CustomBadge
|
||||
position="absolute"
|
||||
width="2.3rem"
|
||||
top="-0.8rem"
|
||||
left="1.7rem"
|
||||
aria-label={label}
|
||||
|
@ -1,6 +1,6 @@
|
||||
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 { useSelector } from 'react-redux';
|
||||
|
||||
@ -12,7 +12,7 @@ import { selectAdminPermissions } from '../selectors';
|
||||
* useMenu
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
type MenuItem = Omit<StrapiAppContextValue['menu'][number], 'Component'>;
|
||||
export type MenuItem = Omit<StrapiAppContextValue['menu'][number], 'Component'>;
|
||||
|
||||
export interface Menu {
|
||||
generalSectionLinks: MenuItem[];
|
||||
@ -26,6 +26,16 @@ const useMenu = (shouldUpdateStrapi: boolean) => {
|
||||
const permissions = useSelector(selectAdminPermissions);
|
||||
const [menuWithUserPermissions, setMenuWithUserPermissions] = React.useState<Menu>({
|
||||
generalSectionLinks: [
|
||||
{
|
||||
icon: House,
|
||||
intlLabel: {
|
||||
id: 'global.home',
|
||||
defaultMessage: 'Home',
|
||||
},
|
||||
to: '/',
|
||||
permissions: [],
|
||||
position: 0,
|
||||
},
|
||||
{
|
||||
icon: PuzzlePiece,
|
||||
intlLabel: {
|
||||
@ -34,6 +44,7 @@ const useMenu = (shouldUpdateStrapi: boolean) => {
|
||||
},
|
||||
to: '/list-plugins',
|
||||
permissions: permissions.marketplace?.main ?? [],
|
||||
position: 7,
|
||||
},
|
||||
{
|
||||
icon: ShoppingCart,
|
||||
@ -43,6 +54,7 @@ const useMenu = (shouldUpdateStrapi: boolean) => {
|
||||
},
|
||||
to: '/marketplace',
|
||||
permissions: permissions.marketplace?.main ?? [],
|
||||
position: 8,
|
||||
},
|
||||
{
|
||||
icon: Cog,
|
||||
@ -55,6 +67,7 @@ const useMenu = (shouldUpdateStrapi: boolean) => {
|
||||
// using the settings menu
|
||||
permissions: [],
|
||||
notificationsCount: 0,
|
||||
position: 10,
|
||||
},
|
||||
],
|
||||
pluginsSectionLinks: [],
|
||||
|
@ -27,6 +27,7 @@ export default {
|
||||
},
|
||||
permissions: [],
|
||||
Component: () => import('./layout').then((mod) => ({ default: mod.Layout })),
|
||||
position: 1,
|
||||
});
|
||||
|
||||
app.registerPlugin(cm.config);
|
||||
|
@ -32,6 +32,7 @@ const admin: Plugin.Config.AdminInput = {
|
||||
},
|
||||
Component: () => import('./pages/App').then((mod) => ({ default: mod.App })),
|
||||
permissions: PERMISSIONS.main,
|
||||
position: 2,
|
||||
});
|
||||
|
||||
/**
|
||||
@ -77,6 +78,7 @@ const admin: Plugin.Config.AdminInput = {
|
||||
return { default: PurchaseContentReleases };
|
||||
},
|
||||
lockIcon: true,
|
||||
position: 2,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -23,6 +23,7 @@ export default {
|
||||
},
|
||||
permissions: PERMISSIONS.main,
|
||||
Component: () => import('./pages/App'),
|
||||
position: 5,
|
||||
});
|
||||
|
||||
app.registerPlugin({
|
||||
|
@ -22,6 +22,7 @@ export default {
|
||||
},
|
||||
permissions: PERMISSIONS.main,
|
||||
Component: () => import('./pages/App'),
|
||||
position: 4,
|
||||
});
|
||||
|
||||
app.addSettingsLink('global', {
|
||||
|
@ -20,6 +20,7 @@ export default {
|
||||
const { App } = await import('./pages/App');
|
||||
return App;
|
||||
},
|
||||
position: 9,
|
||||
});
|
||||
|
||||
app.addMiddlewares([() => api.middleware]);
|
||||
|
Loading…
x
Reference in New Issue
Block a user