From 81010311f9505feadd8e87bd9a0dc4a92549e413 Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Fri, 15 Sep 2023 16:40:08 -0400 Subject: [PATCH] chore: builds main menu modal (#3313) --- package.json | 2 + .../components/elements/Drawer/index.tsx | 4 +- .../components/elements/Eyebrow/index.tsx | 3 +- .../components/elements/Hamburger/index.scss | 67 +++++ .../components/elements/Hamburger/index.tsx | 23 ++ .../components/elements/Loading/index.scss | 5 - .../components/elements/Loading/index.tsx | 1 + .../{ => MainMenu}/NavGroup/index.scss | 26 +- .../{ => MainMenu}/NavGroup/index.tsx | 8 +- .../components/elements/MainMenu/index.scss | 126 +++++++++ .../components/elements/MainMenu/index.tsx | 153 +++++++++++ .../admin/components/elements/Nav/index.scss | 247 +++++------------- .../admin/components/elements/Nav/index.tsx | 167 ++---------- .../components/templates/Default/index.scss | 5 +- packages/payload/src/admin/scss/app.scss | 9 +- pnpm-lock.yaml | 25 +- test/access-control/e2e.spec.ts | 6 +- test/admin/components/AfterNavLinks/index.tsx | 27 +- test/admin/e2e.spec.ts | 30 +-- test/admin/styles.scss | 5 +- test/helpers.ts | 19 ++ test/localization/e2e.spec.ts | 36 +-- test/refresh-permissions/e2e.spec.ts | 7 + test/versions/e2e.spec.ts | 13 +- 24 files changed, 578 insertions(+), 436 deletions(-) create mode 100644 packages/payload/src/admin/components/elements/Hamburger/index.scss create mode 100644 packages/payload/src/admin/components/elements/Hamburger/index.tsx rename packages/payload/src/admin/components/elements/{ => MainMenu}/NavGroup/index.scss (76%) rename packages/payload/src/admin/components/elements/{ => MainMenu}/NavGroup/index.tsx (88%) create mode 100644 packages/payload/src/admin/components/elements/MainMenu/index.scss create mode 100644 packages/payload/src/admin/components/elements/MainMenu/index.tsx diff --git a/package.json b/package.json index bbd498299..4f0adb07b 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,8 @@ "prettier": "^3.0.3", "qs": "6.11.2", "react": "18.2.0", + "react-i18next": "11.18.6", + "react-router-dom": "5.3.4", "rimraf": "3.0.2", "shelljs": "0.8.5", "ts-node": "10.9.1", diff --git a/packages/payload/src/admin/components/elements/Drawer/index.tsx b/packages/payload/src/admin/components/elements/Drawer/index.tsx index ff146ed98..aca315158 100644 --- a/packages/payload/src/admin/components/elements/Drawer/index.tsx +++ b/packages/payload/src/admin/components/elements/Drawer/index.tsx @@ -86,9 +86,7 @@ export const Drawer: React.FC = ({ id={`close-drawer__${slug}`} onClick={() => closeModal(slug)} style={{ - width: `calc(${midBreak ? 'var(--gutter-h)' : 'var(--nav-width)'} + ${ - drawerDepth - 1 - } * 25px)`, + width: 'var(--gutter-h)', }} type="button" /> diff --git a/packages/payload/src/admin/components/elements/Eyebrow/index.tsx b/packages/payload/src/admin/components/elements/Eyebrow/index.tsx index b3d9efefd..4de44dfaf 100644 --- a/packages/payload/src/admin/components/elements/Eyebrow/index.tsx +++ b/packages/payload/src/admin/components/elements/Eyebrow/index.tsx @@ -8,11 +8,10 @@ import './index.scss' const baseClass = 'eyebrow' -const Eyebrow: React.FC = ({ actions }) => ( +const Eyebrow: React.FC = () => (
- {actions}
) diff --git a/packages/payload/src/admin/components/elements/Hamburger/index.scss b/packages/payload/src/admin/components/elements/Hamburger/index.scss new file mode 100644 index 000000000..ad6901d4b --- /dev/null +++ b/packages/payload/src/admin/components/elements/Hamburger/index.scss @@ -0,0 +1,67 @@ +@import '../../../scss/styles'; + +.hamburger { + position: relative; + padding: 0; + border: 0; + cursor: pointer; + width: calc(var(--base) / 1.5); + height: calc(var(--base) / 1.5); + background-color: transparent; + outline: none; + + &:focus { + outline: none; + } + + &__line { + background-color: var(--theme-text); + width: 100%; + height: 1px; + position: absolute; + } + + &__top { + top: 2px; + transform: translate3d(0, 0, 0) rotate(0); + } + + &__middle { + top: 50%; + transform: translate3d(0, -50%, 0) rotate(0); + } + + &__bottom { + bottom: 2px; + transform: translate3d(0, 0, 0) rotate(0); + } + + &__x-left { + opacity: 0; + top: 50%; + left: 50%; + transform: translate3d(-50%, -50%, 0) rotate(45deg); + } + + &__x-right { + opacity: 0; + top: 50%; + left: 50%; + transform: translate3d(-50%, -50%, 0) rotate(-45deg); + } + + &--active { + .hamburger { + &__x-left, + &__x-right { + opacity: 1; + } + + &__top, + &__middle, + &__bottom { + opacity: 0; + } + } + } +} diff --git a/packages/payload/src/admin/components/elements/Hamburger/index.tsx b/packages/payload/src/admin/components/elements/Hamburger/index.tsx new file mode 100644 index 000000000..78df02f42 --- /dev/null +++ b/packages/payload/src/admin/components/elements/Hamburger/index.tsx @@ -0,0 +1,23 @@ +import React from 'react' + +import './index.scss' + +const baseClass = 'hamburger' + +const Hamburger: React.FC<{ + isActive?: boolean +}> = (props) => { + const { isActive = false } = props + + return ( +
+
+
+
+
+
+
+ ) +} + +export default Hamburger diff --git a/packages/payload/src/admin/components/elements/Loading/index.scss b/packages/payload/src/admin/components/elements/Loading/index.scss index e9ac6678f..1720041a8 100644 --- a/packages/payload/src/admin/components/elements/Loading/index.scss +++ b/packages/payload/src/admin/components/elements/Loading/index.scss @@ -28,11 +28,6 @@ animation: fade-out ease; } - &.loading-overlay--withoutNav { - left: var(--nav-width); - width: calc(100% - var(--nav-width)); - } - &:after { content: ''; position: absolute; diff --git a/packages/payload/src/admin/components/elements/Loading/index.tsx b/packages/payload/src/admin/components/elements/Loading/index.tsx index 360461ed1..bc8debc49 100644 --- a/packages/payload/src/admin/components/elements/Loading/index.tsx +++ b/packages/payload/src/admin/components/elements/Loading/index.tsx @@ -16,6 +16,7 @@ type Props = { overlayType?: string show?: boolean } + export const LoadingOverlay: React.FC = ({ animationDuration, loadingText, diff --git a/packages/payload/src/admin/components/elements/NavGroup/index.scss b/packages/payload/src/admin/components/elements/MainMenu/NavGroup/index.scss similarity index 76% rename from packages/payload/src/admin/components/elements/NavGroup/index.scss rename to packages/payload/src/admin/components/elements/MainMenu/NavGroup/index.scss index 4a2839911..b64e242d0 100644 --- a/packages/payload/src/admin/components/elements/NavGroup/index.scss +++ b/packages/payload/src/admin/components/elements/MainMenu/NavGroup/index.scss @@ -1,25 +1,28 @@ -@import '../../../scss/styles.scss'; +@import '../../../../scss/styles.scss'; .nav-group { width: 100%; - margin-bottom: base(0.5); + display: flex; + align-items: flex-start; + flex-direction: column; + gap: calc(var(--base) * 0.25); &__toggle { cursor: pointer; color: var(--theme-elevation-400); background: transparent; border: 0; - margin-top: base(0.25); - width: 100%; display: flex; + align-items: center; + padding: 0; + [dir='ltr'] & { - padding-right: base(0.5); padding-left: 0; align-items: flex-start; text-align: left; } + [dir='rtl'] & { - padding-left: base(0.5); padding-right: 0; align-items: flex-start; text-align: right; @@ -27,7 +30,6 @@ svg { flex-shrink: 0; - margin-top: base(-0.2); } &:hover, @@ -44,6 +46,16 @@ } } + &__label { + margin: 0; + } + + &__content { + display: flex; + flex-direction: column; + gap: calc(var(--base) * 0.25); + } + &__indicator { [dir='ltr'] & { margin-left: auto; diff --git a/packages/payload/src/admin/components/elements/NavGroup/index.tsx b/packages/payload/src/admin/components/elements/MainMenu/NavGroup/index.tsx similarity index 88% rename from packages/payload/src/admin/components/elements/NavGroup/index.tsx rename to packages/payload/src/admin/components/elements/MainMenu/NavGroup/index.tsx index 5ba3198ee..561000dd1 100644 --- a/packages/payload/src/admin/components/elements/NavGroup/index.tsx +++ b/packages/payload/src/admin/components/elements/MainMenu/NavGroup/index.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useState } from 'react' import AnimateHeight from 'react-animate-height' -import Chevron from '../../icons/Chevron' -import { usePreferences } from '../../utilities/Preferences' +import Chevron from '../../../icons/Chevron' +import { usePreferences } from '../../../utilities/Preferences' import './index.scss' const baseClass = 'nav-group' @@ -44,7 +44,7 @@ const NavGroup: React.FC = ({ children, label }) => { return (
= ({ children, label }) => { onClick={toggleCollapsed} type="button" > -
{label}
+

{label}

diff --git a/packages/payload/src/admin/components/elements/MainMenu/index.scss b/packages/payload/src/admin/components/elements/MainMenu/index.scss new file mode 100644 index 000000000..927354396 --- /dev/null +++ b/packages/payload/src/admin/components/elements/MainMenu/index.scss @@ -0,0 +1,126 @@ +@import '../../../scss/styles.scss'; + +$transTime: 200ms; + +.main-menu { + display: flex; + position: fixed; + height: 100vh; + + &__blur-bg { + @include blur-bg(); + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + opacity: 0; + transition: all $transTime linear; + } + + &__content { + padding: base(1) 0 base(2); + position: relative; + opacity: 0; + transform: translateX(calc(var(--base) * -1)); + transition: transform $transTime linear; + overflow: auto; + width: 50%; + } + + &__content-children { + position: relative; + display: flex; + flex-direction: column; + gap: var(--base); + } + + &__close { + @extend %btn-reset; + position: relative; + z-index: 2; + flex-shrink: 0; + text-indent: -9999px; + background: rgba(0, 0, 0, 0.2); + cursor: pointer; + opacity: 0; + will-change: opacity; + transition: none; + transition-delay: 0ms; + flex-grow: 1; + flex-shrink: 1; + + &:active, + &:focus { + outline: 0; + } + + &::before { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: calc(var(--base) * 4); + content: ' '; + background: linear-gradient(to right, rgba(0, 0, 0, 0.25) 0%, rgba(0, 0, 0, 0) 100%); + } + } + + &--is-open { + .main-menu { + &__content, + &__blur-bg, + &__close { + opacity: 1; + } + + &__close { + transition: opacity $transTime ease-in-out; + transition-delay: $transTime; + } + + &__content { + transform: translateX(0); + } + } + } + + &__link { + margin: 0; + + a { + text-decoration: none; + } + + &:hover { + text-decoration: underline; + } + } + + &__controls { + display: flex; + flex-direction: column; + gap: calc(var(--base) / 2); + } + + &.payload__modal-item--exitActive { + transition: none; + } + + @include mid-break { + .main-menu { + &__close { + display: none; + } + + &__content { + width: 100%; + padding-top: calc(var(--base) * 2); + + &::after { + display: none; + } + } + } + } +} diff --git a/packages/payload/src/admin/components/elements/MainMenu/index.tsx b/packages/payload/src/admin/components/elements/MainMenu/index.tsx new file mode 100644 index 000000000..1067ee220 --- /dev/null +++ b/packages/payload/src/admin/components/elements/MainMenu/index.tsx @@ -0,0 +1,153 @@ +import { Modal, useModal } from '@faceless-ui/modal' +import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Link, NavLink } from 'react-router-dom' + +import type { EntityToGroup, Group } from '../../../utilities/groupNavItems' + +import { getTranslation } from '../../../../utilities/getTranslation' +import { EntityType, groupNavItems } from '../../../utilities/groupNavItems' +import Account from '../../graphics/Account' +import { useAuth } from '../../utilities/Auth' +import { useConfig } from '../../utilities/Config' +import { Gutter } from '../Gutter' +import Localizer from '../Localizer' +import Logout from '../Logout' +import NavGroup from './NavGroup' +import './index.scss' + +const baseClass = 'main-menu' + +export const mainMenuSlug = 'main-menu' + +export const MainMenuDrawer: React.FC = () => { + const { permissions, user } = useAuth() + const { closeModal, modalState } = useModal() + + const { i18n, t } = useTranslation('general') + + const { + admin: { + components: { afterNavLinks, beforeNavLinks }, + }, + collections, + globals, + routes: { admin }, + } = useConfig() + + const [groups, setGroups] = useState([]) + + const [isOpen, setIsOpen] = useState(false) + + useEffect(() => { + setIsOpen(modalState[mainMenuSlug]?.isOpen) + }, [modalState]) + + useEffect(() => { + setGroups( + groupNavItems( + [ + ...collections + .filter( + ({ admin: { hidden } }) => + !(typeof hidden === 'function' ? hidden({ user }) : hidden), + ) + .map((collection) => { + const entityToGroup: EntityToGroup = { + entity: collection, + type: EntityType.collection, + } + + return entityToGroup + }), + ...globals + .filter( + ({ admin: { hidden } }) => + !(typeof hidden === 'function' ? hidden({ user }) : hidden), + ) + .map((global) => { + const entityToGroup: EntityToGroup = { + entity: global, + type: EntityType.global, + } + + return entityToGroup + }), + ], + permissions, + i18n, + ), + ) + }, [collections, globals, permissions, i18n, i18n.language, user]) + + return ( + +
+ + - - -
- + +
+
+
+ + + +
+
+ +
) } diff --git a/packages/payload/src/admin/components/templates/Default/index.scss b/packages/payload/src/admin/components/templates/Default/index.scss index 7d8629cda..6cdb026a0 100644 --- a/packages/payload/src/admin/components/templates/Default/index.scss +++ b/packages/payload/src/admin/components/templates/Default/index.scss @@ -2,7 +2,6 @@ .template-default { min-height: 100vh; - display: flex; &__wrap { min-width: 0; @@ -11,15 +10,15 @@ } @include mid-break { - display: block; width: 100%; + [dir='ltr'] & { margin-left: 0; } + [dir='rtl'] & { margin-right: 0; } - padding-top: base(3); &__wrap { padding: 0 0 $baseline; diff --git a/packages/payload/src/admin/scss/app.scss b/packages/payload/src/admin/scss/app.scss index 5f6b49061..89f9878b3 100644 --- a/packages/payload/src/admin/scss/app.scss +++ b/packages/payload/src/admin/scss/app.scss @@ -13,7 +13,6 @@ --breakpoint-m-width: #{$breakpoint-m-width}; --breakpoint-l-width: #{$breakpoint-l-width}; --scrollbar-width: 17px; - --nav-width: #{base(9)}; --theme-bg: var(--theme-elevation-0); --theme-input-bg: var(--theme-elevation-0); @@ -36,16 +35,10 @@ --accessibility-outline: 2px solid var(--theme-text); --accessibility-outline-offset: 2px; - --gutter-h: #{base(5)}; - - @include large-break { - --gutter-h: #{base(3)}; - --nav-width: #{base(8)}; - } + --gutter-h: #{base(3)}; @include mid-break { --gutter-h: #{base(2)}; - --nav-width: 0px; } @include small-break { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c8b7a6fb..894c6d580 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,18 +74,21 @@ importers: prettier: specifier: ^3.0.3 version: 3.0.3 -<<<<<<< HEAD qs: specifier: 6.11.2 version: 6.11.2 react: specifier: 18.2.0 version: 18.2.0 -======= + react-i18next: + specifier: 11.18.6 + version: 11.18.6(i18next@22.5.1)(react-dom@18.2.0)(react@18.2.0) + react-router-dom: + specifier: 5.3.4 + version: 5.3.4(react@18.2.0) rimraf: specifier: 3.0.2 version: 3.0.2 ->>>>>>> dd0514bd2cd312dbbc01ec8f018ff575bb93182f shelljs: specifier: 0.8.5 version: 0.8.5 @@ -8515,13 +8518,11 @@ packages: tiny-invariant: 1.3.1 tiny-warning: 1.0.3 value-equal: 1.0.1 - dev: false /hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} dependencies: react-is: 16.13.1 - dev: false /hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -8566,7 +8567,6 @@ packages: resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} dependencies: void-elements: 3.1.0 - dev: false /html-webpack-plugin@5.5.3(webpack@5.88.2): resolution: {integrity: sha512-6YrDKTuqaP/TquFH7h4srYWsZx+x6k6+FbsTm0ziCwGHDP78Unr1r9F/H4+sGmMbX08GQcJ+K64x55b+7VM/jg==} @@ -8690,7 +8690,6 @@ packages: resolution: {integrity: sha512-8TGPgM3pAD+VRsMtUMNknRz3kzqwp/gPALrWMsDnmC1mKqJwpWyooQRLMcbTwq8z8YwSmuj+ZYvc+xCuEpkssA==} dependencies: '@babel/runtime': 7.22.11 - dev: false /iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} @@ -10715,7 +10714,6 @@ packages: /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - dev: false /object-inspect@1.12.3: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} @@ -11147,7 +11145,6 @@ packages: resolution: {integrity: sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==} dependencies: isarray: 0.0.1 - dev: false /path-type@3.0.0: resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} @@ -12187,7 +12184,6 @@ packages: loose-envify: 1.4.0 object-assign: 4.1.1 react-is: 16.13.1 - dev: false /proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} @@ -12425,11 +12421,9 @@ packages: i18next: 22.5.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - dev: false /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - dev: false /react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} @@ -12474,7 +12468,6 @@ packages: react-router: 5.3.4(react@18.2.0) tiny-invariant: 1.3.1 tiny-warning: 1.0.3 - dev: false /react-router-navigation-prompt@1.9.6(react-router-dom@5.3.4)(react@18.2.0): resolution: {integrity: sha512-l0sAtbroHK8i1/Eyy29XcrMpBEt0R08BaScgMUt8r5vWWbLz7G0ChOikayTCQm7QgDFsHw8gVnxDJb7TBZCAKg==} @@ -12501,7 +12494,6 @@ packages: react-is: 16.13.1 tiny-invariant: 1.3.1 tiny-warning: 1.0.3 - dev: false /react-select@5.7.4(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-NhuE56X+p9QDFh4BgeygHFIvJJszO1i1KSkg/JPcIJrbovyRtI+GuOEa4XzFCEpZRAEoEI8u/cAHK+jG/PgUzQ==} @@ -12824,7 +12816,6 @@ packages: /resolve-pathname@3.0.0: resolution: {integrity: sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==} - dev: false /resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -13816,11 +13807,9 @@ packages: /tiny-invariant@1.3.1: resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} - dev: false /tiny-warning@1.0.3: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} - dev: false /titleize@3.0.0: resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==} @@ -14371,7 +14360,6 @@ packages: /value-equal@1.0.1: resolution: {integrity: sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==} - dev: false /vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} @@ -14380,7 +14368,6 @@ packages: /void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} - dev: false /w3c-xmlserializer@4.0.0: resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} diff --git a/test/access-control/e2e.spec.ts b/test/access-control/e2e.spec.ts index 926bf8c00..dcbfd2280 100644 --- a/test/access-control/e2e.spec.ts +++ b/test/access-control/e2e.spec.ts @@ -6,6 +6,7 @@ import type { ReadOnlyCollection, RestrictedVersion } from './payload-types' import payload from '../../packages/payload/src' import wait from '../../packages/payload/src/utilities/wait' +import { openMainMenu } from '../helpers' import { AdminUrlUtil } from '../helpers/adminUrlUtil' import { initPayloadE2E } from '../helpers/configHelpers' import { @@ -99,6 +100,7 @@ describe('access control', () => { test('should not show in nav', async () => { await page.goto(url.admin) + await openMainMenu(page) await expect(page.locator('.nav >> a:has-text("Restricteds")')).toHaveCount(0) }) @@ -137,7 +139,9 @@ describe('access control', () => { test('should show in nav', async () => { await page.goto(url.admin) - await expect(page.locator(`.nav a[href="/admin/collections/${readOnlySlug}"]`)).toHaveCount(1) + await expect( + page.locator(`.main-menu a[href="/admin/collections/${readOnlySlug}"]`), + ).toHaveCount(1) }) test('should have collection url', async () => { diff --git a/test/admin/components/AfterNavLinks/index.tsx b/test/admin/components/AfterNavLinks/index.tsx index bc4c6f145..dfdab7f35 100644 --- a/test/admin/components/AfterNavLinks/index.tsx +++ b/test/admin/components/AfterNavLinks/index.tsx @@ -2,11 +2,9 @@ import React from 'react' import { NavLink } from 'react-router-dom' // As this is the demo project, we import our dependencies from the `src` directory. -import Chevron from '../../../../packages/payload/src/admin/components/icons/Chevron' import { useConfig } from '../../../../packages/payload/src/admin/components/utilities/Config' // In your projects, you can import as follows: -// import { Chevron } from 'payload/components'; // import { useConfig } from 'payload/components/utilities'; const baseClass = 'after-nav-links' @@ -17,26 +15,35 @@ const AfterNavLinks: React.FC = () => { } = useConfig() return ( -
- Custom Routes - +
) } diff --git a/test/admin/e2e.spec.ts b/test/admin/e2e.spec.ts index d7c49e1f5..0da758f92 100644 --- a/test/admin/e2e.spec.ts +++ b/test/admin/e2e.spec.ts @@ -8,7 +8,7 @@ import type { Post } from './config' import payload from '../../packages/payload/src' import { mapAsync } from '../../packages/payload/src/utilities/mapAsync' import wait from '../../packages/payload/src/utilities/wait' -import { saveDocAndAssert, saveDocHotkeyAndAssert } from '../helpers' +import { openMainMenu, saveDocAndAssert, saveDocHotkeyAndAssert } from '../helpers' import { AdminUrlUtil } from '../helpers/adminUrlUtil' import { initPayloadE2E } from '../helpers/configHelpers' import { globalSlug, slug } from './shared' @@ -45,27 +45,29 @@ describe('admin', () => { describe('Nav', () => { test('should nav to collection - sidebar', async () => { await page.goto(url.admin) - const collectionLink = page.locator(`#nav-${slug}`) - await collectionLink.click() - + await openMainMenu(page) + await page.locator(`#nav-${slug}`).click() expect(page.url()).toContain(url.list) }) test('should nav to a global - sidebar', async () => { await page.goto(url.admin) + await openMainMenu(page) await page.locator(`#nav-global-${globalSlug}`).click() - expect(page.url()).toContain(url.global(globalSlug)) }) test('should navigate to collection - card', async () => { await page.goto(url.admin) + await wait(200) await page.locator(`#card-${slug}`).click() expect(page.url()).toContain(url.list) }) test('should collapse and expand collection groups', async () => { await page.goto(url.admin) + await openMainMenu(page) + const navGroup = page.locator('#nav-group-One .nav-group__toggle') const link = page.locator('#nav-group-one-collection-ones') @@ -81,6 +83,8 @@ describe('admin', () => { test('should collapse and expand globals groups', async () => { await page.goto(url.admin) + await openMainMenu(page) + const navGroup = page.locator('#nav-group-Group .nav-group__toggle') const link = page.locator('#nav-global-group-globals-one') @@ -96,12 +100,9 @@ describe('admin', () => { test('should save nav group collapse preferences', async () => { await page.goto(url.admin) - - const navGroup = page.locator('#nav-group-One .nav-group__toggle') - await navGroup.click() - + await openMainMenu(page) + await page.locator('#nav-group-One .nav-group__toggle').click() await page.goto(url.admin) - const link = page.locator('#nav-group-one-collection-ones') await expect(link).toBeHidden() }) @@ -189,13 +190,11 @@ describe('admin', () => { }) test('should delete existing', async () => { - const { id, ...post } = await createPost() - + const { id, title } = await createPost() await page.goto(url.edit(id)) await page.locator('#action-delete').click() await page.locator('#confirm-delete').click() - - await expect(page.locator(`text=Post en "${post.title}" successfully deleted.`)).toBeVisible() + await expect(page.locator(`text=Post en "${title}" successfully deleted.`)).toBeVisible() expect(page.url()).toContain(url.list) }) @@ -724,7 +723,8 @@ describe('admin', () => { describe('custom css', () => { test('should see custom css in admin UI', async () => { await page.goto(url.admin) - const navControls = page.locator('.nav__controls') + await openMainMenu(page) + const navControls = page.locator('.main-menu__controls') await expect(navControls).toHaveCSS('font-family', 'monospace') }) }) diff --git a/test/admin/styles.scss b/test/admin/styles.scss index 4a36b1982..755bd6b62 100644 --- a/test/admin/styles.scss +++ b/test/admin/styles.scss @@ -1,7 +1,8 @@ -.nav__controls { +.main-menu__controls { font-family: monospace; background-image: url('/placeholder.png'); } -.nav__controls:before { + +.main-menu__controls:before { content: 'custom-css'; } diff --git a/test/helpers.ts b/test/helpers.ts index 740ae5f87..53810ff3e 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -55,3 +55,22 @@ export async function saveDocAndAssert(page: Page, selector = '#action-save'): P await expect(page.locator('.Toastify')).toContainText('successfully') expect(page.url()).not.toContain('create') } + +export async function openMainMenu(page: Page): Promise { + await page.locator('.payload__modal-toggler--slug-main-menu').click() + const mainMenuModal = page.locator('#main-menu') + await expect(mainMenuModal).toBeVisible() +} + +export async function closeMainMenu(page: Page): Promise { + await page.locator('.payload__modal-toggler--slug-main-menu--is-open').click() + const mainMenuModal = page.locator('#main-menu') + await expect(mainMenuModal).toBeHidden() +} + +export async function changeLocale(page: Page, newLocale: string) { + await openMainMenu(page) + await page.locator('.localizer >> button').first().click() + await page.locator(`.localizer >> a:has-text("${newLocale}")`).click() + expect(page.url()).toContain(`locale=${newLocale}`) +} diff --git a/test/localization/e2e.spec.ts b/test/localization/e2e.spec.ts index 96b76d26b..042943cbe 100644 --- a/test/localization/e2e.spec.ts +++ b/test/localization/e2e.spec.ts @@ -5,7 +5,7 @@ import { expect, test } from '@playwright/test' import type { LocalizedPost } from './payload-types' import payload from '../../packages/payload/src' -import { saveDocAndAssert } from '../helpers' +import { changeLocale, saveDocAndAssert } from '../helpers' import { AdminUrlUtil } from '../helpers/adminUrlUtil' import { initPayloadTest } from '../helpers/configHelpers' import { englishTitle, localizedPostsSlug, spanishLocale } from './shared' @@ -29,6 +29,7 @@ const arabicTitle = 'arabic title' const description = 'description' let page: Page + describe('Localization', () => { beforeAll(async ({ browser }) => { const { serverURL } = await initPayloadTest({ @@ -52,7 +53,7 @@ describe('Localization', () => { await saveDocAndAssert(page) // Change back to English - await changeLocale('es') + await changeLocale(page, 'es') // Localized field should not be populated await expect(page.locator('#field-title')).toBeEmpty() @@ -60,7 +61,7 @@ describe('Localization', () => { await fillValues({ title: spanishTitle, description }) await saveDocAndAssert(page) - await changeLocale(defaultLocale) + await changeLocale(page, defaultLocale) // Expect english title await expect(page.locator('#field-title')).toHaveValue(title) @@ -73,13 +74,13 @@ describe('Localization', () => { const newLocale = 'es' // Change to Spanish - await changeLocale(newLocale) + await changeLocale(page, newLocale) await fillValues({ title: spanishTitle, description }) await saveDocAndAssert(page) // Change back to English - await changeLocale(defaultLocale) + await changeLocale(page, defaultLocale) // Localized field should not be populated await expect(page.locator('#field-title')).toBeEmpty() @@ -101,13 +102,13 @@ describe('Localization', () => { const newLocale = 'ar' // Change to Arabic - await changeLocale(newLocale) + await changeLocale(page, newLocale) await fillValues({ title: arabicTitle, description }) await saveDocAndAssert(page) // Change back to English - await changeLocale(defaultLocale) + await changeLocale(page, defaultLocale) // Localized field should not be populated await expect(page.locator('#field-title')).toBeEmpty() @@ -125,16 +126,16 @@ describe('Localization', () => { }) describe('localized duplicate', () => { - let id - - beforeAll(async () => { + test('should duplicate data for all locales', async () => { const localizedPost = await payload.create({ collection: localizedPostsSlug, data: { title: englishTitle, }, }) - id = localizedPost.id + + const id = localizedPost.id.toString() + await payload.update({ collection: localizedPostsSlug, id, @@ -143,17 +144,12 @@ describe('Localization', () => { title: spanishTitle, }, }) - }) - test('should duplicate data for all locales', async () => { await page.goto(url.edit(id)) - await page.locator('.btn.duplicate').first().click() await expect(page.locator('.Toastify')).toContainText('successfully') - await expect(page.locator('#field-title')).toHaveValue(englishTitle) - - await changeLocale(spanishLocale) + await changeLocale(page, spanishLocale) await expect(page.locator('#field-title')).toHaveValue(spanishTitle) }) }) @@ -165,9 +161,3 @@ async function fillValues(data: Partial) { if (titleVal) await page.locator('#field-title').fill(titleVal) if (descVal) await page.locator('#field-description').fill(descVal) } - -async function changeLocale(newLocale: string) { - await page.locator('.localizer >> button').first().click() - await page.locator(`.localizer >> a:has-text("${newLocale}")`).click() - expect(page.url()).toContain(`locale=${newLocale}`) -} diff --git a/test/refresh-permissions/e2e.spec.ts b/test/refresh-permissions/e2e.spec.ts index 2d52660b6..7b64f1c3b 100644 --- a/test/refresh-permissions/e2e.spec.ts +++ b/test/refresh-permissions/e2e.spec.ts @@ -2,6 +2,7 @@ import type { Page } from '@playwright/test' import { expect, test } from '@playwright/test' +import { closeMainMenu, openMainMenu } from '../helpers' import { initPayloadE2E } from '../helpers/configHelpers' const { beforeAll, describe } = test @@ -19,6 +20,8 @@ describe('refresh-permissions', () => { test('should show test global immediately after allowing access', async () => { await page.goto(`${serverURL}/admin/globals/settings`) + await openMainMenu(page) + // Ensure that we have loaded accesses by checking that settings collection // at least is visible in the menu. await expect(page.locator('#nav-global-settings')).toBeVisible() @@ -26,10 +29,14 @@ describe('refresh-permissions', () => { // Test collection should be hidden at first. await expect(page.locator('#nav-global-test')).toBeHidden() + await closeMainMenu(page) + // Allow access to test global. await page.locator('.custom-checkbox:has(#field-test) input').check() await page.locator('#action-save').click() + await openMainMenu(page) + // Now test collection should appear in the menu. await expect(page.locator('#nav-global-test')).toBeVisible() }) diff --git a/test/versions/e2e.spec.ts b/test/versions/e2e.spec.ts index eec0606f2..dfa03ee6c 100644 --- a/test/versions/e2e.spec.ts +++ b/test/versions/e2e.spec.ts @@ -28,6 +28,7 @@ import type { Page } from '@playwright/test' import { expect, test } from '@playwright/test' import wait from '../../packages/payload/src/utilities/wait' +import { changeLocale } from '../helpers' import { AdminUrlUtil } from '../helpers/adminUrlUtil' import { initPayloadE2E } from '../helpers/configHelpers' import { autosaveSlug, draftSlug } from './shared' @@ -130,25 +131,19 @@ describe('versions', () => { await page.locator('#field-description').fill(description) await wait(500) - await changeLocale(spanishLocale) + await changeLocale(page, spanishLocale) await page.locator('#field-title').fill(spanishTitle) await wait(500) - await changeLocale(locale) + await changeLocale(page, locale) await page.locator('#field-description').fill(newDescription) await wait(500) - await changeLocale(spanishLocale) + await changeLocale(page, spanishLocale) await wait(500) await page.reload() await expect(page.locator('#field-title')).toHaveValue(spanishTitle) await expect(page.locator('#field-description')).toHaveValue(newDescription) }) }) - - async function changeLocale(newLocale: string) { - await page.locator('.localizer >> button').first().click() - await page.locator(`.localizer >> a:has-text("${newLocale}")`).click() - expect(page.url()).toContain(`locale=${newLocale}`) - } })