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

feat(main-nav): Main nav refactoring, refactor the main nav container and all the links and user profile #20245

Merged
merged 28 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8e552ff
feat(main-nav): replace DS NavLink with admin NavLink
simotae14 Apr 22, 2024
0cfe908
Merge branch 'v5/main' into feat/main-nav-refactoring-home-nav-link
simotae14 Apr 22, 2024
dfd0653
feat(main-nav): change icon type
simotae14 Apr 22, 2024
5ac8e4c
feat(main-nav): fix prettier errors
simotae14 Apr 23, 2024
bf1a3e6
Merge branch 'v5/main' into feat/main-nav-refactoring-home-nav-link
simotae14 Apr 23, 2024
b7827e3
feat(main-nav): refactor navlink code and add more test cases
simotae14 Apr 23, 2024
49a70c0
Merge branch 'v5/main' into feat/main-nav-refactoring-home-nav-link
simotae14 Apr 23, 2024
6f68642
feat(main-nav): minor fixes
simotae14 Apr 24, 2024
e50c9f4
feat(main-nav): fix ui errors
simotae14 Apr 24, 2024
ed788a0
Merge branch 'v5/main' into feat/main-nav-refactoring-home-nav-link
simotae14 Apr 26, 2024
86fde86
feat(main-nav): fix merge issues
simotae14 Apr 26, 2024
b17f8d7
feat(main-nav): fix unit test and types
simotae14 Apr 26, 2024
ee86ec4
Merge branch 'v5/main' into feat/main-nav-refactoring-new-nav
simotae14 Apr 29, 2024
2d73606
Merge branch 'v5/main' into feat/main-nav-refactoring-new-nav
simotae14 Apr 30, 2024
dd8daaa
feat(main-nav): implement the new main nav ui
simotae14 May 2, 2024
25f2f46
Merge branch 'v5/main' into feat/main-nav-refactoring-new-nav
simotae14 May 2, 2024
346af1d
feat(main-nav): change on blur handler
simotae14 May 2, 2024
30fd02d
Merge branch 'v5/main' into feat/main-nav-refactoring-new-nav
simotae14 May 2, 2024
b7e3605
feat(main-nav): fix TS error
simotae14 May 2, 2024
a65a85f
feat(main-nav): refactor navUser using the Menu component
simotae14 May 2, 2024
0e21260
feat(main-nav): add aria label to the links
simotae14 May 3, 2024
b7894f6
Merge branch 'v5/main' into feat/main-nav-refactoring-new-nav
simotae14 May 3, 2024
995954b
feat(main-nav): add menu item in the nav user links
simotae14 May 3, 2024
f897ead
feat(main-nav): refactor nav user and the menu items
simotae14 May 3, 2024
c053321
Merge branch 'v5/main' into feat/main-nav-refactoring-new-nav
simotae14 May 3, 2024
3f2ef2f
feat(main-nav): change locator
simotae14 May 3, 2024
d6c1670
feat(main-nav): revert e2e utils
simotae14 May 3, 2024
42ef2b8
feat(main-nav): add nav user unit test
simotae14 May 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
326 changes: 103 additions & 223 deletions packages/core/admin/admin/src/components/LeftMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,82 +1,38 @@
import * as React from 'react';

import {
Box,
Divider,
Flex,
FocusTrap,
Typography,
MainNav,
NavBrand,
NavCondense,
NavFooter,
NavLink,
NavSection,
NavSections,
NavUser,
simotae14 marked this conversation as resolved.
Show resolved Hide resolved
} from '@strapi/design-system';
import { SignOut, Feather, Lock, House } from '@strapi/icons';
import { Divider, Flex } from '@strapi/design-system';
import { Feather, Lock, House } from '@strapi/icons';
import { useIntl } from 'react-intl';
import { NavLink as RouterNavLink, useLocation } from 'react-router-dom';
import { useLocation } from 'react-router-dom';
import styled from 'styled-components';

import { useAuth } from '../features/Auth';
import { useConfiguration } from '../features/Configuration';
import { useTracking } from '../features/Tracking';
import { Menu } from '../hooks/useMenu';
import { usePersistentState } from '../hooks/usePersistentState';
import { getDisplayName } from '../utils/users';

import { NavBrand as NewNavBrand } from './MainNav/NavBrand';
import { NavLink as NewNavLink } from './MainNav/NavLink';
import { MainNav } from './MainNav/MainNav';
import { NavBrand } from './MainNav/NavBrand';
import { NavLink } from './MainNav/NavLink';
import { NavUser } from './MainNav/NavUser';

const LinkUserWrapper = styled(Box)`
width: 15rem;
position: absolute;
bottom: ${({ theme }) => theme.spaces[9]};
left: ${({ theme }) => theme.spaces[5]};
`;

const LinkUser = styled(RouterNavLink)<{ logout?: boolean }>`
display: flex;
justify-content: space-between;
align-items: center;
text-decoration: none;
padding: ${({ theme }) => `${theme.spaces[2]} ${theme.spaces[4]}`};
border-radius: ${({ theme }) => theme.spaces[1]};

&:hover {
background: ${({ theme, logout }) =>
logout ? theme.colors.danger100 : theme.colors.primary100};
text-decoration: none;
}

svg {
fill: ${({ theme }) => theme.colors.danger600};
const NewNavLinkBadge = styled(NavLink.Badge)`
span {
color: ${({ theme }) => theme.colors.neutral0};
}
`;

const NavLinkWrapper = styled(Box)`
div:nth-child(2) {
/* remove badge background color */
background: transparent;
}
const NavListWrapper = styled(Flex)`
overflow-y: auto;
`;

interface LeftMenuProps extends Pick<Menu, 'generalSectionLinks' | 'pluginsSectionLinks'> {}

const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) => {
const navUserRef = React.useRef<HTMLDivElement>(null!);
const [userLinksVisible, setUserLinksVisible] = React.useState(false);
const {
logos: { menu },
} = useConfiguration('LeftMenu');
const [condensed, setCondensed] = usePersistentState('navbar-condensed', false);
const user = useAuth('AuthenticatedApp', (state) => state.user);
const { formatMessage } = useIntl();
const { trackUsage } = useTracking();
const { pathname } = useLocation();
const logout = useAuth('Logout', (state) => state.logout);
const userDisplayName = getDisplayName(user);

const initials = userDisplayName
Expand All @@ -85,198 +41,122 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) =
.join('')
.substring(0, 2);

const handleToggleUserLinks = () => setUserLinksVisible((prev) => !prev);

const handleBlur: React.FocusEventHandler = (e) => {
if (
!e.currentTarget.contains(e.relatedTarget) &&
/**
* TODO: can we replace this by just using the navUserRef?
*/
e.relatedTarget?.parentElement?.id !== 'main-nav-user-button'
) {
setUserLinksVisible(false);
}
};

const handleClickOnLink = (destination: string) => {
trackUsage('willNavigate', { from: pathname, to: destination });
};

const menuTitle = formatMessage({
id: 'app.components.LeftMenu.navbrand.title',
defaultMessage: 'Strapi Dashboard',
});

return (
<MainNav condensed={condensed}>
{condensed ? (
/**
* TODO: remove the conditional rendering once the new Main nav is fully implemented
*/
<NewNavBrand />
) : (
<NavBrand
as={RouterNavLink}
workplace={formatMessage({
id: 'app.components.LeftMenu.navbrand.workplace',
defaultMessage: 'Workplace',
})}
title={menuTitle}
icon={
<img
src={menu.custom?.url || menu.default}
alt={formatMessage({
id: 'app.components.LeftMenu.logo.alt',
defaultMessage: 'Application logo',
})}
/>
}
/>
)}
<MainNav>
<NavBrand />

<Divider />

<NavSections>
{condensed && (
<NewNavLink.Link to="/" onClick={() => handleClickOnLink('/')}>
<NewNavLink.Tooltip
label={formatMessage({ id: 'global.home', defaultMessage: 'Home' })}
>
<NewNavLink.Icon>
<NavListWrapper as="ul" gap={3} direction="column" flex={1} paddingTop={3} paddingBottom={3}>
<Flex as="li">
<NavLink.Link
to="/"
onClick={() => handleClickOnLink('/')}
aria-label={formatMessage({ id: 'global.home', defaultMessage: 'Home' })}
>
<NavLink.Tooltip label={formatMessage({ id: 'global.home', defaultMessage: 'Home' })}>
<NavLink.Icon>
<House fill="neutral500" />
</NewNavLink.Icon>
</NewNavLink.Tooltip>
</NewNavLink.Link>
)}
<NavLink
as={RouterNavLink}
// @ts-expect-error the props from the passed as prop are not inferred // joined together
to="/content-manager"
icon={<Feather />}
onClick={() => handleClickOnLink('/content-manager')}
>
{formatMessage({ id: 'global.content-manager', defaultMessage: 'Content manager' })}
</NavLink>

{pluginsSectionLinks.length > 0 ? (
<NavSection
label={formatMessage({
id: 'app.components.LeftMenu.plugins',
defaultMessage: 'Plugins',
</NavLink.Icon>
</NavLink.Tooltip>
</NavLink.Link>
</Flex>
<Flex as="li">
<NavLink.Link
to="/content-manager"
onClick={() => handleClickOnLink('/content-manager')}
aria-label={formatMessage({
id: 'global.content-manager',
defaultMessage: 'Content Manager',
})}
>
{pluginsSectionLinks.map((link) => {
<NavLink.Tooltip
label={formatMessage({
id: 'global.content-manager',
defaultMessage: 'Content Manager',
})}
>
<NavLink.Icon>
<Feather fill="neutral500" />
</NavLink.Icon>
</NavLink.Tooltip>
</NavLink.Link>
</Flex>
{pluginsSectionLinks.length > 0
? pluginsSectionLinks.map((link) => {
if (link.to === 'content-manager') {
return null;
}

const LinkIcon = link.icon;
const badgeContent = link?.lockIcon ? <Lock /> : undefined;

const labelValue = formatMessage(link.intlLabel);
return (
<NavLinkWrapper key={link.to}>
<NavLink
as={RouterNavLink}
<Flex as="li" key={link.to}>
<NavLink.Link
to={link.to}
icon={<LinkIcon />}
onClick={() => handleClickOnLink(link.to)}
// @ts-expect-error: badgeContent in the DS accept only strings
badgeContent={
link?.lockIcon ? <Lock width="1.5rem" height="1.5rem" /> : undefined
}
aria-label={labelValue}
>
{formatMessage(link.intlLabel)}
</NavLink>
</NavLinkWrapper>
<NavLink.Tooltip label={labelValue}>
<NavLink.Icon>
<LinkIcon fill="neutral500" />
</NavLink.Icon>
{badgeContent && (
<NavLink.Badge
label="locked"
background="transparent"
textColor="neutral500"
>
{badgeContent}
</NavLink.Badge>
)}
</NavLink.Tooltip>
</NavLink.Link>
</Flex>
);
})}
</NavSection>
) : null}

{generalSectionLinks.length > 0 ? (
<NavSection
label={formatMessage({
id: 'app.components.LeftMenu.general',
defaultMessage: 'General',
})}
>
{generalSectionLinks.map((link) => {
})
: null}
{generalSectionLinks.length > 0
? generalSectionLinks.map((link) => {
const LinkIcon = link.icon;

return (
<NavLink
as={RouterNavLink}
badgeContent={
link.notificationsCount && link.notificationsCount > 0
? link.notificationsCount.toString()
: undefined
}
// @ts-expect-error the props from the passed as prop are not inferred // joined together
to={link.to}
key={link.to}
icon={<LinkIcon />}
onClick={() => handleClickOnLink(link.to)}
>
{formatMessage(link.intlLabel)}
</NavLink>
);
})}
</NavSection>
) : null}
</NavSections>
const badgeContent =
link.notificationsCount && link.notificationsCount > 0
? link.notificationsCount.toString()
: undefined;

<NavFooter>
<NavUser
id="main-nav-user-button"
ref={navUserRef}
onClick={handleToggleUserLinks}
initials={initials}
>
{userDisplayName}
</NavUser>
{userLinksVisible && (
<LinkUserWrapper
onBlur={handleBlur}
padding={1}
shadow="tableShadow"
background="neutral0"
hasRadius
>
<FocusTrap onEscape={handleToggleUserLinks}>
<Flex direction="column" alignItems="stretch" gap={0}>
<LinkUser tabIndex={0} onClick={handleToggleUserLinks} to="/me">
<Typography>
{formatMessage({
id: 'global.profile',
defaultMessage: 'Profile',
})}
</Typography>
</LinkUser>
<LinkUser tabIndex={0} onClick={logout} to="/auth/login">
<Typography textColor="danger600">
{formatMessage({
id: 'app.components.LeftMenu.logout',
defaultMessage: 'Logout',
})}
</Typography>
<SignOut />
</LinkUser>
</Flex>
</FocusTrap>
</LinkUserWrapper>
)}
const labelValue = formatMessage(link.intlLabel);

<NavCondense onClick={() => setCondensed((s) => !s)}>
{condensed
? formatMessage({
id: 'app.components.LeftMenu.expand',
defaultMessage: 'Expand the navbar',
})
: formatMessage({
id: 'app.components.LeftMenu.collapse',
defaultMessage: 'Collapse the navbar',
})}
</NavCondense>
</NavFooter>
return (
<Flex as="li" key={link.to}>
<NavLink.Link
aria-label={labelValue}
to={link.to}
onClick={() => handleClickOnLink(link.to)}
>
<NavLink.Tooltip label={labelValue}>
<NavLink.Icon>
<LinkIcon fill="neutral500" />
</NavLink.Icon>
{badgeContent && (
<NewNavLinkBadge label={badgeContent} backgroundColor="primary600">
{badgeContent}
</NewNavLinkBadge>
)}
</NavLink.Tooltip>
</NavLink.Link>
</Flex>
);
})
: null}
</NavListWrapper>
<NavUser initials={initials}>{userDisplayName}</NavUser>
</MainNav>
);
};
Expand Down