chore: builds main menu modal (#3313)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -16,6 +16,7 @@ type Props = {
|
||||
overlayType?: string
|
||||
show?: boolean
|
||||
}
|
||||
|
||||
export const LoadingOverlay: React.FC<Props> = ({
|
||||
animationDuration,
|
||||
loadingText,
|
||||
|
||||
@@ -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;
|
||||
@@ -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'}>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
margin-bottom: base(1.5);
|
||||
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;
|
||||
|
||||
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;
|
||||
.nav {
|
||||
&__content {
|
||||
padding: 0 var(--gutter-h);
|
||||
}
|
||||
|
||||
header,
|
||||
&__wrap {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: $baseline var(--gutter-h);
|
||||
&__modalToggler {
|
||||
transform: unset;
|
||||
}
|
||||
|
||||
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 a {
|
||||
font-size: base(0.875);
|
||||
line-height: base(1.25);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
<Fragment>
|
||||
<header
|
||||
className={[
|
||||
baseClass,
|
||||
!isOpen && `${baseClass}--show-bg`,
|
||||
isOpen && `${baseClass}--main-menu-open`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{menuActive && <CloseMenu />}
|
||||
{!menuActive && <Menu />}
|
||||
</button>
|
||||
<div className={`${baseClass}__bg`} />
|
||||
<div className={`${baseClass}__content`}>
|
||||
<ModalToggler className={`${baseClass}__modalToggler`} slug={mainMenuSlug}>
|
||||
<Hamburger isActive={isOpen} />
|
||||
</ModalToggler>
|
||||
</div>
|
||||
</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>
|
||||
<MainMenuDrawer />
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)};
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
--gutter-h: #{base(2)};
|
||||
--nav-width: 0px;
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
|
||||
25
pnpm-lock.yaml
generated
25
pnpm-lock.yaml
generated
@@ -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==}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user