From 8e552ff9f7765d84313f4b66f6f535d5ed1f9f88 Mon Sep 17 00:00:00 2001 From: Simone Taeggi Date: Mon, 22 Apr 2024 19:10:48 +0200 Subject: [PATCH 01/18] feat(main-nav): replace DS NavLink with admin NavLink --- .../admin/admin/src/components/LeftMenu.tsx | 8 +- .../admin/src/components/MainNav/NavLink.tsx | 96 +++++++++++++++++++ .../components/MainNav/tests/NavLink.test.tsx | 25 +++++ .../core/admin/admin/src/translations/en.json | 1 + 4 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 packages/core/admin/admin/src/components/MainNav/NavLink.tsx create mode 100644 packages/core/admin/admin/src/components/MainNav/tests/NavLink.test.tsx diff --git a/packages/core/admin/admin/src/components/LeftMenu.tsx b/packages/core/admin/admin/src/components/LeftMenu.tsx index bbfd7400717..f1191a6c6b0 100644 --- a/packages/core/admin/admin/src/components/LeftMenu.tsx +++ b/packages/core/admin/admin/src/components/LeftMenu.tsx @@ -11,7 +11,7 @@ import { NavSections, NavUser, } from '@strapi/design-system/v2'; -import { Exit, Write, Lock } from '@strapi/icons'; +import { Exit, Write, Lock, House } from '@strapi/icons'; import { useIntl } from 'react-intl'; import { NavLink as RouterNavLink, useLocation } from 'react-router-dom'; import styled from 'styled-components'; @@ -24,6 +24,7 @@ import { usePersistentState } from '../hooks/usePersistentState'; import { getDisplayName } from '../utils/users'; import { NavBrand as NewNavBrand } from './MainNav/NavBrand'; +import { NavLink as NewNavLink } from './MainNav/NavLink'; const LinkUserWrapper = styled(Box)` width: ${150 / 16}rem; @@ -135,6 +136,11 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) = + {condensed && ( + handleClickOnLink('/')}> + {formatMessage({ id: 'global.home', defaultMessage: 'Home' })} + + )} ; + badgeAriaLabel?: string; + badgeContent?: string | number; +} + +const MainNavLinkWrapper = styled(RouterLink)` + text-decoration: none; + display: block; + border-radius: ${({ theme }) => theme.borderRadius}; + background: ${({ theme }) => theme.colors.neutral0}; + color: ${({ theme }) => theme.colors.neutral600}; + position: relative; + + svg path { + fill: ${({ theme }) => theme.colors.neutral500}; + } + + &:hover, + &.active { + background: ${({ theme }) => theme.colors.neutral100}; + } + + &:hover { + svg path { + fill: ${({ theme }) => theme.colors.neutral600}; + } + color: ${({ theme }) => theme.colors.neutral700}; + } + + &.active { + svg path { + fill: ${({ theme }) => theme.colors.primary600}; + } + + color: ${({ theme }) => theme.colors.primary600}; + font-weight: 500; + } +`; + +const CustomBadge = styled(Badge)` + span { + color: ${({ theme }) => theme.colors.neutral0}; + line-height: 0; + } + min-width: ${({ theme }) => theme.spaces[6]}; + height: ${({ theme }) => theme.spaces[5]}; + border-radius: ${({ theme }) => theme.spaces[10]}; + padding: ${({ theme }) => `0 ${theme.spaces[2]}`}; +`; + +export const NavLink = ({ + children, + icon, + badgeContent, + badgeAriaLabel, + ...props +}: NavLinkProps) => { + return ( + + + <> + + + + {badgeContent && ( + + {badgeContent} + + )} + + + + ); +}; diff --git a/packages/core/admin/admin/src/components/MainNav/tests/NavLink.test.tsx b/packages/core/admin/admin/src/components/MainNav/tests/NavLink.test.tsx new file mode 100644 index 00000000000..679078fc10f --- /dev/null +++ b/packages/core/admin/admin/src/components/MainNav/tests/NavLink.test.tsx @@ -0,0 +1,25 @@ +import { render, screen } from '@tests/utils'; + +import { NavLink } from '../NavLink'; + +describe('NavLink', () => { + it('shows the NavLink with link to destination', async () => { + render( + + test link + + ); + const link = screen.getByRole('link'); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', '/content-manager'); + }); + it('shows the badge next to the link', async () => { + render( + + test link + + ); + const badge = screen.getByText('5'); + expect(badge).toBeInTheDocument(); + }); +}); diff --git a/packages/core/admin/admin/src/translations/en.json b/packages/core/admin/admin/src/translations/en.json index f09420b1aad..1d1a2400751 100644 --- a/packages/core/admin/admin/src/translations/en.json +++ b/packages/core/admin/admin/src/translations/en.json @@ -685,6 +685,7 @@ "global.change-password": "Change password", "global.close": "Close", "global.content-manager": "Content Manager", + "global.home": "Home", "global.continue": "Continue", "global.delete": "Delete", "global.delete-target": "Delete {target}", From dfd0653557610c04b05841365dbfb8a457cf55ba Mon Sep 17 00:00:00 2001 From: Simone Taeggi Date: Mon, 22 Apr 2024 19:54:17 +0200 Subject: [PATCH 02/18] feat(main-nav): change icon type --- packages/core/admin/admin/src/components/MainNav/NavLink.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/admin/admin/src/components/MainNav/NavLink.tsx b/packages/core/admin/admin/src/components/MainNav/NavLink.tsx index e80669d057d..3276e98dbc7 100644 --- a/packages/core/admin/admin/src/components/MainNav/NavLink.tsx +++ b/packages/core/admin/admin/src/components/MainNav/NavLink.tsx @@ -6,8 +6,7 @@ import styled from 'styled-components'; export interface NavLinkProps extends LinkProps { children: React.ReactNode; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - icon?: string | React.ComponentType; + icon?: React.ElementType>; badgeAriaLabel?: string; badgeContent?: string | number; } From 5ac8e4ce299a05beb27f6fa9ce73d3204d1926ee Mon Sep 17 00:00:00 2001 From: Simone Taeggi Date: Tue, 23 Apr 2024 10:28:17 +0200 Subject: [PATCH 03/18] feat(main-nav): fix prettier errors --- .../05-type-system/02-concepts/01-schema.mdx | 19 +++--- .../05-type-system/02-concepts/02-uid.mdx | 18 +++--- .../02-concepts/03-public-registry.mdx | 64 +++++++++++-------- 3 files changed, 57 insertions(+), 44 deletions(-) diff --git a/docs/docs/guides/05-type-system/02-concepts/01-schema.mdx b/docs/docs/guides/05-type-system/02-concepts/01-schema.mdx index 115ad857cab..a5a76d78e8d 100644 --- a/docs/docs/guides/05-type-system/02-concepts/01-schema.mdx +++ b/docs/docs/guides/05-type-system/02-concepts/01-schema.mdx @@ -67,8 +67,8 @@ For instance, a string attribute will resolve to a primitive string in an entity ### Usage -import Tabs from '@theme/Tabs' -import TabItem from '@theme/TabItem' +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; @@ -89,22 +89,23 @@ declare const component: Schema.Component; declare function processAnySchema(schema: Schema.Schema): void; -processAnySchema(schema); // ✅ +processAnySchema(schema); // ✅ processAnySchema(contentType); // ✅ processAnySchema(component); // ✅ declare function processContentTypeSchema(schema: Schema.ContentType): void; -processContentTypeSchema(schema); // ✅ +processContentTypeSchema(schema); // ✅ processContentTypeSchema(contentType); // ✅ processContentTypeSchema(component); // ❌ Error, a component schema is not assignable to a content-type schema declare function processComponentSchema(schema: Schema.Component): void; -processComponentSchema(schema); // ✅ +processComponentSchema(schema); // ✅ processComponentSchema(contentType); // ❌ Error, a content-type schema is not assignable to a component schema processComponentSchema(component); // ✅ ``` + Schema definitions exported from the `Struct` namespace defines the low level type representation of Strapi schemas. @@ -112,6 +113,7 @@ Schema definitions exported from the `Struct` namespace defines the low level ty :::caution Those types can be useful when you want to validate other types against the base ones, but realistically, the public Schema types should almost always be preferred. ::: + ```typescript import type { Struct } from '@strapi/strapi'; @@ -121,21 +123,22 @@ declare const component: Struct.ComponentSchema; declare function processAnySchema(schema: Struct.Schema): void; -processAnySchema(schema); // ✅ +processAnySchema(schema); // ✅ processAnySchema(contentType); // ✅ processAnySchema(component); // ✅ declare function processContentTypeSchema(schema: Struct.ContentTypeSchema): void; -processContentTypeSchema(schema); // ✅ +processContentTypeSchema(schema); // ✅ processContentTypeSchema(contentType); // ✅ processContentTypeSchema(component); // ❌ Error, a component schema is not assignable to a content-type schema declare function processComponentSchema(schema: Struct.ComponentSchema): void; -processComponentSchema(schema); // ✅ +processComponentSchema(schema); // ✅ processComponentSchema(contentType); // ❌ Error, a content-type schema is not assignable to a component schema processComponentSchema(component); // ✅ ``` + diff --git a/docs/docs/guides/05-type-system/02-concepts/02-uid.mdx b/docs/docs/guides/05-type-system/02-concepts/02-uid.mdx index db2692423d8..6d26d1c1eea 100644 --- a/docs/docs/guides/05-type-system/02-concepts/02-uid.mdx +++ b/docs/docs/guides/05-type-system/02-concepts/02-uid.mdx @@ -14,7 +14,6 @@ On this page, **a resource** is considered as **anything that can be identified This includes (but is not limited to) controllers, schema, services, policies, middlewares, etc... ::: - In the Type System, UIDs play a crucial role in referencing various resources (such as schema and entities) by attaching a unique identifier. To put it simply, a UID is a unique (string) literal key used to identify, locate, or access a particular resource within the system. @@ -26,6 +25,7 @@ This makes it the perfect tool to index type registries or to use as a type para ### Format A UID is composed of 3 different parts: + 1. A namespace ([link](#1-namespaces)) 2. A separator ([link](#2-separators)) 3. A name ([link](#3-names)) @@ -46,7 +46,7 @@ Scoped namespaces are defined by a base name, followed by a separator (`::`) and In Strapi there are two of them: | Name | Definition | Description | -|--------|:-----------------:|------------------------------------------------------| +| ------ | :---------------: | ---------------------------------------------------- | | API | `api::` | Represent a resource present in the `` API | | Plugin | `plugin::` | Represent a resource present in the `` plugin | @@ -57,7 +57,7 @@ These namespaces are used as a simple prefix and define the origin of a resource Strapi uses three of them to create UIDs | Name | Definition | Description | -|--------|:----------:|-------------------------------------------------------------------------------| +| ------ | :--------: | ----------------------------------------------------------------------------- | | Strapi | `strapi` | Represent a resource present in the core of strapi | | Admin | `admin` | Represent a resource present in Strapi admin | | Global | `global` | Rarely used (_e.g. policies or middlewares_), it represents a global resource | @@ -90,7 +90,7 @@ ContentType and Component are referring to both the related schema and entity. ::: | | ContentType | Component | Middleware | Policy | Controller | Service | -|--------------------------|:------------------:|:------------------:|:------------------:|:------------------:|:------------------:|:------------------:| +| ------------------------ | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | | `api::.` | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | `plugin::.` | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | `.` | :x: | :white_check_mark: | :x: | :x: | :x: | :x: | @@ -141,6 +141,7 @@ fetch('api::article.article'); fetch('admin::user'); // ^ this should return a Data.Entity<'admin::user'> ``` + To do that, we'll need the function to be able to provide us with the current `uid` type based on usage. ```typescript @@ -171,14 +172,15 @@ Let's add the possibility to select which fields we want to return for our entit ```typescript import type { UID, Data, Schema } from '@strapi/types'; -declare function fetch< - T extends UID.ContentType, - F extends Schema.AttributeNames ->(uid: T, fields: F[]): Data.ContentType; +declare function fetch>( + uid: T, + fields: F[] +): Data.ContentType; ``` :::tip You may have noticed that we're using the inferred UID type (`T`) to reference both: + - An entity (`Data.Entity`) - A schema (`Schema.AttributeNames`) diff --git a/docs/docs/guides/05-type-system/02-concepts/03-public-registry.mdx b/docs/docs/guides/05-type-system/02-concepts/03-public-registry.mdx index 3ee780a4715..efc7e96be4b 100644 --- a/docs/docs/guides/05-type-system/02-concepts/03-public-registry.mdx +++ b/docs/docs/guides/05-type-system/02-concepts/03-public-registry.mdx @@ -9,8 +9,8 @@ tags: toc_max_heading_level: 5 --- -import Tabs from '@theme/Tabs' -import TabItem from '@theme/TabItem' +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; ### Context @@ -108,6 +108,7 @@ Creating a new registry is as simple as exporting an indexed interface from the Let's declare the content-type schema registry. It should accept: + - Content-type UIDs as keys - Content-type schemas as values @@ -130,7 +131,7 @@ We use low level types to define our index (`Internal`/`Struct`) to keep it as g To define `UID.ContentType`, we extract every key (`Internal.Registry.Keys`) from the public content-type registry (`Public.ContentTypeSchemas`) that matches with the base definition of a content-type UID (`Internal.UID.ContentType`). ```ts title="@strapi/types/uid.ts" -import type { Internal, Public } from '@strapi/types' +import type { Internal, Public } from '@strapi/types'; export type ContentType = Internal.Registry.Keys< Public.ContentTypeSchemas, @@ -151,7 +152,7 @@ Since `UID.ContentType` (`TUID`) is [dynamically built based on actual keys](#ui ::: ```ts title="@strapi/types/schema.ts" -import type { UID, Public } from '@strapi/types' +import type { UID, Public } from '@strapi/types'; export type ContentType = Public.ContentTypeSchemas[TUID]; ``` @@ -172,6 +173,7 @@ Remember to use dynamic type definitions (`UID`, `Data`, `Schema`) and not stati :::info[Reminder] Registries are **indexed**, which means that: + - **When augmented** (_e.g. in users' applications_), they'll return **strongly typed values** that correspond to the defined types. - **When empty** (_e.g. in Strapi codebase_), they'll return **generic low level types** based on their index definition. @@ -183,6 +185,7 @@ Registries are **indexed**, which means that: const uid: UID.ContentType; // ^ 'api::article.article' | 'admin::user' ``` + ```ts @@ -191,6 +194,7 @@ Registries are **indexed**, which means that: const uid: UID.ContentType; // ^ `admin::${string}` | `api::${string}.${string}` | `plugin::${string}.${string}` | `strapi::${string}` ``` + ::: @@ -215,7 +219,7 @@ declare module '@strapi/strapi' { } } } -```` +``` This will force every type that depends on the `Public.ContentTypeSchemas` registry to recognize `'api::article.article'` as the only valid UID and `ApiArticleArticle` the only valid schema. @@ -235,18 +239,20 @@ The process will generate type definitions based on the user application state ( Generate the types once. - ```shell title="my-app/" - yarn strapi ts:generate-types - ``` +```shell title="my-app/" +yarn strapi ts:generate-types +``` + Start the application in dev mode, and generate types on every server restart. Useful when working with the content-type builder. - ```shell title="my-app/" - yarn develop - ``` +```shell title="my-app/" +yarn develop +``` + @@ -277,7 +283,6 @@ declare module '@strapi/strapi' { When coupling everything together, the end result is a TypeScript developer experience automatically adjusted to the current context. - ```ts title="my-app/src/index.ts" @@ -292,25 +297,28 @@ When coupling everything together, the end result is a TypeScript developer expe strapi.findOne('api::blog.blog'); // ^ Error, TypeScript will complain } - }) - ``` - - - ```ts title="@strapi/strapi/document-service.ts" - import type { UID } from '@strapi/types'; - export const findOne(uid: TUID) { - // ... - } +}) + +```` + + +```ts title="@strapi/strapi/document-service.ts" +import type { UID } from '@strapi/types'; - findOne('admin::foo'); - // ^ Valid, matches 'admin::${string}' +export const findOne(uid: TUID) { + // ... +} + +findOne('admin::foo'); +// ^ Valid, matches 'admin::${string}' - findOne('plugin::bar.bar'); - // ^ Valid, matches 'plugin::${string}.${string}' +findOne('plugin::bar.bar'); +// ^ Valid, matches 'plugin::${string}.${string}' + +findOne('baz'); +// ^ Error, does not correspond to any content-type UID format +```` - findOne('baz'); - // ^ Error, does not correspond to any content-type UID format - ``` From b7827e304de803a7bfe18f2ec05ab7d24ecf888a Mon Sep 17 00:00:00 2001 From: Simone Taeggi Date: Tue, 23 Apr 2024 17:57:18 +0200 Subject: [PATCH 04/18] feat(main-nav): refactor navlink code and add more test cases --- .../admin/admin/src/components/LeftMenu.tsx | 15 +- .../admin/src/components/MainNav/NavLink.tsx | 135 +++++++++++------- .../components/MainNav/tests/NavLink.test.tsx | 42 ++++-- 3 files changed, 122 insertions(+), 70 deletions(-) diff --git a/packages/core/admin/admin/src/components/LeftMenu.tsx b/packages/core/admin/admin/src/components/LeftMenu.tsx index f1191a6c6b0..95fe8cee5e7 100644 --- a/packages/core/admin/admin/src/components/LeftMenu.tsx +++ b/packages/core/admin/admin/src/components/LeftMenu.tsx @@ -137,9 +137,18 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) = {condensed && ( - handleClickOnLink('/')}> - {formatMessage({ id: 'global.home', defaultMessage: 'Home' })} - + + + <> + + + + 5 + + + )} >; - badgeAriaLabel?: string; - badgeContent?: string | number; -} - +/* ------------------------------------------------------------------------------------------------- + * Link + * -----------------------------------------------------------------------------------------------*/ const MainNavLinkWrapper = styled(RouterLink)` text-decoration: none; display: block; @@ -19,10 +15,6 @@ const MainNavLinkWrapper = styled(RouterLink)` color: ${({ theme }) => theme.colors.neutral600}; position: relative; - svg path { - fill: ${({ theme }) => theme.colors.neutral500}; - } - &:hover, &.active { background: ${({ theme }) => theme.colors.neutral100}; @@ -45,51 +37,88 @@ const MainNavLinkWrapper = styled(RouterLink)` } `; +const LinkImpl = ({ children, ...props }: LinkProps) => { + return {children}; +}; + +/* ------------------------------------------------------------------------------------------------- + * Tooltip + * -----------------------------------------------------------------------------------------------*/ +const TooltipImpl = ({ children, label, position = 'right' }: NavLink.TooltipProps) => { + return ( + + {children} + + ); +}; + +/* ------------------------------------------------------------------------------------------------- + * Icon + * -----------------------------------------------------------------------------------------------*/ +const IconImpl = ({ children }: { children: React.ReactNode }) => { + return ( + + {children} + + ); +}; + +/* ------------------------------------------------------------------------------------------------- + * Badge + * -----------------------------------------------------------------------------------------------*/ const CustomBadge = styled(Badge)` - span { - color: ${({ theme }) => theme.colors.neutral0}; - line-height: 0; - } - min-width: ${({ theme }) => theme.spaces[6]}; - height: ${({ theme }) => theme.spaces[5]}; + /* override default badge styles to change the border radius of the Base element in the Design System */ border-radius: ${({ theme }) => theme.spaces[10]}; - padding: ${({ theme }) => `0 ${theme.spaces[2]}`}; `; -export const NavLink = ({ - children, - icon, - badgeContent, - badgeAriaLabel, - ...props -}: NavLinkProps) => { +const BadgeImpl = ({ children, label }: NavLink.BadgeProps) => { + if (!children) { + return null; + } return ( - - - <> - - - - {badgeContent && ( - - {badgeContent} - - )} - - - + + {children} + ); }; + +/* ------------------------------------------------------------------------------------------------- + * EXPORTS + * -----------------------------------------------------------------------------------------------*/ + +const NavLink = { + Link: LinkImpl, + Tooltip: TooltipImpl, + Icon: IconImpl, + Badge: BadgeImpl, +}; + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace NavLink { + export interface BadgeProps { + children: React.ReactNode; + label: string; + } + + export interface TooltipProps { + position?: 'top' | 'bottom' | 'left' | 'right'; + label: string; + children: React.ReactNode; + } +} + +export { NavLink }; diff --git a/packages/core/admin/admin/src/components/MainNav/tests/NavLink.test.tsx b/packages/core/admin/admin/src/components/MainNav/tests/NavLink.test.tsx index 679078fc10f..a9b09b040f8 100644 --- a/packages/core/admin/admin/src/components/MainNav/tests/NavLink.test.tsx +++ b/packages/core/admin/admin/src/components/MainNav/tests/NavLink.test.tsx @@ -1,25 +1,39 @@ -import { render, screen } from '@tests/utils'; +import { Icon } from '@strapi/design-system'; +import { House, Lock } from '@strapi/icons'; +import { screen, render as renderRTL } from '@tests/utils'; import { NavLink } from '../NavLink'; describe('NavLink', () => { + const Component = () => ( + + + <> + + + + + + + + + + ); + + const render = () => renderRTL(); + it('shows the NavLink with link to destination', async () => { - render( - - test link - - ); + render(); const link = screen.getByRole('link'); expect(link).toBeInTheDocument(); - expect(link).toHaveAttribute('href', '/content-manager'); + expect(link).toHaveAttribute('href', '/test-link'); + }); + it('shows the home icon in the link', async () => { + render(); + expect(screen.getByTestId('nav-link-icon')).toBeInTheDocument(); }); it('shows the badge next to the link', async () => { - render( - - test link - - ); - const badge = screen.getByText('5'); - expect(badge).toBeInTheDocument(); + render(); + expect(screen.getByTestId('nav-link-badge')).toBeInTheDocument(); }); }); From 6f68642f2c98f9f30c820964568f9a5d82eb21e1 Mon Sep 17 00:00:00 2001 From: Simone Taeggi Date: Wed, 24 Apr 2024 10:30:15 +0200 Subject: [PATCH 05/18] feat(main-nav): minor fixes --- packages/core/admin/admin/src/components/LeftMenu.tsx | 11 ++++------- .../admin/admin/src/components/MainNav/NavLink.tsx | 5 +++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/core/admin/admin/src/components/LeftMenu.tsx b/packages/core/admin/admin/src/components/LeftMenu.tsx index 95fe8cee5e7..fe5262b1701 100644 --- a/packages/core/admin/admin/src/components/LeftMenu.tsx +++ b/packages/core/admin/admin/src/components/LeftMenu.tsx @@ -137,16 +137,13 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) = {condensed && ( - + handleClickOnLink('/')}> - <> - - - - 5 - + + + )} diff --git a/packages/core/admin/admin/src/components/MainNav/NavLink.tsx b/packages/core/admin/admin/src/components/MainNav/NavLink.tsx index bc4a3796999..f0ebe984684 100644 --- a/packages/core/admin/admin/src/components/MainNav/NavLink.tsx +++ b/packages/core/admin/admin/src/components/MainNav/NavLink.tsx @@ -47,7 +47,7 @@ const LinkImpl = ({ children, ...props }: LinkProps) => { const TooltipImpl = ({ children, label, position = 'right' }: NavLink.TooltipProps) => { return ( - {children} + {children} ); }; @@ -64,6 +64,7 @@ const IconImpl = ({ children }: { children: React.ReactNode }) => { paddingRight={`${12 / 16}rem`} justifyContent="center" aria-hidden + as="span" > {children} @@ -116,7 +117,7 @@ namespace NavLink { export interface TooltipProps { position?: 'top' | 'bottom' | 'left' | 'right'; - label: string; + label?: string; children: React.ReactNode; } } From e50c9f45d8931a6770067babc5c985d86f49ea1d Mon Sep 17 00:00:00 2001 From: Simone Taeggi Date: Wed, 24 Apr 2024 15:33:29 +0200 Subject: [PATCH 06/18] feat(main-nav): fix ui errors --- packages/core/admin/admin/src/components/LeftMenu.tsx | 7 ++++++- .../core/admin/admin/src/components/MainNav/NavLink.tsx | 9 +++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/core/admin/admin/src/components/LeftMenu.tsx b/packages/core/admin/admin/src/components/LeftMenu.tsx index fe5262b1701..546b648b2f7 100644 --- a/packages/core/admin/admin/src/components/LeftMenu.tsx +++ b/packages/core/admin/admin/src/components/LeftMenu.tsx @@ -142,7 +142,12 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) = label={formatMessage({ id: 'global.home', defaultMessage: 'Home' })} > - + diff --git a/packages/core/admin/admin/src/components/MainNav/NavLink.tsx b/packages/core/admin/admin/src/components/MainNav/NavLink.tsx index f0ebe984684..781f26ba279 100644 --- a/packages/core/admin/admin/src/components/MainNav/NavLink.tsx +++ b/packages/core/admin/admin/src/components/MainNav/NavLink.tsx @@ -56,12 +56,17 @@ const TooltipImpl = ({ children, label, position = 'right' }: NavLink.TooltipPro * Icon * -----------------------------------------------------------------------------------------------*/ const IconImpl = ({ children }: { children: React.ReactNode }) => { + if (!children) { + return null; + } return ( { return ( Date: Fri, 26 Apr 2024 12:40:01 +0200 Subject: [PATCH 07/18] feat(main-nav): fix merge issues --- .../core/admin/admin/src/components/LeftMenu.tsx | 7 +------ .../admin/src/components/MainNav/NavBrand.tsx | 8 ++++---- .../admin/src/components/MainNav/NavLink.tsx | 16 ++++------------ 3 files changed, 9 insertions(+), 22 deletions(-) diff --git a/packages/core/admin/admin/src/components/LeftMenu.tsx b/packages/core/admin/admin/src/components/LeftMenu.tsx index f5ba501ce09..6e61da6e89b 100644 --- a/packages/core/admin/admin/src/components/LeftMenu.tsx +++ b/packages/core/admin/admin/src/components/LeftMenu.tsx @@ -144,12 +144,7 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) = label={formatMessage({ id: 'global.home', defaultMessage: 'Home' })} > - + diff --git a/packages/core/admin/admin/src/components/MainNav/NavBrand.tsx b/packages/core/admin/admin/src/components/MainNav/NavBrand.tsx index bb3b59995f8..eccf617d2d7 100644 --- a/packages/core/admin/admin/src/components/MainNav/NavBrand.tsx +++ b/packages/core/admin/admin/src/components/MainNav/NavBrand.tsx @@ -9,9 +9,9 @@ const BrandIconWrapper = styled(Box)` img { border-radius: ${({ theme }) => theme.borderRadius}; object-fit: contain; - height: ${24 / 16}rem; - width: ${24 / 16}rem; - margin: ${3 / 16}rem; + height: 2.4rem; + width: 2.4rem; + margin: 0.4rem; } `; @@ -22,7 +22,7 @@ export const NavBrand = () => { } = useConfiguration('LeftMenu'); return ( - + {formatMessage({ { const TooltipImpl = ({ children, label, position = 'right' }: NavLink.TooltipProps) => { return ( - {children} + + {children} + ); }; @@ -60,17 +62,7 @@ const IconImpl = ({ children }: { children: React.ReactNode }) => { return null; } return ( - + {children} ); From b17f8d7664f39ee2f74300e7326c8da37e37be68 Mon Sep 17 00:00:00 2001 From: Simone Taeggi Date: Fri, 26 Apr 2024 12:57:13 +0200 Subject: [PATCH 08/18] feat(main-nav): fix unit test and types --- packages/core/admin/admin/src/components/MainNav/NavLink.tsx | 5 ++--- .../admin/src/components/MainNav/tests/NavLink.test.tsx | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/core/admin/admin/src/components/MainNav/NavLink.tsx b/packages/core/admin/admin/src/components/MainNav/NavLink.tsx index 42e3cb7565a..33b794e6082 100644 --- a/packages/core/admin/admin/src/components/MainNav/NavLink.tsx +++ b/packages/core/admin/admin/src/components/MainNav/NavLink.tsx @@ -76,7 +76,7 @@ const CustomBadge = styled(Badge)` border-radius: ${({ theme }) => theme.spaces[10]}; `; -const BadgeImpl = ({ children, label }: NavLink.BadgeProps) => { +const BadgeImpl = ({ children, label, ...props }: NavLink.BadgeProps) => { if (!children) { return null; } @@ -86,8 +86,7 @@ const BadgeImpl = ({ children, label }: NavLink.BadgeProps) => { top={`-${12 / 16}rem`} right={`-${4 / 16}rem`} aria-label={label} - background="primary600" - textColor="neutral0" + {...props} > {children} diff --git a/packages/core/admin/admin/src/components/MainNav/tests/NavLink.test.tsx b/packages/core/admin/admin/src/components/MainNav/tests/NavLink.test.tsx index a9b09b040f8..f5e02abcf3c 100644 --- a/packages/core/admin/admin/src/components/MainNav/tests/NavLink.test.tsx +++ b/packages/core/admin/admin/src/components/MainNav/tests/NavLink.test.tsx @@ -1,4 +1,3 @@ -import { Icon } from '@strapi/design-system'; import { House, Lock } from '@strapi/icons'; import { screen, render as renderRTL } from '@tests/utils'; @@ -10,10 +9,10 @@ describe('NavLink', () => { <> - + - + From dd8daaaf78a028651725e804d27749ec91b0caa2 Mon Sep 17 00:00:00 2001 From: Simone Taeggi Date: Thu, 2 May 2024 11:50:25 +0200 Subject: [PATCH 09/18] feat(main-nav): implement the new main nav ui --- .../admin/admin/src/components/LeftMenu.tsx | 295 +++++++----------- .../admin/src/components/MainNav/MainNav.tsx | 23 ++ .../admin/src/components/MainNav/NavLink.tsx | 26 +- .../admin/src/components/MainNav/NavUser.tsx | 25 ++ 4 files changed, 179 insertions(+), 190 deletions(-) create mode 100644 packages/core/admin/admin/src/components/MainNav/MainNav.tsx create mode 100644 packages/core/admin/admin/src/components/MainNav/NavUser.tsx diff --git a/packages/core/admin/admin/src/components/LeftMenu.tsx b/packages/core/admin/admin/src/components/LeftMenu.tsx index 6e61da6e89b..ac2e8b97a72 100644 --- a/packages/core/admin/admin/src/components/LeftMenu.tsx +++ b/packages/core/admin/admin/src/components/LeftMenu.tsx @@ -1,34 +1,20 @@ import * as React from 'react'; -import { - Box, - Divider, - Flex, - FocusTrap, - Typography, - MainNav, - NavBrand, - NavCondense, - NavFooter, - NavLink, - NavSection, - NavSections, - NavUser, -} from '@strapi/design-system'; +import { Box, Divider, Flex, FocusTrap, Typography } from '@strapi/design-system'; import { SignOut, Feather, Lock, House } from '@strapi/icons'; import { useIntl } from 'react-intl'; import { NavLink as RouterNavLink, 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; @@ -56,22 +42,21 @@ const LinkUser = styled(RouterNavLink)<{ logout?: boolean }>` } `; -const NavLinkWrapper = styled(Box)` - div:nth-child(2) { - /* remove badge background color */ - background: transparent; +const NewNavLinkBadge = styled(NavLink.Badge)` + span { + color: ${({ theme }) => theme.colors.neutral0}; } `; +const NavListWrapper = styled(Flex)` + overflow-y: auto; +`; + interface LeftMenuProps extends Pick {} const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) => { - const navUserRef = React.useRef(null!); + const navUserRef = React.useRef(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(); @@ -87,7 +72,8 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) = const handleToggleUserLinks = () => setUserLinksVisible((prev) => !prev); - const handleBlur: React.FocusEventHandler = (e) => { + const handleBlur: React.FocusEventHandler = (e) => { + e.preventDefault(); if ( !e.currentTarget.contains(e.relatedTarget) && /** @@ -103,180 +89,121 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) = trackUsage('willNavigate', { from: pathname, to: destination }); }; - const menuTitle = formatMessage({ - id: 'app.components.LeftMenu.navbrand.title', - defaultMessage: 'Strapi Dashboard', - }); - return ( - - {condensed ? ( - /** - * TODO: remove the conditional rendering once the new Main nav is fully implemented - */ - - ) : ( - - } - /> - )} + + - - {condensed && ( - handleClickOnLink('/')}> - - - - - - - )} - } - onClick={() => handleClickOnLink('/content-manager')} - > - {formatMessage({ id: 'global.content-manager', defaultMessage: 'Content manager' })} - - - {pluginsSectionLinks.length > 0 ? ( - + handleClickOnLink('/')}> + + + + + + + handleClickOnLink('/content-manager')}> + - {pluginsSectionLinks.map((link) => { + + + + + + {pluginsSectionLinks.length > 0 + ? pluginsSectionLinks.map((link) => { if (link.to === 'content-manager') { return null; } const LinkIcon = link.icon; + const badgeContent = link?.lockIcon ? : undefined; return ( - - } - onClick={() => handleClickOnLink(link.to)} - // @ts-expect-error: badgeContent in the DS accept only strings - badgeContent={ - link?.lockIcon ? : undefined - } - > - {formatMessage(link.intlLabel)} - - + handleClickOnLink(link.to)}> + + + + + {badgeContent && ( + + {badgeContent} + + )} + + ); - })} - - ) : null} - - {generalSectionLinks.length > 0 ? ( - - {generalSectionLinks.map((link) => { + }) + : null} + {generalSectionLinks.length > 0 + ? generalSectionLinks.map((link) => { const LinkIcon = link.icon; + const badgeContent = + link.notificationsCount && link.notificationsCount > 0 + ? link.notificationsCount.toString() + : undefined; + return ( - 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={} - onClick={() => handleClickOnLink(link.to)} - > - {formatMessage(link.intlLabel)} - + handleClickOnLink(link.to)}> + + + + + {badgeContent && ( + + {badgeContent} + + )} + + ); - })} - - ) : null} - - - - + + {userDisplayName} + + {userLinksVisible && ( + - {userDisplayName} - - {userLinksVisible && ( - - - - - - {formatMessage({ - id: 'global.profile', - defaultMessage: 'Profile', - })} - - - - - {formatMessage({ - id: 'app.components.LeftMenu.logout', - defaultMessage: 'Logout', - })} - - - - - - - )} - - setCondensed((s) => !s)}> - {condensed - ? formatMessage({ - id: 'app.components.LeftMenu.expand', - defaultMessage: 'Expand the navbar', - }) - : formatMessage({ - id: 'app.components.LeftMenu.collapse', - defaultMessage: 'Collapse the navbar', - })} - - + + + + + {formatMessage({ + id: 'global.profile', + defaultMessage: 'Profile', + })} + + + + + {formatMessage({ + id: 'app.components.LeftMenu.logout', + defaultMessage: 'Logout', + })} + + + + + + + )} ); }; diff --git a/packages/core/admin/admin/src/components/MainNav/MainNav.tsx b/packages/core/admin/admin/src/components/MainNav/MainNav.tsx new file mode 100644 index 00000000000..86275905b2c --- /dev/null +++ b/packages/core/admin/admin/src/components/MainNav/MainNav.tsx @@ -0,0 +1,23 @@ +import { Flex, FlexProps } from '@strapi/design-system'; +import styled from 'styled-components'; + +const MainNavWrapper = styled(Flex)` + border-right: 1px solid ${({ theme }) => theme.colors.neutral150}; +`; + +const MainNav = (props: FlexProps<'nav'>) => ( + +); + +export { MainNav }; diff --git a/packages/core/admin/admin/src/components/MainNav/NavLink.tsx b/packages/core/admin/admin/src/components/MainNav/NavLink.tsx index 3c3d19f471a..3a3a8fb8c97 100644 --- a/packages/core/admin/admin/src/components/MainNav/NavLink.tsx +++ b/packages/core/admin/admin/src/components/MainNav/NavLink.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { Tooltip, Flex, Badge } from '@strapi/design-system'; +import { Tooltip, Flex, Badge, BadgeProps } from '@strapi/design-system'; import { NavLink as RouterLink, LinkProps } from 'react-router-dom'; import styled from 'styled-components'; @@ -12,8 +12,9 @@ const MainNavLinkWrapper = styled(RouterLink)` display: block; border-radius: ${({ theme }) => theme.borderRadius}; background: ${({ theme }) => theme.colors.neutral0}; - color: ${({ theme }) => theme.colors.neutral600}; + color: ${({ theme }) => theme.colors.neutral500}; position: relative; + width: fit-content; &:hover, &.active { @@ -38,7 +39,11 @@ const MainNavLinkWrapper = styled(RouterLink)` `; const LinkImpl = ({ children, ...props }: LinkProps) => { - return {children}; + return ( + + {children} + + ); }; /* ------------------------------------------------------------------------------------------------- @@ -74,14 +79,23 @@ const IconImpl = ({ children }: { children: React.ReactNode }) => { const CustomBadge = styled(Badge)` /* override default badge styles to change the border radius of the Base element in the Design System */ border-radius: ${({ theme }) => theme.spaces[10]}; + height: 2rem; `; -const BadgeImpl = ({ children, label, ...props }: NavLink.BadgeProps) => { +const BadgeImpl = ({ children, label, ...props }: NavLink.NavBadgeProps) => { if (!children) { return null; } return ( - + {children} ); @@ -100,7 +114,7 @@ const NavLink = { // eslint-disable-next-line @typescript-eslint/no-namespace namespace NavLink { - export interface BadgeProps { + export interface NavBadgeProps extends BadgeProps { children: React.ReactNode; label: string; } diff --git a/packages/core/admin/admin/src/components/MainNav/NavUser.tsx b/packages/core/admin/admin/src/components/MainNav/NavUser.tsx new file mode 100644 index 00000000000..553f16c3da7 --- /dev/null +++ b/packages/core/admin/admin/src/components/MainNav/NavUser.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +import { Initials, Flex, ButtonProps, VisuallyHidden } from '@strapi/design-system'; + +export interface NavUserProps extends ButtonProps { + id: string; + initials: string; + children?: React.ReactNode; + onClick?: React.MouseEventHandler; +} + +export const NavUser = React.forwardRef( + ({ children, initials, ...props }, ref) => { + return ( + + + {initials} + + {children} + + + + ); + } +); From 346af1dfdaf97bc73413fb79df0ae11241636c0c Mon Sep 17 00:00:00 2001 From: Simone Taeggi Date: Thu, 2 May 2024 13:54:56 +0200 Subject: [PATCH 10/18] feat(main-nav): change on blur handler --- packages/core/admin/admin/src/components/LeftMenu.tsx | 7 ++----- .../core/admin/admin/src/components/MainNav/NavUser.tsx | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/core/admin/admin/src/components/LeftMenu.tsx b/packages/core/admin/admin/src/components/LeftMenu.tsx index ac2e8b97a72..679bb543936 100644 --- a/packages/core/admin/admin/src/components/LeftMenu.tsx +++ b/packages/core/admin/admin/src/components/LeftMenu.tsx @@ -55,7 +55,7 @@ const NavListWrapper = styled(Flex)` interface LeftMenuProps extends Pick {} const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) => { - const navUserRef = React.useRef(null!); + const navUserRef = React.useRef(null!); const [userLinksVisible, setUserLinksVisible] = React.useState(false); const user = useAuth('AuthenticatedApp', (state) => state.user); const { formatMessage } = useIntl(); @@ -76,10 +76,7 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) = e.preventDefault(); if ( !e.currentTarget.contains(e.relatedTarget) && - /** - * TODO: can we replace this by just using the navUserRef? - */ - e.relatedTarget?.parentElement?.id !== 'main-nav-user-button' + e.relatedTarget?.parentElement?.id !== navUserRef.current.id ) { setUserLinksVisible(false); } diff --git a/packages/core/admin/admin/src/components/MainNav/NavUser.tsx b/packages/core/admin/admin/src/components/MainNav/NavUser.tsx index 553f16c3da7..0253ef02baf 100644 --- a/packages/core/admin/admin/src/components/MainNav/NavUser.tsx +++ b/packages/core/admin/admin/src/components/MainNav/NavUser.tsx @@ -12,8 +12,8 @@ export interface NavUserProps extends ButtonProps { export const NavUser = React.forwardRef( ({ children, initials, ...props }, ref) => { return ( - - + + {initials} {children} From b7e36055212a2205e4b7bde640418bdf6cbd92f2 Mon Sep 17 00:00:00 2001 From: Simone Taeggi Date: Thu, 2 May 2024 15:31:38 +0200 Subject: [PATCH 11/18] feat(main-nav): fix TS error --- packages/core/admin/admin/src/components/MainNav/NavUser.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/admin/admin/src/components/MainNav/NavUser.tsx b/packages/core/admin/admin/src/components/MainNav/NavUser.tsx index 0253ef02baf..a135e0a8250 100644 --- a/packages/core/admin/admin/src/components/MainNav/NavUser.tsx +++ b/packages/core/admin/admin/src/components/MainNav/NavUser.tsx @@ -9,7 +9,7 @@ export interface NavUserProps extends ButtonProps { onClick?: React.MouseEventHandler; } -export const NavUser = React.forwardRef( +export const NavUser = React.forwardRef( ({ children, initials, ...props }, ref) => { return ( From a65a85fdea97faae8679d3ffc5f9d79af61abd26 Mon Sep 17 00:00:00 2001 From: Simone Taeggi Date: Thu, 2 May 2024 17:44:21 +0200 Subject: [PATCH 12/18] feat(main-nav): refactor navUser using the Menu component --- .../admin/admin/src/components/LeftMenu.tsx | 185 ++++++------------ .../admin/src/components/MainNav/NavLink.tsx | 6 +- .../admin/src/components/MainNav/NavUser.tsx | 85 +++++++- 3 files changed, 135 insertions(+), 141 deletions(-) diff --git a/packages/core/admin/admin/src/components/LeftMenu.tsx b/packages/core/admin/admin/src/components/LeftMenu.tsx index 679bb543936..4469975cc5b 100644 --- a/packages/core/admin/admin/src/components/LeftMenu.tsx +++ b/packages/core/admin/admin/src/components/LeftMenu.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; -import { Box, Divider, Flex, FocusTrap, Typography } 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'; @@ -16,32 +16,6 @@ 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}; @@ -55,13 +29,10 @@ const NavListWrapper = styled(Flex)` interface LeftMenuProps extends Pick {} const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) => { - const navUserRef = React.useRef(null!); - const [userLinksVisible, setUserLinksVisible] = React.useState(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 @@ -70,18 +41,6 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) = .join('') .substring(0, 2); - const handleToggleUserLinks = () => setUserLinksVisible((prev) => !prev); - - const handleBlur: React.FocusEventHandler = (e) => { - e.preventDefault(); - if ( - !e.currentTarget.contains(e.relatedTarget) && - e.relatedTarget?.parentElement?.id !== navUserRef.current.id - ) { - setUserLinksVisible(false); - } - }; - const handleClickOnLink = (destination: string) => { trackUsage('willNavigate', { from: pathname, to: destination }); }; @@ -93,25 +52,29 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) = - handleClickOnLink('/')}> - - - - - - - handleClickOnLink('/content-manager')}> - - - - - - + + handleClickOnLink('/')}> + + + + + + + + + handleClickOnLink('/content-manager')}> + + + + + + + {pluginsSectionLinks.length > 0 ? pluginsSectionLinks.map((link) => { if (link.to === 'content-manager') { @@ -121,18 +84,24 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) = const LinkIcon = link.icon; const badgeContent = link?.lockIcon ? : undefined; return ( - handleClickOnLink(link.to)}> - - - - - {badgeContent && ( - - {badgeContent} - - )} - - + + handleClickOnLink(link.to)}> + + + + + {badgeContent && ( + + {badgeContent} + + )} + + + ); }) : null} @@ -146,61 +115,25 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) = : undefined; return ( - handleClickOnLink(link.to)}> - - - - - {badgeContent && ( - - {badgeContent} - - )} - - + + handleClickOnLink(link.to)}> + + + + + {badgeContent && ( + + {badgeContent} + + )} + + + ); }) : null} - - {userDisplayName} - - {userLinksVisible && ( - - - - - - {formatMessage({ - id: 'global.profile', - defaultMessage: 'Profile', - })} - - - - - {formatMessage({ - id: 'app.components.LeftMenu.logout', - defaultMessage: 'Logout', - })} - - - - - - - )} + {userDisplayName} ); }; diff --git a/packages/core/admin/admin/src/components/MainNav/NavLink.tsx b/packages/core/admin/admin/src/components/MainNav/NavLink.tsx index 3a3a8fb8c97..61127897421 100644 --- a/packages/core/admin/admin/src/components/MainNav/NavLink.tsx +++ b/packages/core/admin/admin/src/components/MainNav/NavLink.tsx @@ -39,11 +39,7 @@ const MainNavLinkWrapper = styled(RouterLink)` `; const LinkImpl = ({ children, ...props }: LinkProps) => { - return ( - - {children} - - ); + return {children}; }; /* ------------------------------------------------------------------------------------------------- diff --git a/packages/core/admin/admin/src/components/MainNav/NavUser.tsx b/packages/core/admin/admin/src/components/MainNav/NavUser.tsx index a135e0a8250..1adff67208b 100644 --- a/packages/core/admin/admin/src/components/MainNav/NavUser.tsx +++ b/packages/core/admin/admin/src/components/MainNav/NavUser.tsx @@ -1,24 +1,89 @@ import React from 'react'; -import { Initials, Flex, ButtonProps, VisuallyHidden } from '@strapi/design-system'; +import { + Initials, + Flex, + Menu, + ButtonProps, + VisuallyHidden, + Typography, +} from '@strapi/design-system'; +import { SignOut } from '@strapi/icons'; +import { useIntl } from 'react-intl'; +import { NavLink as RouterNavLink } from 'react-router-dom'; +import styled from 'styled-components'; + +import { useAuth } from '../../features/Auth'; export interface NavUserProps extends ButtonProps { - id: string; initials: string; children?: React.ReactNode; - onClick?: React.MouseEventHandler; } +/** + * TODO: this needs to be solved in the Design-System + */ +const MenuTrigger = styled(Menu.Trigger)` + height: 100%; +`; + +const MenuContent = styled(Menu.Content)` + left: ${({ theme }) => theme.spaces[5]}; +`; + +const LinkUser = styled(RouterNavLink)<{ logout?: boolean }>` + display: flex; + width: 100%; + 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}; + width: 1.6rem; + height: 1.6rem; + } +`; + export const NavUser = React.forwardRef( ({ children, initials, ...props }, ref) => { + const { formatMessage } = useIntl(); + const logout = useAuth('Logout', (state) => state.logout); return ( - - - {initials} - - {children} - - + + + + {initials} + {children} + + + + + {formatMessage({ + id: 'global.profile', + defaultMessage: 'Profile', + })} + + + + + {formatMessage({ + id: 'app.components.LeftMenu.logout', + defaultMessage: 'Logout', + })} + + + + + ); } From 0e21260615ea232f10c581abde49262bf033fae8 Mon Sep 17 00:00:00 2001 From: Simone Taeggi Date: Fri, 3 May 2024 10:21:43 +0200 Subject: [PATCH 13/18] feat(main-nav): add aria label to the links --- .../admin/admin/src/components/LeftMenu.tsx | 37 ++++++++-- .../admin/src/components/MainNav/NavUser.tsx | 68 +++++++++---------- 2 files changed, 63 insertions(+), 42 deletions(-) diff --git a/packages/core/admin/admin/src/components/LeftMenu.tsx b/packages/core/admin/admin/src/components/LeftMenu.tsx index 4469975cc5b..5242a398304 100644 --- a/packages/core/admin/admin/src/components/LeftMenu.tsx +++ b/packages/core/admin/admin/src/components/LeftMenu.tsx @@ -53,7 +53,11 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) = - handleClickOnLink('/')}> + handleClickOnLink('/')} + aria-label={formatMessage({ id: 'global.home', defaultMessage: 'Home' })} + > @@ -62,11 +66,18 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) = - handleClickOnLink('/content-manager')}> + handleClickOnLink('/content-manager')} + aria-label={formatMessage({ + id: 'global.content-manager', + defaultMessage: 'Content Manager', + })} + > @@ -83,10 +94,16 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) = const LinkIcon = link.icon; const badgeContent = link?.lockIcon ? : undefined; + + const labelValue = formatMessage(link.intlLabel); return ( - handleClickOnLink(link.to)}> - + handleClickOnLink(link.to)} + aria-label={labelValue} + > + @@ -114,10 +131,16 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) = ? link.notificationsCount.toString() : undefined; + const labelValue = formatMessage(link.intlLabel); + return ( - handleClickOnLink(link.to)}> - + handleClickOnLink(link.to)} + > + diff --git a/packages/core/admin/admin/src/components/MainNav/NavUser.tsx b/packages/core/admin/admin/src/components/MainNav/NavUser.tsx index 1adff67208b..00c34bebff5 100644 --- a/packages/core/admin/admin/src/components/MainNav/NavUser.tsx +++ b/packages/core/admin/admin/src/components/MainNav/NavUser.tsx @@ -53,38 +53,36 @@ const LinkUser = styled(RouterNavLink)<{ logout?: boolean }>` } `; -export const NavUser = React.forwardRef( - ({ children, initials, ...props }, ref) => { - const { formatMessage } = useIntl(); - const logout = useAuth('Logout', (state) => state.logout); - return ( - - - - {initials} - {children} - - - - - {formatMessage({ - id: 'global.profile', - defaultMessage: 'Profile', - })} - - - - - {formatMessage({ - id: 'app.components.LeftMenu.logout', - defaultMessage: 'Logout', - })} - - - - - - - ); - } -); +export const NavUser = ({ children, initials, ...props }: NavUserProps) => { + const { formatMessage } = useIntl(); + const logout = useAuth('Logout', (state) => state.logout); + return ( + + + + {initials} + {children} + + + + + {formatMessage({ + id: 'global.profile', + defaultMessage: 'Profile', + })} + + + + + {formatMessage({ + id: 'app.components.LeftMenu.logout', + defaultMessage: 'Logout', + })} + + + + + + + ); +}; From 995954b07f9b144218b79378b4594b5d791cd79c Mon Sep 17 00:00:00 2001 From: Simone Taeggi Date: Fri, 3 May 2024 10:48:47 +0200 Subject: [PATCH 14/18] feat(main-nav): add menu item in the nav user links --- .../admin/src/components/MainNav/NavUser.tsx | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/packages/core/admin/admin/src/components/MainNav/NavUser.tsx b/packages/core/admin/admin/src/components/MainNav/NavUser.tsx index 00c34bebff5..5e5bc1c98d4 100644 --- a/packages/core/admin/admin/src/components/MainNav/NavUser.tsx +++ b/packages/core/admin/admin/src/components/MainNav/NavUser.tsx @@ -64,23 +64,27 @@ export const NavUser = ({ children, initials, ...props }: NavUserProps) => { {children} - - - {formatMessage({ - id: 'global.profile', - defaultMessage: 'Profile', - })} - - - - - {formatMessage({ - id: 'app.components.LeftMenu.logout', - defaultMessage: 'Logout', - })} - - - + + + + {formatMessage({ + id: 'global.profile', + defaultMessage: 'Profile', + })} + + + + + + + {formatMessage({ + id: 'app.components.LeftMenu.logout', + defaultMessage: 'Logout', + })} + + + + From f897ead6455f6274e2c78ab361f581c9b9a9bbc6 Mon Sep 17 00:00:00 2001 From: Simone Taeggi Date: Fri, 3 May 2024 11:27:20 +0200 Subject: [PATCH 15/18] feat(main-nav): refactor nav user and the menu items --- .../admin/src/components/MainNav/NavUser.tsx | 78 +++++++------------ 1 file changed, 29 insertions(+), 49 deletions(-) diff --git a/packages/core/admin/admin/src/components/MainNav/NavUser.tsx b/packages/core/admin/admin/src/components/MainNav/NavUser.tsx index 5e5bc1c98d4..3220b2cb8ee 100644 --- a/packages/core/admin/admin/src/components/MainNav/NavUser.tsx +++ b/packages/core/admin/admin/src/components/MainNav/NavUser.tsx @@ -1,16 +1,9 @@ import React from 'react'; -import { - Initials, - Flex, - Menu, - ButtonProps, - VisuallyHidden, - Typography, -} from '@strapi/design-system'; +import { Initials, Flex, Menu, ButtonProps, VisuallyHidden } from '@strapi/design-system'; import { SignOut } from '@strapi/icons'; import { useIntl } from 'react-intl'; -import { NavLink as RouterNavLink } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; import { useAuth } from '../../features/Auth'; @@ -31,31 +24,25 @@ const MenuContent = styled(Menu.Content)` left: ${({ theme }) => theme.spaces[5]}; `; -const LinkUser = styled(RouterNavLink)<{ logout?: boolean }>` - display: flex; - width: 100%; - 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}; - width: 1.6rem; - height: 1.6rem; +const MenuItem = styled(Menu.Item)` + span { + width: 100%; + display: flex; + justify-content: space-between; } `; export const NavUser = ({ children, initials, ...props }: NavUserProps) => { const { formatMessage } = useIntl(); + const navigate = useNavigate(); const logout = useAuth('Logout', (state) => state.logout); + const handleProfile = () => { + navigate('/me'); + }; + const handleLogout = () => { + logout(); + navigate('/auth/login'); + }; return ( @@ -64,27 +51,20 @@ export const NavUser = ({ children, initials, ...props }: NavUserProps) => { {children} - - - - {formatMessage({ - id: 'global.profile', - defaultMessage: 'Profile', - })} - - - - - - - {formatMessage({ - id: 'app.components.LeftMenu.logout', - defaultMessage: 'Logout', - })} - - - - + + {formatMessage({ + id: 'global.profile', + defaultMessage: 'Profile', + })} + + + + {formatMessage({ + id: 'app.components.LeftMenu.logout', + defaultMessage: 'Logout', + })} + + From 3f2ef2fe74e5dd52f6a75def494b833655b3b784 Mon Sep 17 00:00:00 2001 From: Simone Taeggi Date: Fri, 3 May 2024 14:21:44 +0200 Subject: [PATCH 16/18] feat(main-nav): change locator --- tests/e2e/utils/shared.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/e2e/utils/shared.ts b/tests/e2e/utils/shared.ts index 5f68a18f86d..8663f517317 100644 --- a/tests/e2e/utils/shared.ts +++ b/tests/e2e/utils/shared.ts @@ -14,7 +14,8 @@ export const navToHeader = async (page: Page, navItems: string[], headerText: st // This does not use getByRole because sometimes "Settings" is "Settings 1" if there's a badge notification // BUT if we don't match exact it conflicts with "Advanceed Settings" // As a workaround, we implement our own startsWith with page.locator - const item = page.locator(`role=link[name^="${navItem}"]`); + //const item = page.locator(`role=link[name^="${navItem}"]`); + const item = page.getByRole('link', { name: navItem }); await expect(item).toBeVisible(); await item.click(); } From d6c1670e6fb2227905aae3823b77208c41f55e49 Mon Sep 17 00:00:00 2001 From: Simone Taeggi Date: Fri, 3 May 2024 14:43:25 +0200 Subject: [PATCH 17/18] feat(main-nav): revert e2e utils --- tests/e2e/utils/shared.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/e2e/utils/shared.ts b/tests/e2e/utils/shared.ts index 8663f517317..5f68a18f86d 100644 --- a/tests/e2e/utils/shared.ts +++ b/tests/e2e/utils/shared.ts @@ -14,8 +14,7 @@ export const navToHeader = async (page: Page, navItems: string[], headerText: st // This does not use getByRole because sometimes "Settings" is "Settings 1" if there's a badge notification // BUT if we don't match exact it conflicts with "Advanceed Settings" // As a workaround, we implement our own startsWith with page.locator - //const item = page.locator(`role=link[name^="${navItem}"]`); - const item = page.getByRole('link', { name: navItem }); + const item = page.locator(`role=link[name^="${navItem}"]`); await expect(item).toBeVisible(); await item.click(); } From 42ef2b821d6decee1a4ace5821ba2ab5742e3598 Mon Sep 17 00:00:00 2001 From: Simone Taeggi Date: Fri, 3 May 2024 15:14:01 +0200 Subject: [PATCH 18/18] feat(main-nav): add nav user unit test --- .../components/MainNav/tests/NavUser.test.tsx | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 packages/core/admin/admin/src/components/MainNav/tests/NavUser.test.tsx diff --git a/packages/core/admin/admin/src/components/MainNav/tests/NavUser.test.tsx b/packages/core/admin/admin/src/components/MainNav/tests/NavUser.test.tsx new file mode 100644 index 00000000000..bfeb1236775 --- /dev/null +++ b/packages/core/admin/admin/src/components/MainNav/tests/NavUser.test.tsx @@ -0,0 +1,40 @@ +import { screen, render } from '@tests/utils'; + +import { NavUser } from '../NavUser'; + +describe('NavUser', () => { + it('shows the initials of the user', async () => { + render(John Doe); + expect(screen.getByText('JD')).toBeInTheDocument(); + }); + + it('contains the user name', async () => { + render(John Doe); + const userName = screen.getByText('John Doe'); + expect(userName).toBeInTheDocument(); + }); + + it('shows the user menu when clicked', async () => { + const { user } = render(John Doe); + const buttonMenu = screen.getByRole('button'); + await user.click(buttonMenu); + const userMenu = screen.getByRole('menu'); + expect(userMenu).toBeInTheDocument(); + }); + + it('shows the profile link in the user menu when clicked', async () => { + const { user } = render(John Doe); + const buttonMenu = screen.getByRole('button'); + await user.click(buttonMenu); + const profileLink = screen.getByText('Profile'); + expect(profileLink).toBeInTheDocument(); + }); + + it('shows the logout link in the user menu when clicked', async () => { + const { user } = render(John Doe); + const buttonMenu = screen.getByRole('button'); + await user.click(buttonMenu); + const logoutLink = screen.getByText('Logout'); + expect(logoutLink).toBeInTheDocument(); + }); +});