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:
Simone 2024-05-13 08:58:09 +02:00 committed by GitHub
parent 8c5105d949
commit c734c14d5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 77 additions and 93 deletions

View File

@ -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'> {

View File

@ -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>

View File

@ -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}

View File

@ -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: [],

View File

@ -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);

View File

@ -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,
}); });
} }
}, },

View File

@ -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({

View File

@ -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', {

View File

@ -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]);