chore: builds main menu modal (#3313)

This commit is contained in:
Jacob Fletcher
2023-09-15 16:40:08 -04:00
committed by GitHub
parent 4b514d4c94
commit 81010311f9
24 changed files with 578 additions and 436 deletions

View File

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

View File

@@ -86,9 +86,7 @@ export const Drawer: React.FC<Props> = ({
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"
/>

View File

@@ -8,11 +8,10 @@ import './index.scss'
const baseClass = 'eyebrow'
const Eyebrow: React.FC<Props> = ({ actions }) => (
const Eyebrow: React.FC<Props> = () => (
<div className={baseClass}>
<Gutter className={`${baseClass}__wrap`}>
<StepNav />
{actions}
</Gutter>
</div>
)

View File

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

View File

@@ -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 (
<div className={[baseClass, isActive && `${baseClass}--active`].filter(Boolean).join(' ')}>
<div className={`${baseClass}__line ${baseClass}__top`} />
<div className={`${baseClass}__line ${baseClass}__middle`} />
<div className={`${baseClass}__line ${baseClass}__bottom`} />
<div className={`${baseClass}__line ${baseClass}__x-left`} />
<div className={`${baseClass}__line ${baseClass}__x-right`} />
</div>
)
}
export default Hamburger

View File

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

View File

@@ -16,6 +16,7 @@ type Props = {
overlayType?: string
show?: boolean
}
export const LoadingOverlay: React.FC<Props> = ({
animationDuration,
loadingText,

View File

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

View File

@@ -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<Props> = ({ children, label }) => {
return (
<div
className={[`${baseClass}`, `${label}`, collapsed && `${baseClass}--collapsed`]
className={[baseClass, `${label}`, collapsed && `${baseClass}--collapsed`]
.filter(Boolean)
.join(' ')}
id={`nav-group-${label}`}
@@ -59,7 +59,7 @@ const NavGroup: React.FC<Props> = ({ children, label }) => {
onClick={toggleCollapsed}
type="button"
>
<div className={`${baseClass}__label`}>{label}</div>
<h4 className={`${baseClass}__label`}>{label}</h4>
<Chevron className={`${baseClass}__indicator`} />
</button>
<AnimateHeight duration={animate ? 200 : 0} height={collapsed ? 0 : 'auto'}>

View File

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

View File

@@ -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<Group[]>([])
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 (
<Modal
className={[baseClass, isOpen && `${baseClass}--is-open`].filter(Boolean).join(' ')}
slug={mainMenuSlug}
>
<div className={`${baseClass}__blur-bg`} />
<nav className={`${baseClass}__content`}>
<Gutter className={`${baseClass}__content-children`}>
{Array.isArray(beforeNavLinks) &&
beforeNavLinks.map((Component, i) => <Component key={i} />)}
<h4 className={`${baseClass}__link`}>
<NavLink activeClassName="active" id="nav-dashboard" to={admin}>
{t('dashboard')}
</NavLink>
</h4>
{groups.map(({ entities, label }, key) => {
return (
<NavGroup {...{ key, label }}>
{entities.map(({ entity, type }, i) => {
let entityLabel: string
let href: string
let id: string
if (type === EntityType.collection) {
href = `${admin}/collections/${entity.slug}`
entityLabel = getTranslation(entity.labels.plural, i18n)
id = `nav-${entity.slug}`
}
if (type === EntityType.global) {
href = `${admin}/globals/${entity.slug}`
entityLabel = getTranslation(entity.label, i18n)
id = `nav-global-${entity.slug}`
}
return (
<h4 className={`${baseClass}__link`} key={i}>
<NavLink activeClassName="active" id={id} to={href}>
{entityLabel}
</NavLink>
</h4>
)
})}
</NavGroup>
)
})}
{Array.isArray(afterNavLinks) &&
afterNavLinks.map((Component, i) => <Component key={i} />)}
<div className={`${baseClass}__controls`}>
<Localizer />
<Link
aria-label={t('authentication:account')}
className={`${baseClass}__account`}
to={`${admin}/account`}
>
<Account />
</Link>
<Logout />
</div>
</Gutter>
</nav>
<button
aria-label={t('close')}
className={`${baseClass}__close`}
id={`close-drawer__${mainMenuSlug}`}
onClick={() => closeModal(mainMenuSlug)}
type="button"
/>
</Modal>
)
}

View File

@@ -1,208 +1,83 @@
@import '../../../scss/styles.scss';
.nav {
flex-shrink: 0;
position: sticky;
top: 0;
left: 0;
height: 100vh;
width: var(--nav-width);
overflow: hidden;
[dir='ltr'] & {
border-right: 1px solid var(--theme-elevation-100);
}
[dir='rtl'] & {
border-left: 1px solid var(--theme-elevation-100);
}
width: 100%;
height: calc(var(--base) * 3);
z-index: $z-status;
// TODO: remove this once the nav has been refactored
// eventually it will contain nav items with click events
pointer-events: none;
header {
width: 100%;
display: flex;
margin-bottom: base(1.5);
a,
button {
display: block;
padding: 0;
svg {
display: block;
}
}
}
&__brand {
[dir='ltr'] & {
margin-right: base(1);
}
[dir='rtl'] & {
margin-left: base(1);
}
}
&__mobile-menu-btn {
background: none;
border: 0;
&__bg {
@include blur-bg;
opacity: 0;
visibility: hidden;
cursor: pointer;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
&--show-bg {
opacity: 1;
}
// when the modal is open, the nav overlays the top portion
// we need to make sure the modal content is clickable
// so we disable pointer events on the nav
// but reenable them on the modal toggler
&--main-menu-open {
pointer-events: none;
.nav__modalToggler {
pointer-events: all;
}
}
&__content {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
padding: 0 calc(var(--gutter-h) / 2);
position: relative;
}
&__modalToggler {
pointer-events: all;
background-color: transparent;
border: 0;
padding: 0;
cursor: pointer;
z-index: 999999;
transform: translate3d(-50%, 0, 0);
&:active,
&:focus {
outline: none;
}
}
&__scroll {
height: 100%;
display: flex;
flex-direction: column;
padding: base(1.5) base(1);
width: calc(100% + #{base(1)});
overflow-y: scroll;
}
&__wrap {
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
flex-grow: 1;
}
&__label {
color: var(--theme-elevation-400);
}
&__controls {
margin-top: auto;
margin-bottom: 0;
> * {
margin-top: base(1);
}
a:focus-visible {
outline: var(--accessibility-outline);
}
}
&__log-out {
&:hover {
g {
transform: translateX(-#{base(0.125)});
}
}
}
nav {
a {
position: relative;
padding: base(0.125) base(1.5) base(0.125) 0;
display: flex;
text-decoration: none;
[dir='rtl'] & {
padding: base(0.125) 0 base(0.125) base(1.5);
}
&:focus:not(:focus-visible) {
box-shadow: none;
font-weight: 600;
}
&:hover,
&:focus-visible {
text-decoration: underline;
}
&.active {
font-weight: normal;
font-weight: 600;
[dir='ltr'] & {
padding-left: base(0.6);
}
[dir='rtl'] & {
padding-right: base(0.6);
}
}
}
}
&__link {
svg {
opacity: 0;
position: absolute;
[dir='ltr'] & {
left: - base(0.5);
transform: rotate(-90deg);
}
[dir='rtl'] & {
right: - base(0.5);
transform: rotate(90deg);
}
}
&.active {
svg {
opacity: 1;
}
}
&__scrollable {
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
@include mid-break {
@include blur-bg;
position: fixed;
width: 100%;
height: base(3);
z-index: var(--z-modal);
height: calc(var(--base) * 2);
&__scroll {
padding: 0;
overflow: hidden;
width: 100%;
display: block;
}
header,
&__wrap {
position: relative;
z-index: 1;
padding: $baseline var(--gutter-h);
}
header {
justify-content: space-between;
margin: 0;
}
&__mobile-menu-btn {
opacity: 1;
visibility: visible;
}
&__wrap {
padding-top: 0;
visibility: hidden;
opacity: 0;
overflow-y: scroll;
position: fixed;
top: base(4);
bottom: 0;
}
&.nav--menu-active {
height: 100vh;
.nav__wrap {
visibility: visible;
opacity: 1;
.nav {
&__content {
padding: 0 var(--gutter-h);
}
}
nav a {
font-size: base(0.875);
line-height: base(1.25);
font-weight: 600;
&__modalToggler {
transform: unset;
}
}
}
}

View File

@@ -1,159 +1,48 @@
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Link, NavLink, useHistory } from 'react-router-dom'
import { ModalToggler, useModal } from '@faceless-ui/modal'
import React, { Fragment, useEffect } from 'react'
import { useHistory } 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 Icon from '../../graphics/Icon'
import Chevron from '../../icons/Chevron'
import CloseMenu from '../../icons/CloseMenu'
import Menu from '../../icons/Menu'
import { useAuth } from '../../utilities/Auth'
import { useConfig } from '../../utilities/Config'
import RenderCustomComponent from '../../utilities/RenderCustomComponent'
import Localizer from '../Localizer'
import Logout from '../Logout'
import NavGroup from '../NavGroup'
import Hamburger from '../Hamburger'
import { MainMenuDrawer, mainMenuSlug } from '../MainMenu'
import './index.scss'
const baseClass = 'nav'
const DefaultNav = () => {
const { permissions, user } = useAuth()
const [menuActive, setMenuActive] = useState(false)
const [groups, setGroups] = useState<Group[]>([])
const history = useHistory()
const { i18n, t } = useTranslation('general')
const {
admin: {
components: { afterNavLinks, beforeNavLinks },
},
collections,
globals,
routes: { admin },
} = useConfig()
const classes = [baseClass, menuActive && `${baseClass}--menu-active`].filter(Boolean).join(' ')
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])
const { closeModal, isModalOpen } = useModal()
const isOpen = isModalOpen(mainMenuSlug)
useEffect(
() =>
history.listen(() => {
setMenuActive(false)
closeModal(mainMenuSlug)
}),
[history],
[history, closeModal],
)
return (
<aside className={classes}>
<div className={`${baseClass}__scroll`}>
<header>
<Link aria-label={t('dashboard')} className={`${baseClass}__brand`} to={admin}>
<Icon />
</Link>
<button
className={`${baseClass}__mobile-menu-btn`}
onClick={() => setMenuActive(!menuActive)}
type="button"
>
{menuActive && <CloseMenu />}
{!menuActive && <Menu />}
</button>
</header>
<nav className={`${baseClass}__wrap`}>
{Array.isArray(beforeNavLinks) &&
beforeNavLinks.map((Component, i) => <Component key={i} />)}
{groups.map(({ entities, label }, key) => {
return (
<NavGroup {...{ key, label }}>
{entities.map(({ entity, type }, i) => {
let entityLabel: string
let href: string
let id: string
if (type === EntityType.collection) {
href = `${admin}/collections/${entity.slug}`
entityLabel = getTranslation(entity.labels.plural, i18n)
id = `nav-${entity.slug}`
}
if (type === EntityType.global) {
href = `${admin}/globals/${entity.slug}`
entityLabel = getTranslation(entity.label, i18n)
id = `nav-global-${entity.slug}`
}
return (
<NavLink
activeClassName="active"
className={`${baseClass}__link`}
id={id}
key={i}
to={href}
>
<Chevron />
{entityLabel}
</NavLink>
)
})}
</NavGroup>
)
})}
{Array.isArray(afterNavLinks) &&
afterNavLinks.map((Component, i) => <Component key={i} />)}
<div className={`${baseClass}__controls`}>
<Localizer />
<Link
aria-label={t('authentication:account')}
className={`${baseClass}__account`}
to={`${admin}/account`}
>
<Account />
</Link>
<Logout />
</div>
</nav>
</div>
</aside>
<Fragment>
<header
className={[
baseClass,
!isOpen && `${baseClass}--show-bg`,
isOpen && `${baseClass}--main-menu-open`,
]
.filter(Boolean)
.join(' ')}
>
<div className={`${baseClass}__bg`} />
<div className={`${baseClass}__content`}>
<ModalToggler className={`${baseClass}__modalToggler`} slug={mainMenuSlug}>
<Hamburger isActive={isOpen} />
</ModalToggler>
</div>
</header>
<MainMenuDrawer />
</Fragment>
)
}

View File

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

View File

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

25
pnpm-lock.yaml generated
View File

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

View File

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

View File

@@ -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 (
<div className={baseClass}>
<span className="nav__label">Custom Routes</span>
<nav>
<div
className={baseClass}
style={{
display: 'flex',
flexDirection: 'column',
gap: 'calc(var(--base) / 4)',
}}
>
<h4 className="nav__label" style={{ color: 'var(--theme-elevation-400)', margin: 0 }}>
Custom Routes
</h4>
<h4 className="nav__link" style={{ margin: 0 }}>
<NavLink
activeClassName="active"
className="nav__link"
style={{ textDecoration: 'none' }}
to={`${adminRoute}/custom-default-route`}
>
<Chevron />
Default Template
</NavLink>
</h4>
<h4 className="nav__link" style={{ margin: 0 }}>
<NavLink
activeClassName="active"
className="nav__link"
style={{ textDecoration: 'none' }}
to={`${adminRoute}/custom-minimal-route`}
>
<Chevron />
Minimal Template
</NavLink>
</nav>
</h4>
</div>
)
}

View File

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

View File

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

View File

@@ -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<void> {
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<void> {
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}`)
}

View File

@@ -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<LocalizedPost>) {
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}`)
}

View File

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

View File

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