chore: overhauls admin navigation (#3339)

This commit is contained in:
Jacob Fletcher
2023-09-15 17:33:28 -04:00
committed by GitHub
parent 055c65f229
commit 85c8e4dc65
107 changed files with 1871 additions and 1740 deletions

View File

@@ -62,6 +62,8 @@
"react-i18next": "11.18.6",
"react-router-dom": "5.3.4",
"rimraf": "3.0.2",
"react-i18next": "11.18.6",
"react-router-dom": "5.3.4",
"shelljs": "0.8.5",
"ts-node": "10.9.1",
"turbo": "^1.10.13",

View File

@@ -0,0 +1,157 @@
@import '../../../scss/styles.scss';
.doc-controls {
@include blur-bg;
position: sticky;
top: 0;
width: 100%;
z-index: 1;
&::after {
content: '';
display: block;
position: absolute;
height: 1px;
background: var(--theme-elevation-100);
width: calc(100% + var(--gutter-h));
left: calc(var(--gutter-h) * -1);
top: 100%;
}
&__wrapper {
position: relative;
width: 100%;
display: flex;
align-items: center;
gap: var(--base);
padding-top: calc(var(--base) / 2);
padding-bottom: calc((var(--base) / 2) + 1px);
z-index: 1;
}
&__timestamps {
flex-grow: 1;
display: flex;
list-style: none;
padding: 0;
gap: var(--base);
margin: 0;
overflow: hidden;
}
&__timestamp {
display: flex;
align-items: center;
margin: 0;
overflow: hidden;
}
&__stamp {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0;
font-weight: 600;
}
&__label {
color: var(--theme-elevation-500);
white-space: nowrap;
}
&__controls-wrapper {
display: flex;
align-items: center;
margin: 0;
// move to the right to account for the padding on the dots
// this will make sure the alignment is correct
// while still keeping a large button hitbox
transform: translate3d(var(--base), 0, 0);
}
&__controls {
display: flex;
align-items: center;
margin: 0;
gap: calc(var(--base) / 2);
button {
margin: 0;
white-space: nowrap;
}
}
&__popup {
.popup-button {
padding: var(--base);
background: transparent;
border: none;
cursor: pointer;
color: var(--theme-elevation-500);
&:hover {
color: var(--theme-text);
}
}
}
&__dots {
display: flex;
gap: 2px;
background-color: transparent;
padding: 0;
> div {
width: 3px;
height: 3px;
border-radius: 100%;
background-color: currentColor;
}
}
&__popup-actions {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: calc(var(--base) / 4);
a {
text-decoration: none;
}
li {
position: relative;
&::before {
content: '';
display: block;
position: absolute;
height: 100%;
width: 100%;
border-radius: 1px;
background: var(--theme-elevation-100);
width: calc(100% + (var(--base) / 2));
left: calc(var(--base) / -4);
top: 0;
opacity: 0;
transition: opacity 50ms linear;
}
&:hover::before {
opacity: 1;
}
> * {
position: relative;
width: 100%;
height: 100%;
text-align: left;
font-size: inherit;
line-height: inherit;
font-family: inherit;
}
}
}
}

View File

@@ -0,0 +1,205 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import type { CollectionPermission, GlobalPermission } from '../../../../auth'
import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from '../../../../exports/types'
import { formatDate } from '../../../utilities/formatDate'
import { useConfig } from '../../utilities/Config'
import { useDocumentInfo } from '../../utilities/DocumentInfo'
import Autosave from '../Autosave'
import DeleteDocument from '../DeleteDocument'
import DuplicateDocument from '../DuplicateDocument'
import { Gutter } from '../Gutter'
import Popup from '../Popup'
import PreviewButton from '../PreviewButton'
import { Publish } from '../Publish'
import { Save } from '../Save'
import { SaveDraft } from '../SaveDraft'
import Status from '../Status'
import './index.scss'
const baseClass = 'doc-controls'
export const DocumentControls: React.FC<{
apiURL: string
collection?: SanitizedCollectionConfig
data?: any
disableActions?: boolean
global?: SanitizedGlobalConfig
hasSavePermission?: boolean
id?: string
isEditing?: boolean
permissions?: CollectionPermission | GlobalPermission
}> = (props) => {
const {
collection,
data,
disableActions,
global,
hasSavePermission,
id,
isEditing,
permissions,
} = props
const { publishedDoc } = useDocumentInfo()
const {
admin: { dateFormat },
routes: { admin: adminRoute },
} = useConfig()
const { i18n, t } = useTranslation('general')
let showPreviewButton = false
if (collection) {
showPreviewButton =
isEditing &&
collection?.admin?.preview &&
collection?.versions?.drafts &&
!collection?.versions?.drafts?.autosave
}
if (global) {
showPreviewButton =
isEditing &&
global?.admin?.preview &&
global?.versions?.drafts &&
!global?.versions?.drafts?.autosave
}
return (
<Gutter className={baseClass}>
<div className={`${baseClass}__wrapper`}>
{(collection?.versions?.drafts || global?.versions?.drafts) && (
<React.Fragment>
<Status />
{((collection?.versions?.drafts && collection?.versions?.drafts?.autosave) ||
(global?.versions?.drafts && global?.versions?.drafts?.autosave)) &&
hasSavePermission && (
<Autosave
collection={collection}
global={global}
id={id}
publishedDocUpdatedAt={publishedDoc?.updatedAt || data?.createdAt}
/>
)}
</React.Fragment>
)}
{collection?.timestamps && (
<ul className={`${baseClass}__timestamps`}>
<li
className={`${baseClass}__timestamp`}
title={data?.updatedAt ? formatDate(data?.updatedAt, dateFormat, i18n?.language) : ''}
>
<div className={`${baseClass}__label`}>{t('lastModified')}:&nbsp;</div>
{data?.updatedAt && (
<p className={`${baseClass}__stamp`}>
{formatDate(data.updatedAt, dateFormat, i18n?.language)}
</p>
)}
</li>
<li
className={`${baseClass}__timestamp`}
title={
publishedDoc?.createdAt || data?.createdAt
? formatDate(
publishedDoc?.createdAt || data?.createdAt,
dateFormat,
i18n?.language,
)
: ''
}
>
<div className={`${baseClass}__label`}>{t('created')}:&nbsp;</div>
{(publishedDoc?.createdAt || data?.createdAt) && (
<p className={`${baseClass}__stamp`}>
{formatDate(
publishedDoc?.createdAt || data?.createdAt,
dateFormat,
i18n?.language,
)}
</p>
)}
</li>
</ul>
)}
<div className={`${baseClass}__controls-wrapper`}>
<div className={`${baseClass}__controls`}>
{showPreviewButton && (
<PreviewButton
CustomComponent={collection?.admin?.components?.edit?.PreviewButton}
generatePreviewURL={collection?.admin?.preview || global?.admin?.preview}
/>
)}
{hasSavePermission && (
<React.Fragment>
{collection?.versions?.drafts || global?.versions?.drafts ? (
<React.Fragment>
{((collection?.versions?.drafts && !collection?.versions?.drafts?.autosave) ||
(global?.versions?.drafts && !global?.versions?.drafts?.autosave)) && (
<SaveDraft
CustomComponent={collection?.admin?.components?.edit?.SaveDraftButton}
/>
)}
<Publish CustomComponent={collection?.admin?.components?.edit?.PublishButton} />
</React.Fragment>
) : (
<Save CustomComponent={collection?.admin?.components?.edit?.SaveButton} />
)}
</React.Fragment>
)}
</div>
{Boolean(collection && !disableActions) && (
<Popup
button={
<div className={`${baseClass}__dots`}>
<div />
<div />
<div />
</div>
}
caret={false}
className={`${baseClass}__popup`}
horizontalAlign="center"
size="large"
verticalAlign="bottom"
>
<ul className={`${baseClass}__popup-actions`}>
{'create' in permissions && permissions?.create?.permission && (
<React.Fragment>
<li>
<Link
id="action-create"
to={`${adminRoute}/collections/${collection?.slug}/create`}
>
{t('createNew')}
</Link>
</li>
{!collection?.admin?.disableDuplicate && isEditing && (
<li>
<DuplicateDocument
collection={collection}
id={id}
slug={collection?.slug}
/>
</li>
)}
</React.Fragment>
)}
{'delete' in permissions && permissions?.delete?.permission && id && (
<li>
<DeleteDocument buttonId="action-delete" collection={collection} id={id} />
</li>
)}
</ul>
</Popup>
)}
</div>
</div>
</Gutter>
)
}

View File

@@ -135,7 +135,6 @@ const Content: React.FC<DocumentDrawerProps> = ({
),
data,
disableActions: true,
disableEyebrow: true,
disableLeaveWithoutSaving: true,
hasSavePermission,
id,

View File

@@ -2,6 +2,7 @@
.doc-drawer {
&__header {
width: 100%;
margin-top: base(2.5);
margin-bottom: base(1);

View File

@@ -0,0 +1,76 @@
@import '../../../../scss/styles.scss';
.doc-tabs {
display: flex;
gap: calc(var(--base) / 2);
list-style: none;
align-items: center;
margin: 0;
&__tab-link {
@extend %h5;
text-decoration: none;
position: relative;
display: flex;
justify-content: center;
align-items: center;
padding: calc(var(--base) / 2) calc(var(--base));
&:focus:not(:focus-visible) {
opacity: 1;
}
&::before {
content: '';
display: block;
position: absolute;
width: 100%;
height: 100%;
border-radius: 2px;
background-color: var(--theme-elevation-50);
opacity: 0;
}
&:hover {
&::before {
opacity: 1;
}
.doc-tabs__count {
background-color: var(--theme-elevation-150);
}
}
}
&__tab--active {
.doc-tabs__tab-link {
&::before {
opacity: 1;
background-color: var(--theme-elevation-100);
}
}
.doc-tabs__count {
background-color: var(--theme-elevation-250);
}
&:hover {
.doc-tabs__count {
background-color: var(--theme-elevation-250);
}
}
}
&__tab-label {
position: relative;
display: flex;
align-items: center;
gap: 4px;
}
&__count {
padding: 0px 6px;
background-color: var(--theme-elevation-100);
border-radius: 1px;
}
}

View File

@@ -0,0 +1,140 @@
import React, { Fragment } from 'react'
import { useTranslation } from 'react-i18next'
import { Link, useLocation, useRouteMatch } from 'react-router-dom'
import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from '../../../../../exports/types'
import { useConfig } from '../../../utilities/Config'
import { useDocumentInfo } from '../../../utilities/DocumentInfo'
import './index.scss'
const baseClass = 'doc-tabs'
const baseTabs = [
{
label: 'Edit',
path: '',
},
{
label: 'Versions',
path: 'versions',
},
]
export const DocumentTabs: React.FC<{
apiURL: string
collection?: SanitizedCollectionConfig
global?: SanitizedGlobalConfig
id: string
isEditing?: boolean
}> = (props) => {
const { apiURL, collection, global, id, isEditing } = props
const match = useRouteMatch()
const location = useLocation()
const { t } = useTranslation('general')
const {
routes: { admin },
} = useConfig()
const { versions } = useDocumentInfo()
const tabs = [
...baseTabs,
// TODO: extract overrides and custom views from collection config
]
let docURL: string
let versionsURL: string
let editTabActive = false
if (collection) {
docURL = `${admin}/collections/${collection.slug}/${id}`
versionsURL = `${docURL}/versions`
editTabActive =
location.pathname === `${admin}/collections/${collection.slug}` ||
location.pathname === `${admin}/collections/${collection.slug}/create` ||
location.pathname === docURL
}
if (global) {
docURL = `${admin}/globals/${global.slug}`
versionsURL = `${docURL}/versions`
editTabActive =
location.pathname === `${admin}/globals/${global.slug}` || location.pathname === docURL
}
// Don't show tabs when creating new documents
if ((tabs && collection && isEditing) || global) {
return (
<ul className={baseClass}>
<li
className={[`${baseClass}__tab`, editTabActive && `${baseClass}__tab--active`]
.filter(Boolean)
.join(' ')}
>
<Link className={`${baseClass}__tab-link`} to={docURL}>
<div className={`${baseClass}__tab-label`}>{t('edit')}</div>
</Link>
</li>
{(collection?.versions || global?.versions) && (
<li
className={[
`${baseClass}__tab`,
location.pathname.startsWith(versionsURL) && `${baseClass}__tab--active`,
]
.filter(Boolean)
.join(' ')}
>
<Link className={`${baseClass}__tab-link`} to={versionsURL}>
<div className={`${baseClass}__tab-label`}>
{t('version:versions')}
{typeof versions?.totalDocs === 'number' && versions?.totalDocs > 0 && (
<Fragment>
&nbsp;
<span className={`${baseClass}__count`}>{versions?.totalDocs}</span>
</Fragment>
)}
</div>
</Link>
</li>
)}
{(!collection?.admin?.hideAPIURL || !global?.admin?.hideAPIURL) && (
<li
className={[`${baseClass}__tab`, match.url === apiURL && `${baseClass}__tab--active`]
.filter(Boolean)
.join(' ')}
>
<Link
className={`${baseClass}__tab-link`}
rel="noopener noreferrer"
target="_blank"
to={apiURL}
>
<div className={`${baseClass}__tab-label`}>API</div>
</Link>
</li>
)}
{/* {tabs.map((tab) => {
const tabHref = `${match.url}${tab.path ? `/${tab.path}` : ''}`
const isActive = location.pathname === tabHref
return (
<li
className={[`${baseClass}__tab`, isActive && `${baseClass}__tab--active`]
.filter(Boolean)
.join(' ')}
key={tab.label}
>
<Link className={`${baseClass}__tab-link`} to={tabHref}>
<span className={`${baseClass}__tab-label`}>{tab.label}</span>
</Link>
</li>
)
})} */}
</ul>
)
}
return null
}

View File

@@ -0,0 +1,33 @@
@import '../../../scss/styles.scss';
.doc-header {
width: 100%;
padding-bottom: var(--base);
display: flex;
align-items: center;
position: relative;
&::after {
content: '';
display: block;
position: absolute;
height: 1px;
background: var(--theme-elevation-100);
width: calc(100% + var(--gutter-h));
left: calc(var(--gutter-h) * -1);
top: calc(100% - 1px);
}
&__title {
flex-grow: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.25;
margin: 0;
}
@include mid-break {
padding-bottom: calc(var(--base) / 2);
}
}

View File

@@ -0,0 +1,50 @@
import React, { Fragment } from 'react'
import { useTranslation } from 'react-i18next'
import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from '../../../../exports/types'
import { Gutter } from '../Gutter'
import RenderTitle from '../RenderTitle'
import { DocumentTabs } from './Tabs'
import './index.scss'
const baseClass = `doc-header`
export const DocumentHeader: React.FC<{
apiURL: string
collection?: SanitizedCollectionConfig
customHeader?: React.ReactNode
data?: any
global?: SanitizedGlobalConfig
id?: string
isEditing?: boolean
}> = (props) => {
const { apiURL, collection, customHeader, data, global, id, isEditing } = props
const { t } = useTranslation('general')
return (
<Gutter className={baseClass}>
{customHeader && customHeader}
{!customHeader && (
<Fragment>
{collection && (
<RenderTitle
className={`${baseClass}__title`}
collection={collection}
data={data}
fallback={`[${t('untitled')}]`}
useAsTitle={collection?.admin?.useAsTitle}
/>
)}
{global && <h1 className={`${baseClass}__title`}>{global?.slug}</h1>}
<DocumentTabs
apiURL={apiURL}
collection={collection}
global={global}
id={id}
isEditing={isEditing}
/>
</Fragment>
)}
</Gutter>
)
}

View File

@@ -143,11 +143,7 @@ const EditMany: React.FC<Props> = (props) => {
<div className={`${baseClass}__sidebar`}>
<div className={`${baseClass}__sidebar-sticky-wrap`}>
<div className={`${baseClass}__document-actions`}>
<Submit
action={`${serverURL}${api}/${slug}${getQueryParams()}`}
disabled={selected.length === 0}
/>
{collection.versions && (
{collection.versions ? (
<React.Fragment>
<Publish
action={`${serverURL}${api}/${slug}${getQueryParams()}`}
@@ -158,6 +154,11 @@ const EditMany: React.FC<Props> = (props) => {
disabled={selected.length === 0}
/>
</React.Fragment>
) : (
<Submit
action={`${serverURL}${api}/${slug}${getQueryParams()}`}
disabled={selected.length === 0}
/>
)}
</div>
</div>

View File

@@ -4,8 +4,12 @@ import './index.scss'
const baseClass = 'id-label'
const IDLabel: React.FC<{ id: string; prefix?: string }> = ({ id, prefix = 'ID:' }) => (
<div className={baseClass}>
const IDLabel: React.FC<{ className?: string; id: string; prefix?: string }> = ({
className,
id,
prefix = 'ID:',
}) => (
<div className={[baseClass, className].filter(Boolean).join(' ')} title={id}>
{prefix}
&nbsp;&nbsp;
{id}

View File

@@ -239,7 +239,6 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
</header>
),
data,
disableEyebrow: true,
handlePageChange: setPage,
handlePerPageChange: setLimit,
handleSortChange: setSort,

View File

@@ -2,9 +2,18 @@
.localizer {
position: relative;
padding: base(0.125) base(1.5) base(0.125) 0;
[dir='rtl'] & {
padding: base(0.125) 0 base(0.125) base(1.5);
display: flex;
align-items: center;
flex-wrap: nowrap;
&__label {
color: var(--theme-elevation-500);
}
&__chevron {
.stroke {
stroke: var(--theme-elevation-500);
}
}
button {
@@ -28,6 +37,10 @@
}
}
&__button {
white-space: nowrap;
}
span {
color: var(--theme-elevation-400);
}

View File

@@ -3,6 +3,7 @@ import React from 'react'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import { Chevron } from '../..'
import { useConfig } from '../../utilities/Config'
import { useLocale } from '../../utilities/Locale'
import { useSearchParams } from '../../utilities/SearchParams'
@@ -11,7 +12,10 @@ import './index.scss'
const baseClass = 'localizer'
const Localizer: React.FC = () => {
const Localizer: React.FC<{
className?: string
}> = (props) => {
const { className } = props
const config = useConfig()
const { localization } = config
@@ -23,13 +27,19 @@ const Localizer: React.FC = () => {
const { locales } = localization
return (
<div className={baseClass}>
<div className={[baseClass, className].filter(Boolean).join(' ')}>
<div className={`${baseClass}__label`}>{`${t('locale')}:`}</div>
&nbsp;&nbsp;
<Popup
button={locale.label}
button={
<div className={`${baseClass}__button`}>
{`${locale.label}`}
<Chevron className={`${baseClass}__chevron`} />
</div>
}
caret={false}
horizontalAlign="left"
render={({ close }) => (
<div>
<span>{t('locales')}</span>
<ul>
{locales.map((localeOption) => {
const baseLocaleClass = `${baseClass}__locale`
@@ -53,6 +63,7 @@ const Localizer: React.FC = () => {
<li className={localeClasses} key={localeOption.code}>
<Link onClick={close} to={{ search }}>
{localeOption.label}
{localeOption.label !== localeOption.code && ` (${localeOption.code})`}
</Link>
</li>
)
@@ -61,7 +72,6 @@ const Localizer: React.FC = () => {
return null
})}
</ul>
</div>
)}
showScrollbar
/>

View File

@@ -1,17 +1,15 @@
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 { 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'
@@ -20,7 +18,7 @@ const baseClass = 'main-menu'
export const mainMenuSlug = 'main-menu'
export const MainMenuDrawer: React.FC = () => {
export const MainMenu: React.FC = () => {
const { permissions, user } = useAuth()
const { closeModal, modalState } = useModal()
@@ -129,14 +127,6 @@ export const MainMenuDrawer: React.FC = () => {
{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>
@@ -144,7 +134,7 @@ export const MainMenuDrawer: React.FC = () => {
<button
aria-label={t('close')}
className={`${baseClass}__close`}
id={`close-drawer__${mainMenuSlug}`}
id={`close__${mainMenuSlug}`}
onClick={() => closeModal(mainMenuSlug)}
type="button"
/>

View File

@@ -1,14 +1,10 @@
@import '../../../scss/styles.scss';
.nav {
position: sticky;
top: 0;
position: relative;
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;
z-index: var(--z-modal);
&__bg {
@include blur-bg;
@@ -31,19 +27,34 @@
// but reenable them on the modal toggler
&--main-menu-open {
pointer-events: none;
z-index: calc(var(--z-modal) + 2);
.nav__modalToggler {
pointer-events: all;
}
.nav__nav-wrapper {
display: none;
}
}
&__content {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
padding: 0 calc(var(--gutter-h) / 2);
position: relative;
flex-grow: 1;
}
&__nav-wrapper {
margin-left: calc(var(--base) / 1.25);
display: flex;
gap: calc(var(--base) / 2);
align-items: center;
height: 100%;
flex-grow: 1;
justify-content: space-between;
}
&__modalToggler {
@@ -52,7 +63,6 @@
border: 0;
padding: 0;
cursor: pointer;
z-index: 999999;
transform: translate3d(-50%, 0, 0);
&:focus {
@@ -60,24 +70,46 @@
}
}
&__scrollable {
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
&__account {
transform: translate3d(50%, 0, 0);
&:focus:not(:focus-visible) {
opacity: 1;
}
}
&__controls {
display: flex;
align-items: center;
gap: calc(var(--base) / 2);
flex-grow: 1;
justify-content: flex-end;
}
// the icon has padding so we need to transform slightly
// so that it's bounding box aligns with the other elements
// this can be removed if we ever reformat our SVG icons
&__localizer {
transform: translate3d(8px, 0, 0);
}
@include mid-break {
height: calc(var(--base) * 2);
}
@include small-break {
.nav {
&__content {
padding: 0 var(--gutter-h);
}
&__modalToggler {
&__modalToggler,
&__account {
transform: unset;
}
&__step-nav {
display: none;
}
}
}
}

View File

@@ -1,11 +1,15 @@
import { ModalToggler, useModal } from '@faceless-ui/modal'
import React, { Fragment, useEffect } from 'react'
import { useHistory } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { Link, useHistory } from 'react-router-dom'
import Account from '../../graphics/Account'
import { useConfig } from '../../utilities/Config'
import RenderCustomComponent from '../../utilities/RenderCustomComponent'
import Hamburger from '../Hamburger'
import { MainMenuDrawer, mainMenuSlug } from '../MainMenu'
import Localizer from '../Localizer'
import { MainMenu, mainMenuSlug } from '../MainMenu'
import StepNav from '../StepNav'
import './index.scss'
const baseClass = 'nav'
@@ -13,7 +17,12 @@ const baseClass = 'nav'
const DefaultNav = () => {
const history = useHistory()
const { closeModal, isModalOpen } = useModal()
const isOpen = isModalOpen(mainMenuSlug)
const { t } = useTranslation()
const isMainMenuOpen = isModalOpen(mainMenuSlug)
const {
routes: { admin: adminRoute },
} = useConfig()
useEffect(
() =>
@@ -28,8 +37,8 @@ const DefaultNav = () => {
<header
className={[
baseClass,
!isOpen && `${baseClass}--show-bg`,
isOpen && `${baseClass}--main-menu-open`,
!isMainMenuOpen && `${baseClass}--show-bg`,
isMainMenuOpen && `${baseClass}--main-menu-open`,
]
.filter(Boolean)
.join(' ')}
@@ -37,11 +46,24 @@ const DefaultNav = () => {
<div className={`${baseClass}__bg`} />
<div className={`${baseClass}__content`}>
<ModalToggler className={`${baseClass}__modalToggler`} slug={mainMenuSlug}>
<Hamburger isActive={isOpen} />
<Hamburger isActive={isMainMenuOpen} />
</ModalToggler>
<div className={`${baseClass}__nav-wrapper`}>
<StepNav className={`${baseClass}__step-nav`} />
<div className={`${baseClass}__controls`}>
<Localizer className={`${baseClass}__localizer`} />
<Link
aria-label={t('authentication:account')}
className={`${baseClass}__account`}
to={`${adminRoute}/account`}
>
<Account />
</Link>
</div>
</div>
</div>
</header>
<MainMenuDrawer />
<MainMenu />
</Fragment>
)
}

View File

@@ -12,6 +12,7 @@
z-index: var(--z-popup);
max-width: calc(100vw - #{$baseline});
&--caret {
&:after {
content: ' ';
position: absolute;
@@ -20,6 +21,7 @@
border-top-color: var(--theme-input-bg);
}
}
}
&__wrap {
overflow: hidden;
@@ -80,7 +82,7 @@
}
.popup__scroll {
padding: base(1.5) calc(var(--scrollbar-width) + #{base(1.5)}) base(1) base(1.5);
padding: base(1) calc(var(--scrollbar-width) + #{base(1.5)}) base(1) base(1.5);
}
}

View File

@@ -15,6 +15,7 @@ const Popup: React.FC<Props> = (props) => {
button,
buttonClassName,
buttonType = 'default',
caret = true,
children,
className,
color = 'light',
@@ -157,7 +158,12 @@ const Popup: React.FC<Props> = (props) => {
)}
</div>
<div className={`${baseClass}__content`} ref={contentRef}>
<div
className={[`${baseClass}__content`, caret && `${baseClass}__content--caret`]
.filter(Boolean)
.join(' ')}
ref={contentRef}
>
<div className={`${baseClass}__wrap`} ref={intersectionRef}>
<div
className={`${baseClass}__scroll`}

View File

@@ -6,6 +6,7 @@ export type Props = {
button?: React.ReactNode
buttonClassName?: string
buttonType?: 'custom' | 'default' | 'none'
caret?: boolean
children?: React.ReactNode
className?: string
color?: 'dark' | 'light'

View File

@@ -29,7 +29,13 @@ const DefaultPreviewButton: React.FC<DefaultPreviewButtonProps> = ({
preview,
}) => {
return (
<Button buttonStyle="secondary" className={baseClass} disabled={disabled} onClick={preview}>
<Button
buttonStyle="secondary"
className={baseClass}
disabled={disabled}
onClick={preview}
size="small"
>
{label}
</Button>
)

View File

@@ -13,16 +13,18 @@ export type CustomPublishButtonProps = React.ComponentType<
>
export type DefaultPublishButtonProps = {
disabled: boolean
id?: string
label: string
publish: () => void
}
const DefaultPublishButton: React.FC<DefaultPublishButtonProps> = ({
disabled,
id,
label,
publish,
}) => {
return (
<FormSubmit disabled={disabled} onClick={publish} type="button">
<FormSubmit buttonId={id} disabled={disabled} onClick={publish} size="small" type="button">
{label}
</FormSubmit>
)
@@ -31,6 +33,7 @@ const DefaultPublishButton: React.FC<DefaultPublishButtonProps> = ({
type Props = {
CustomComponent?: CustomPublishButtonProps
}
export const Publish: React.FC<Props> = ({ CustomComponent }) => {
const { publishedDoc, unpublishedVersions } = useDocumentInfo()
const { submit } = useForm()
@@ -55,6 +58,7 @@ export const Publish: React.FC<Props> = ({ CustomComponent }) => {
componentProps={{
DefaultButton: DefaultPublishButton,
disabled: !canPublish,
id: 'action-save',
label: t('publishChanges'),
publish,
}}

View File

@@ -0,0 +1,9 @@
@import '../../../scss/styles.scss';
.render-title {
&__id {
vertical-align: middle;
position: relative;
top: -3px;
}
}

View File

@@ -4,11 +4,20 @@ import type { Props } from './types'
import useTitle from '../../../hooks/useTitle'
import IDLabel from '../IDLabel'
import './index.scss'
const baseClass = 'render-title'
const RenderTitle: React.FC<Props> = (props) => {
const { collection, data, fallback = '[untitled]', title: titleFromProps } = props
const {
className,
collection,
data,
element = 'h1',
fallback = '[untitled]',
title: titleFromProps,
} = props
const titleFromForm = useTitle(collection)
let title = titleFromForm
@@ -18,11 +27,17 @@ const RenderTitle: React.FC<Props> = (props) => {
const idAsTitle = title === data?.id
if (idAsTitle) {
return <IDLabel id={data?.id} />
}
const Tag = element
return <span className={baseClass}>{title}</span>
return (
<Tag
className={[className, baseClass, idAsTitle && `${baseClass}--has-id`]
.filter(Boolean)
.join(' ')}
>
{idAsTitle ? <IDLabel className={`${baseClass}__id`} id={data?.id} /> : title}
</Tag>
)
}
export default RenderTitle

View File

@@ -1,11 +1,15 @@
import type { SanitizedCollectionConfig } from '../../../../collections/config/types'
import type { SanitizedGlobalConfig } from '../../../../exports/types'
export type Props = {
className?: string
collection?: SanitizedCollectionConfig
data?: {
id?: string
}
element?: React.ElementType
fallback?: string
global?: SanitizedGlobalConfig
title?: string
useAsTitle?: string
}

View File

@@ -49,6 +49,7 @@ const DefaultSaveDraftButton: React.FC<DefaultSaveDraftButtonProps> = ({
disabled={disabled}
onClick={saveDraft}
ref={ref}
size="small"
type="button"
>
{label}

View File

@@ -2,13 +2,17 @@
.status {
&__label {
color: gray;
color: var(--theme-elevation-500);
}
&__value {
font-weight: 600;
}
&__value-wrap {
white-space: nowrap;
}
&__action {
text-decoration: underline;
}

View File

@@ -130,8 +130,9 @@ const Status: React.FC = () => {
if (statusToRender) {
return (
<div className={baseClass}>
<div className={baseClass} title={`${t('status')}: ${t(statusToRender)}`}>
<div className={`${baseClass}__value-wrap`}>
<span className={`${baseClass}__label`}>{t('status')}:&nbsp;</span>
<span className={`${baseClass}__value`}>{t(statusToRender)}</span>
{canUpdate && statusToRender === 'published' && (
<React.Fragment>

View File

@@ -3,35 +3,19 @@
.step-nav {
display: flex;
overflow: auto;
gap: calc(var(--base) / 2);
* {
display: block;
}
a {
[dir='ltr'] & {
margin-right: base(0.25);
}
[dir='rtl'] & {
margin-left: base(0.25);
}
border: 0;
display: flex;
align-items: center;
font-weight: 600;
text-decoration: none;
svg {
[dir='ltr'] & {
margin-left: base(0.25);
transform: rotate(-90deg);
}
[dir='rtl'] & {
margin-right: base(0.25);
transform: rotate(90deg);
}
}
label {
cursor: pointer;
}

View File

@@ -1,4 +1,4 @@
import React, { createContext, useContext, useState } from 'react'
import React, { Fragment, createContext, useContext, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
@@ -28,7 +28,10 @@ const StepNavProvider: React.FC<{ children?: React.ReactNode }> = ({ children })
const useStepNav = (): ContextType => useContext(Context)
const StepNav: React.FC = () => {
const StepNav: React.FC<{
className?: string
}> = (props) => {
const { className } = props
const { i18n, t } = useTranslation()
const dashboardLabel = <span>{t('general:dashboard')}</span>
const { stepNav } = useStepNav()
@@ -38,12 +41,12 @@ const StepNav: React.FC = () => {
} = config
return (
<nav className="step-nav">
<nav className={['step-nav', className].filter(Boolean).join(' ')}>
{stepNav.length > 0 ? (
<Link to={admin}>
{dashboardLabel}
<Chevron />
</Link>
<Fragment>
<Link to={admin}>{dashboardLabel}</Link>
<span>/</span>
</Fragment>
) : (
dashboardLabel
)}
@@ -54,10 +57,10 @@ const StepNav: React.FC = () => {
stepNav.length === i + 1 ? (
StepLabel
) : (
<Link key={i} to={item.url}>
{StepLabel}
<Chevron />
</Link>
<Fragment key={i}>
<Link to={item.url}>{StepLabel}</Link>
<span>/</span>
</Fragment>
)
return Step

View File

@@ -1,10 +0,0 @@
@import '../../../scss/styles.scss';
.versions-count__button {
font-weight: 600;
&:hover,
&:focus-visible {
text-decoration: underline;
}
}

View File

@@ -1,45 +0,0 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import type { Props } from './types'
import { useConfig } from '../../utilities/Config'
import { useDocumentInfo } from '../../utilities/DocumentInfo'
import Button from '../Button'
import './index.scss'
const baseClass = 'versions-count'
const VersionsCount: React.FC<Props> = ({ collection, global, id }) => {
const {
routes: { admin },
} = useConfig()
const { versions } = useDocumentInfo()
const { t } = useTranslation('version')
let versionsURL: string
if (collection) {
versionsURL = `${admin}/collections/${collection.slug}/${id}/versions`
}
if (global) {
versionsURL = `${admin}/globals/${global.slug}/versions`
}
const versionCount = versions?.totalDocs || 0
return (
<div className={baseClass}>
{versionCount === 0 && t('versionCount_none')}
{versionCount > 0 && (
<Button buttonStyle="none" className={`${baseClass}__button`} el="link" to={versionsURL}>
{t(versionCount === 1 ? 'versionCount_one' : 'versionCount_many', {
count: versionCount,
})}
</Button>
)}
</div>
)
}
export default VersionsCount

View File

@@ -1,8 +0,0 @@
import type { SanitizedCollectionConfig } from '../../../../collections/config/types'
import type { SanitizedGlobalConfig } from '../../../../globals/config/types'
export type Props = {
collection?: SanitizedCollectionConfig
global?: SanitizedGlobalConfig
id?: number | string
}

View File

@@ -0,0 +1,48 @@
.graphic-account {
&__bg {
fill: var(--theme-elevation-100);
}
&__head,
&__body {
fill: var(--theme-elevation-300);
}
&--active {
.graphic-account {
&__bg {
fill: var(--theme-elevation-500);
}
&__head,
&__body {
fill: var(--theme-text);
}
}
}
&:hover:not(.graphic-account--active) {
.graphic-account {
&__bg {
fill: var(--theme-elevation-200);
}
&__head,
&__body {
fill: var(--theme-elevation-600);
}
}
}
}
[data-theme='light'] {
.graphic-account {
&--active {
.graphic-account {
&__bg {
fill: var(--theme-elevation-300);
}
}
}
}
}

View File

@@ -0,0 +1,24 @@
import React from 'react'
import './index.scss'
const baseClass = 'graphic-account'
export const DefaultAccountIcon: React.FC<{
active: boolean
}> = (props) => (
<svg
className={[baseClass, props?.active && `${baseClass}--active`].filter(Boolean).join(' ')}
height="25"
viewBox="0 0 25 25"
width="25"
xmlns="http://www.w3.org/2000/svg"
>
<circle className={`${baseClass}__bg`} cx="12.5" cy="12.5" r="11.5" />
<circle className={`${baseClass}__head`} cx="12.5" cy="10.73" r="3.98" />
<path
className={`${baseClass}__body`}
d="M12.5,24a11.44,11.44,0,0,0,7.66-2.94c-.5-2.71-3.73-4.8-7.66-4.8s-7.16,2.09-7.66,4.8A11.44,11.44,0,0,0,12.5,24Z"
/>
</svg>
)

View File

@@ -2,7 +2,7 @@ import md5 from 'md5'
import qs from 'qs'
import React from 'react'
import { useAuth } from '../../utilities/Auth'
import { useAuth } from '../../../utilities/Auth'
const Gravatar: React.FC = () => {
const { user } = useAuth()

View File

@@ -1,44 +1,25 @@
import React from 'react'
import { useLocation } from 'react-router-dom'
import { useAuth } from '../../utilities/Auth'
import { useConfig } from '../../utilities/Config'
import { DefaultAccountIcon } from './Default'
import Gravatar from './Gravatar'
const css = `
.graphic-account .circle1 {
fill: var(--theme-elevation-100);
}
.graphic-account .circle2, .graphic-account path {
fill: var(--theme-elevation-300);
}
`
const Default: React.FC = () => (
<svg
className="graphic-account"
height="25"
viewBox="0 0 25 25"
width="25"
xmlns="http://www.w3.org/2000/svg"
>
<style>{css}</style>
<circle className="circle1" cx="12.5" cy="12.5" r="11.5" />
<circle className="circle2" cx="12.5" cy="10.73" r="3.98" />
<path d="M12.5,24a11.44,11.44,0,0,0,7.66-2.94c-.5-2.71-3.73-4.8-7.66-4.8s-7.16,2.09-7.66,4.8A11.44,11.44,0,0,0,12.5,24Z" />
</svg>
)
const Account = () => {
const {
admin: { avatar: Avatar },
routes: { admin: adminRoute },
} = useConfig()
const { user } = useAuth()
const location = useLocation()
if (!user.email || Avatar === 'default') return <Default />
const isOnAccountPage = location.pathname === `${adminRoute}/account`
if (!user.email || Avatar === 'default') return <DefaultAccountIcon active={isOnAccountPage} />
if (Avatar === 'gravatar') return <Gravatar />
if (Avatar) return <Avatar />
return <Default />
if (Avatar) return <Avatar active={isOnAccountPage} />
return <DefaultAccountIcon active={isOnAccountPage} />
}
export default Account

View File

@@ -1,26 +1,20 @@
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import type { Translation } from '../../../../translations/type'
import type { Props } from './types'
import { formatDate } from '../../../utilities/formatDate'
import CopyToClipboard from '../../elements/CopyToClipboard'
import Eyebrow from '../../elements/Eyebrow'
import { DocumentControls } from '../../elements/DocumentControls'
import { DocumentHeader } from '../../elements/DocumentHeader'
import { Gutter } from '../../elements/Gutter'
import { LoadingOverlayToggle } from '../../elements/Loading'
import PreviewButton from '../../elements/PreviewButton'
import ReactSelect from '../../elements/ReactSelect'
import RenderTitle from '../../elements/RenderTitle'
import { Save } from '../../elements/Save'
import Form from '../../forms/Form'
import Label from '../../forms/Label'
import RenderFields from '../../forms/RenderFields'
import fieldTypes from '../../forms/field-types'
import LeaveWithoutSaving from '../../modals/LeaveWithoutSaving'
import { useAuth } from '../../utilities/Auth'
import { useConfig } from '../../utilities/Config'
import Meta from '../../utilities/Meta'
import { OperationContext } from '../../utilities/OperationProvider'
import Auth from '../collections/Edit/Auth'
@@ -42,19 +36,9 @@ const DefaultAccount: React.FC<Props> = (props) => {
permissions,
} = props
const {
admin: { preview, useAsTitle },
auth,
fields,
slug,
timestamps,
} = collection
const { auth, fields } = collection
const { refreshCookieAsync } = useAuth()
const {
admin: { dateFormat },
routes: { admin },
} = useConfig()
const { i18n, t } = useTranslation('authentication')
const languageOptions = Object.entries(i18n.options.resources).map(([language, resource]) => ({
@@ -74,8 +58,8 @@ const DefaultAccount: React.FC<Props> = (props) => {
return (
<React.Fragment>
<LoadingOverlayToggle name="account" show={isLoading} type="withoutNav" />
<div className={classes}>
{!isLoading && (
<div className={classes}>
<OperationContext.Provider value="update">
<Form
action={action}
@@ -85,26 +69,25 @@ const DefaultAccount: React.FC<Props> = (props) => {
method="patch"
onSuccess={onSave}
>
<DocumentHeader apiURL={apiURL} collection={collection} data={data} />
<DocumentControls
apiURL={apiURL}
collection={collection}
data={data}
hasSavePermission={hasSavePermission}
permissions={permissions}
/>
<div className={`${baseClass}__main`}>
<Meta
description={t('accountOfCurrentUser')}
keywords={t('account')}
title={t('account')}
/>
<Eyebrow />
{!(collection.versions?.drafts && collection.versions?.drafts?.autosave) && (
<LeaveWithoutSaving />
)}
<div className={`${baseClass}__edit`}>
<Gutter className={`${baseClass}__header`}>
<h1>
<RenderTitle
collection={collection}
data={data}
fallback={`[${t('general:untitled')}]`}
useAsTitle={useAsTitle}
/>
</h1>
<Auth
collection={collection}
email={data?.email}
@@ -138,33 +121,6 @@ const DefaultAccount: React.FC<Props> = (props) => {
<div className={`${baseClass}__sidebar-wrap`}>
<div className={`${baseClass}__sidebar`}>
<div className={`${baseClass}__sidebar-sticky-wrap`}>
<ul className={`${baseClass}__collection-actions`}>
{permissions?.create?.permission && (
<React.Fragment>
<li>
<Link to={`${admin}/collections/${slug}/create`}>
{t('general:createNew')}
</Link>
</li>
</React.Fragment>
)}
</ul>
<div
className={`${baseClass}__document-actions${
preview ? ` ${baseClass}__document-actions--with-preview` : ''
}`}
>
{preview &&
(!collection.versions?.drafts || collection.versions?.drafts?.autosave) && (
<PreviewButton
CustomComponent={collection?.admin?.components?.edit?.PreviewButton}
generatePreviewURL={preview}
/>
)}
{hasSavePermission && (
<Save CustomComponent={collection?.admin?.components?.edit?.SaveButton} />
)}
</div>
<div className={`${baseClass}__sidebar-fields`}>
<RenderFields
fieldSchema={fields}
@@ -174,45 +130,13 @@ const DefaultAccount: React.FC<Props> = (props) => {
readOnly={!hasSavePermission}
/>
</div>
<ul className={`${baseClass}__meta`}>
<li className={`${baseClass}__api-url`}>
<span className={`${baseClass}__label`}>
API URL <CopyToClipboard value={apiURL} />
</span>
<a href={apiURL} rel="noopener noreferrer" target="_blank">
{apiURL}
</a>
</li>
<li>
<div className={`${baseClass}__label`}>ID</div>
<div>{data?.id}</div>
</li>
{timestamps && (
<React.Fragment>
{data.updatedAt && (
<li>
<div className={`${baseClass}__label`}>
{t('general:lastModified')}
</div>
<div>{formatDate(data.updatedAt, dateFormat, i18n?.language)}</div>
</li>
)}
{data.createdAt && (
<li>
<div className={`${baseClass}__label`}>{t('general:created')}</div>
<div>{formatDate(data.createdAt, dateFormat, i18n?.language)}</div>
</li>
)}
</React.Fragment>
)}
</ul>
</div>
</div>
</div>
</Form>
</OperationContext.Provider>
)}
</div>
)}
</React.Fragment>
)
}

View File

@@ -2,183 +2,24 @@
.account {
width: 100%;
padding-bottom: calc(var(--base) * 4);
&__form {
height: 100%;
}
&__main {
width: calc(100% - #{base(15)});
display: flex;
flex-direction: column;
min-height: 100%;
&__edit {
margin-top: calc(var(--base) * 3);
}
&__header {
h1 {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
&__collection-actions {
list-style: none;
margin: 0;
padding: base(1.5) 0 base(0.5);
display: flex;
li {
[dir='ltr'] & {
margin-right: base(0.75);
}
[dir='rtl'] & {
margin-left: base(0.75);
}
}
}
&__edit {
padding: base(1) 0 base(2);
flex-grow: 1;
}
&__sidebar-wrap {
position: fixed;
width: base(15);
height: 100%;
overflow: visible;
[dir='ltr'] & {
top: 0;
right: 0;
border-left: 1px solid var(--theme-elevation-100);
}
[dir='rtl'] & {
top: 0;
left: 0;
border-right: 1px solid var(--theme-elevation-100);
}
}
&__sidebar {
width: 100%;
height: 100%;
overflow-y: auto;
}
&__sidebar-sticky-wrap {
display: flex;
flex-direction: column;
min-height: 100%;
}
&__collection-actions,
&__document-actions,
&__meta,
&__sidebar-fields {
[dir='ltr'] & {
padding-left: base(1.5);
}
[dir='rtl'] & {
padding-right: base(1.5);
}
}
&__document-actions {
@include blur-bg;
position: sticky;
top: 0;
z-index: 2;
padding-right: $baseline;
> * {
position: relative;
z-index: 1;
}
}
&__document-actions--with-preview {
display: flex;
> * {
width: calc(50% - #{base(0.5)});
}
> *:first-child {
[dir='ltr'] & {
margin-right: base(0.5);
}
[dir='rtl'] & {
margin-left: base(0.5);
}
}
> *:last-child {
[dir='ltr'] & {
margin-left: base(0.5);
}
[dir='rtl'] & {
margin-right: base(0.5);
}
}
.form-submit {
.btn {
width: 100%;
padding-left: base(2);
padding-right: base(2);
}
}
}
&__api-url {
[dir='ltr'] & {
margin-bottom: base(1.5);
padding-right: base(1.5);
}
[dir='rtl'] & {
margin-bottom: base(1.5);
padding-left: base(1.5);
}
a {
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
}
&__meta {
margin: auto 0 $baseline;
padding-top: $baseline;
list-style: none;
li {
margin-bottom: base(0.5);
}
}
&__label {
color: var(--theme-elevation-400);
}
&__collection-actions,
&__api-url {
a,
button {
cursor: pointer;
font-weight: 600;
text-decoration: none;
&:hover,
&:focus-visible {
text-decoration: underline;
}
}
gap: var(--base);
}
&__payload-settings {
margin-top: base(4);
margin-top: base(3);
padding-top: base(3);
border-top: 1px solid var(--theme-elevation-100);
}
@@ -187,81 +28,14 @@
margin-bottom: $baseline;
}
@include mid-break {
&__main {
width: 100%;
min-height: initial;
}
&__sidebar-wrap {
position: static;
width: 100%;
height: initial;
}
&__form {
display: block;
}
@include small-break {
&__edit {
padding: 0;
}
&__document-actions {
position: fixed;
[dir='ltr'] & {
bottom: 0;
left: 0;
right: 0;
top: auto;
}
[dir='rtl'] & {
bottom: 0;
left: 0;
right: 0;
top: auto;
}
}
&__meta {
padding: base(2) 0 base(1.5);
margin: 0;
}
&__collection-actions,
&__document-actions,
&__meta,
&__sidebar-fields {
[dir='ltr'] & {
padding-left: var(--gutter-h);
}
[dir='rtl'] & {
padding-right: var(--gutter-h);
}
}
&__api-url {
margin-bottom: base(0.5);
}
&__collection-actions {
border-top: 1px solid var(--theme-elevation-100);
padding: base(1) 0 0 base(1);
order: 1;
li {
margin: 0 base(0.5) 0 0;
}
}
&__sidebar {
padding-bottom: base(4);
height: auto;
margin: var(--base) 0;
}
&__payload-settings {
margin-top: base(2);
padding-top: base(2);
margin-top: base(1);
padding-top: base(1);
padding-bottom: base(0.5);
border-top: 1px solid var(--theme-elevation-100);
border-bottom: 1px solid var(--theme-elevation-100);

View File

@@ -9,7 +9,6 @@ import { getTranslation } from '../../../../utilities/getTranslation'
import { EntityType, groupNavItems } from '../../../utilities/groupNavItems'
import Button from '../../elements/Button'
import Card from '../../elements/Card'
import Eyebrow from '../../elements/Eyebrow'
import { Gutter } from '../../elements/Gutter'
import { useConfig } from '../../utilities/Config'
import './index.scss'
@@ -70,7 +69,6 @@ const Dashboard: React.FC<Props> = (props) => {
return (
<div className={baseClass}>
<Eyebrow />
<Gutter className={`${baseClass}__wrap`}>
{Array.isArray(beforeDashboard) &&
beforeDashboard.map((Component, i) => <Component key={i} />)}

View File

@@ -4,42 +4,21 @@ import { useTranslation } from 'react-i18next'
import type { Props } from './types'
import { getTranslation } from '../../../../utilities/getTranslation'
import { formatDate } from '../../../utilities/formatDate'
import Autosave from '../../elements/Autosave'
import CopyToClipboard from '../../elements/CopyToClipboard'
import Eyebrow from '../../elements/Eyebrow'
import { Gutter } from '../../elements/Gutter'
import { DocumentHeader } from '../../elements/DocumentHeader'
import { FormLoadingOverlayToggle } from '../../elements/Loading'
import PreviewButton from '../../elements/PreviewButton'
import { Publish } from '../../elements/Publish'
import { Save } from '../../elements/Save'
import { SaveDraft } from '../../elements/SaveDraft'
import Status from '../../elements/Status'
import VersionsCount from '../../elements/VersionsCount'
import ViewDescription from '../../elements/ViewDescription'
import Form from '../../forms/Form'
import RenderFields from '../../forms/RenderFields'
import fieldTypes from '../../forms/field-types'
import LeaveWithoutSaving from '../../modals/LeaveWithoutSaving'
import { useConfig } from '../../utilities/Config'
import { useDocumentInfo } from '../../utilities/DocumentInfo'
import Meta from '../../utilities/Meta'
import { OperationContext } from '../../utilities/OperationProvider'
import { GlobalRoutes } from './Routes'
import './index.scss'
const baseClass = 'global-edit'
const DefaultGlobalView: React.FC<Props> = (props) => {
const { action, apiURL, data, global, initialState, isLoading, onSave, permissions, updatedAt } =
props
const { action, apiURL, data, global, initialState, isLoading, onSave, permissions } = props
const {
admin: { dateFormat },
} = useConfig()
const { publishedDoc } = useDocumentInfo()
const { i18n, t } = useTranslation('general')
const { i18n } = useTranslation('general')
const { admin: { description, hideAPIURL, preview } = {}, fields, label, versions } = global
const { label } = global
const hasSavePermission = permissions?.update?.permission
@@ -57,138 +36,12 @@ const DefaultGlobalView: React.FC<Props> = (props) => {
<FormLoadingOverlayToggle
action="update"
loadingSuffix={getTranslation(label, i18n)}
name={`global-edit--${label}`}
name={`global-edit--${typeof label === 'string' ? label : label?.en}`}
/>
{!isLoading && (
<React.Fragment>
<div className={`${baseClass}__main`}>
<Meta
description={getTranslation(label, i18n)}
keywords={`${getTranslation(label, i18n)}, Payload, CMS`}
title={getTranslation(label, i18n)}
/>
<Eyebrow />
{!(global.versions?.drafts && global.versions?.drafts?.autosave) && (
<LeaveWithoutSaving />
)}
<Gutter className={`${baseClass}__edit`}>
<header className={`${baseClass}__header`}>
<h1>{t('editLabel', { label: getTranslation(label, i18n) })}</h1>
{description && (
<div className={`${baseClass}__sub-header`}>
<ViewDescription description={description} />
</div>
)}
</header>
<RenderFields
fieldSchema={fields}
fieldTypes={fieldTypes}
filter={(field) =>
!field.admin.position ||
(field.admin.position && field.admin.position !== 'sidebar')
}
permissions={permissions.fields}
readOnly={!hasSavePermission}
/>
</Gutter>
</div>
<div className={`${baseClass}__sidebar-wrap`}>
<div className={`${baseClass}__sidebar`}>
<div className={`${baseClass}__sidebar-sticky-wrap`}>
<div
className={`${baseClass}__document-actions${
(global.versions?.drafts && !global.versions?.drafts?.autosave) || preview
? ` ${baseClass}__document-actions--has-2`
: ''
}`}
>
{preview &&
(!global.versions?.drafts || global.versions?.drafts?.autosave) && (
<PreviewButton
CustomComponent={global?.admin?.components?.elements?.PreviewButton}
generatePreviewURL={preview}
/>
)}
{hasSavePermission && (
<React.Fragment>
{global.versions?.drafts && (
<React.Fragment>
{!global.versions.drafts.autosave && (
<SaveDraft
CustomComponent={
global?.admin?.components?.elements?.SaveDraftButton
}
/>
)}
<Publish
CustomComponent={global?.admin?.components?.elements?.PublishButton}
/>
</React.Fragment>
)}
{!global.versions?.drafts && (
<Save
CustomComponent={global?.admin?.components?.elements?.SaveButton}
/>
)}
</React.Fragment>
)}
</div>
<div className={`${baseClass}__sidebar-fields`}>
{preview && global.versions?.drafts && !global.versions?.drafts?.autosave && (
<PreviewButton
CustomComponent={global?.admin?.components?.elements?.PreviewButton}
generatePreviewURL={preview}
/>
)}
{global.versions?.drafts && (
<React.Fragment>
<Status />
{global.versions.drafts.autosave && hasSavePermission && (
<Autosave
global={global}
publishedDocUpdatedAt={publishedDoc?.updatedAt || data?.createdAt}
/>
)}
</React.Fragment>
)}
<RenderFields
fieldSchema={fields}
fieldTypes={fieldTypes}
filter={(field) => field.admin.position === 'sidebar'}
permissions={permissions.fields}
readOnly={!hasSavePermission}
/>
</div>
<ul className={`${baseClass}__meta`}>
{versions && (
<li>
<div className={`${baseClass}__label`}>{t('version:versions')}</div>
<VersionsCount global={global} />
</li>
)}
{data && !hideAPIURL && (
<li className={`${baseClass}__api-url`}>
<span className={`${baseClass}__label`}>
API URL <CopyToClipboard value={apiURL} />
</span>
<a href={apiURL} rel="noopener noreferrer" target="_blank">
{apiURL}
</a>
</li>
)}
{updatedAt && (
<li>
<div className={`${baseClass}__label`}>{t('lastModified')}</div>
<div>{formatDate(updatedAt, dateFormat, i18n?.language)}</div>
</li>
)}
</ul>
</div>
</div>
</div>
<DocumentHeader apiURL={apiURL} data={data} global={global} />
<GlobalRoutes {...props} />
</React.Fragment>
)}
</Form>

View File

@@ -0,0 +1,125 @@
@import '../../../../scss/styles.scss';
.global-edit {
width: 100%;
&__wrapper {
display: flex;
}
&__main {
width: calc(100% - #{base(15)});
display: flex;
flex-direction: column;
min-height: 100%;
}
&__edit {
padding-top: base(1);
padding-bottom: base(4);
flex-grow: 1;
[dir='ltr'] & {
top: 0;
right: 0;
border-right: 1px solid var(--theme-elevation-100);
}
[dir='rtl'] & {
top: 0;
left: 0;
border-left: 1px solid var(--theme-elevation-100);
}
}
&__sidebar-wrap {
position: sticky;
top: 0;
width: base(15);
}
&__sidebar {
width: 100%;
height: 100%;
overflow-y: auto;
}
&__sidebar-sticky-wrap {
display: flex;
flex-direction: column;
min-height: 100%;
}
&__sidebar-fields {
display: flex;
flex-direction: column;
gap: $baseline;
[dir='ltr'] & {
padding-right: $baseline;
}
[dir='rtl'] & {
padding-left: $baseline;
}
.render-fields {
& > *:last-child {
margin-bottom: 0;
}
}
}
&__label {
color: var(--theme-elevation-400);
}
&--is-editing {
.global-edit__sidebar {
padding-top: 0;
}
}
@include mid-break {
&__main {
width: 100%;
min-height: initial;
}
&__sidebar-wrap {
position: static;
width: 100%;
height: initial;
border-left: 0;
}
&__form {
display: block;
}
&__edit {
padding-top: 0;
padding-bottom: 0;
}
&__sidebar-fields {
padding-top: 0;
padding-left: var(--gutter-h);
padding-right: var(--gutter-h);
gap: base(0.5);
[dir='ltr'] & {
padding-right: var(--gutter-h);
}
[dir='rtl'] & {
padding-left: var(--gutter-h);
}
}
&__sidebar {
padding-bottom: base(3.5);
overflow: visible;
}
}
}

View File

@@ -0,0 +1,81 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import type { Props } from '../types'
import { getTranslation } from '../../../../../utilities/getTranslation'
import { DocumentControls } from '../../../elements/DocumentControls'
import { Gutter } from '../../../elements/Gutter'
import ViewDescription from '../../../elements/ViewDescription'
import RenderFields from '../../../forms/RenderFields'
import fieldTypes from '../../../forms/field-types'
import LeaveWithoutSaving from '../../../modals/LeaveWithoutSaving'
import Meta from '../../../utilities/Meta'
import './index.scss'
const baseClass = 'global-edit'
export const DefaultGlobalView: React.FC<Props> = (props) => {
const { apiURL, data, global, permissions } = props
const { i18n } = useTranslation('general')
const { admin: { description } = {}, fields, label } = global
const hasSavePermission = permissions?.update?.permission
return (
<React.Fragment>
{/* <SetStepNav collection={collection} id={id} isEditing={isEditing} /> */}
<DocumentControls
apiURL={apiURL}
data={data}
global={global}
hasSavePermission={hasSavePermission}
isEditing
permissions={permissions}
/>
<div className={`${baseClass}__main`}>
<Meta
description={getTranslation(label, i18n)}
keywords={`${getTranslation(label, i18n)}, Payload, CMS`}
title={getTranslation(label, i18n)}
/>
{!(global.versions?.drafts && global.versions?.drafts?.autosave) && <LeaveWithoutSaving />}
<Gutter className={`${baseClass}__edit`}>
<header className={`${baseClass}__header`}>
{description && (
<div className={`${baseClass}__sub-header`}>
<ViewDescription description={description} />
</div>
)}
</header>
<RenderFields
fieldSchema={fields}
fieldTypes={fieldTypes}
filter={(field) =>
!field.admin.position || (field.admin.position && field.admin.position !== 'sidebar')
}
permissions={permissions.fields}
readOnly={!hasSavePermission}
/>
</Gutter>
</div>
<div className={`${baseClass}__sidebar-wrap`}>
<div className={`${baseClass}__sidebar`}>
<div className={`${baseClass}__sidebar-sticky-wrap`}>
<div className={`${baseClass}__sidebar-fields`}>
<RenderFields
fieldSchema={fields}
fieldTypes={fieldTypes}
filter={(field) => field.admin.position === 'sidebar'}
permissions={permissions.fields}
readOnly={!hasSavePermission}
/>
</div>
</div>
</div>
</div>
</React.Fragment>
)
}

View File

@@ -0,0 +1,61 @@
import type { match } from 'react-router-dom'
import React from 'react'
import { Route } from 'react-router-dom'
import type { GlobalPermission, User } from '../../../../../auth'
import type { SanitizedGlobalConfig } from '../../../../../exports/types'
import Unauthorized from '../../Unauthorized'
export const globalCustomRoutes = (props: {
global?: SanitizedGlobalConfig
match: match<{
[key: string]: string | undefined
}>
permissions: GlobalPermission
user: User
}): React.ReactElement[] => {
const { global, match, permissions, user } = props
let customViews = []
const internalViews = ['Default', 'Versions']
const BaseEdit = global?.admin?.components?.views?.Edit
if (typeof BaseEdit !== 'function' && typeof BaseEdit === 'object') {
customViews = Object.entries(BaseEdit)
.filter(([viewKey, view]) => {
// Remove internal views from the list of custom views
// This way we can easily iterate over the remaining views
return Boolean(
!internalViews.includes(viewKey) &&
typeof view !== 'function' &&
typeof view === 'object',
)
})
?.map(([, view]) => view)
}
return customViews?.reduce((acc, { Component, path }) => {
const routesToReturn = [...acc]
if (global) {
routesToReturn.push(
<Route
exact
key={`${global.slug}-${path}`}
path={`${match.url}/globals/${global.slug}${path}`}
>
{permissions?.read?.permission ? (
<Component global={global} user={user} />
) : (
<Unauthorized />
)}
</Route>,
)
}
return routesToReturn
}, [])
}

View File

@@ -0,0 +1,58 @@
import { lazy } from 'react'
import React from 'react'
import { Route, Switch, useRouteMatch } from 'react-router-dom'
import { useAuth } from '../../../utilities/Auth'
import { useConfig } from '../../../utilities/Config'
import Version from '../../Version'
import VersionsView from '../../Versions'
import { DefaultGlobalView } from '../Default/index'
import { type Props } from '../types'
import { globalCustomRoutes } from './custom'
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
const Unauthorized = lazy(() => import('../../Unauthorized'))
export const GlobalRoutes: React.FC<Props> = (props) => {
const { global, permissions } = props
const match = useRouteMatch()
const {
routes: { admin: adminRoute },
} = useConfig()
const { user } = useAuth()
return (
<Switch>
<Route
exact
key={`${global.slug}-versions`}
path={`${adminRoute}/globals/${global.slug}/versions`}
>
{permissions?.readVersions?.permission ? (
<VersionsView global={global} />
) : (
<Unauthorized />
)}
</Route>
<Route
exact
key={`${global.slug}-view-version`}
path={`${adminRoute}/globals/${global.slug}/versions/:versionID`}
>
{permissions?.readVersions?.permission ? <Version global={global} /> : <Unauthorized />}
</Route>
{globalCustomRoutes({
global,
match,
permissions,
user,
})}
<Route>
<DefaultGlobalView {...props} />
</Route>
</Switch>
)
}

View File

@@ -1,276 +1,9 @@
@import '../../../scss/styles.scss';
.global-edit {
.collection-edit {
width: 100%;
&__form {
height: 100%;
}
&__main {
width: calc(100% - #{base(15)});
display: flex;
flex-direction: column;
min-height: 100%;
}
&__header {
h1 {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin: 0;
line-height: 1.25;
}
margin-bottom: base(1);
}
&__sub-header {
margin-top: base(0.25);
}
&__edit {
padding-top: base(1);
padding-bottom: base(2);
flex-grow: 1;
}
&__sidebar-wrap {
position: fixed;
width: base(15);
height: 100%;
overflow: visible;
[dir='ltr'] & {
top: 0;
right: 0;
border-left: 1px solid var(--theme-elevation-100);
}
[dir='rtl'] & {
top: 0;
left: 0;
border-right: 1px solid var(--theme-elevation-100);
}
}
&__sidebar {
width: 100%;
height: 100%;
overflow-y: auto;
}
&__sidebar-sticky-wrap {
display: flex;
flex-direction: column;
min-height: 100%;
}
&__document-actions,
&__meta,
&__sidebar-fields {
[dir='ltr'] & {
padding-left: base(1.5);
}
[dir='rtl'] & {
padding-right: base(1.5);
}
}
&__sidebar-fields {
[dir='ltr'] & {
padding-right: $baseline;
}
[dir='rtl'] & {
padding-left: $baseline;
}
display: flex;
flex-direction: column;
gap: $baseline;
.preview-btn {
display: inline-block;
margin: 0;
width: calc(50% - #{base(0.5)});
}
.render-fields {
& > *:last-child {
margin-bottom: 0;
}
}
}
&__document-actions {
@include blur-bg;
position: sticky;
top: 0;
z-index: 2;
[dir='ltr'] & {
padding-right: $baseline;
}
[dir='rtl'] & {
padding-left: $baseline;
}
> * {
position: relative;
z-index: 1;
}
}
&__document-actions--has-2 {
display: flex;
> * {
width: calc(50% - #{base(0.5)});
}
> *:first-child {
[dir='ltr'] & {
margin-right: base(0.5);
}
[dir='rtl'] & {
margin-left: base(0.5);
}
}
> *:last-child {
[dir='ltr'] & {
margin-left: base(0.5);
}
[dir='rtl'] & {
margin-right: base(0.5);
}
}
.form-submit {
.btn {
width: 100%;
padding-left: base(0.5);
padding-right: base(0.5);
}
}
}
&__api-url {
margin-bottom: base(1.5);
a {
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
}
&__meta {
margin: auto 0 $baseline;
padding-top: $baseline;
[dir='ltr'] & {
padding-right: $baseline;
}
[dir='rtl'] & {
padding-left: $baseline;
}
list-style: none;
li {
margin-bottom: base(0.5);
&:last-child {
margin-bottom: 0;
}
}
}
&__label {
color: var(--theme-elevation-400);
}
&__api-url {
a,
button {
cursor: pointer;
font-weight: bold;
text-decoration: none;
&:hover,
&:focus-visible {
text-decoration: underline;
}
}
}
&--is-editing {
.collection-edit__sidebar {
padding-top: 0;
}
}
@include mid-break {
&__main {
width: 100%;
min-height: initial;
}
&__sidebar-wrap {
position: static;
width: 100%;
height: initial;
}
&__meta {
border-top: 1px solid var(--theme-elevation-100);
}
&__form {
display: block;
}
&__edit {
padding-top: 0;
padding-bottom: 0;
}
&__document-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
top: auto;
}
&__document-actions,
&__meta,
&__sidebar-fields {
padding-left: var(--gutter-h);
padding-right: var(--gutter-h);
}
&__sidebar-fields {
padding-top: 0;
[dir='ltr'] & {
padding-right: var(--gutter-h);
}
[dir='rtl'] & {
padding-left: var(--gutter-h);
}
gap: base(0.5);
.preview-btn {
width: 100%;
}
}
&__api-url {
margin-bottom: base(0.5);
}
&__sidebar {
padding-bottom: base(3.5);
}
&__sidebar-sticky {
height: auto;
}
}
}

View File

@@ -14,7 +14,7 @@ import { useDocumentInfo } from '../../utilities/DocumentInfo'
import { useLocale } from '../../utilities/Locale'
import { usePreferences } from '../../utilities/Preferences'
import RenderCustomComponent from '../../utilities/RenderCustomComponent'
import DefaultGlobal from './Default'
import DefaultGlobalView from './Default'
const GlobalView: React.FC<IndexProps> = (props) => {
const { state: locationState } = useLocation<{ data?: Record<string, unknown> }>()
@@ -117,7 +117,7 @@ const GlobalView: React.FC<IndexProps> = (props) => {
return (
<RenderCustomComponent
CustomComponent={CustomEditView}
DefaultComponent={DefaultGlobal}
DefaultComponent={DefaultGlobalView}
componentProps={{
action: `${serverURL}${api}/globals/${slug}?locale=${locale}&fallback-locale=null`,
apiURL: `${serverURL}${api}/globals/${slug}?locale=${locale}${

View File

@@ -2,7 +2,6 @@ import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '../../elements/Button'
import Eyebrow from '../../elements/Eyebrow'
import { Gutter } from '../../elements/Gutter'
import { useStepNav } from '../../elements/StepNav'
import { useConfig } from '../../utilities/Config'
@@ -32,7 +31,6 @@ const NotFound: React.FC = () => {
keywords={`404 ${t('notFound')}`}
title={t('notFound')}
/>
<Eyebrow />
<Gutter className={`${baseClass}__wrap`}>
<h1>{t('nothingFound')}</h1>
<p>{t('sorryNotFound')}</p>

View File

@@ -8,10 +8,7 @@ import type { Permissions, User } from '../../../../auth'
import type { SanitizedCollectionConfig } from '../../../../collections/config/types'
import { DocumentInfoProvider } from '../../utilities/DocumentInfo'
import Version from '../Version'
import Versions from '../Versions'
import List from '../collections/List'
import { childRoutes } from './child'
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
const Edit = lazy(() => import('../collections/Edit'))
@@ -63,7 +60,6 @@ export const collectionRoutes = (props: {
)}
</Route>,
<Route
exact
key={`${collection.slug}-edit`}
path={`${match.url}/collections/${collection.slug}/:id`}
>
@@ -75,47 +71,8 @@ export const collectionRoutes = (props: {
<Unauthorized />
)}
</Route>,
childRoutes({
collection,
match,
permissions,
user,
}),
]
// Version routes
if (collection.versions) {
routesToReturn.push(
<Route
exact
key={`${collection.slug}-versions`}
path={`${match.url}/collections/${collection.slug}/:id/versions`}
>
{permissions?.collections?.[collection.slug]?.readVersions?.permission ? (
<Versions collection={collection} />
) : (
<Unauthorized />
)}
</Route>,
)
routesToReturn.push(
<Route
exact
key={`${collection.slug}-view-version`}
path={`${match.url}/collections/${collection.slug}/:id/versions/:versionID`}
>
{permissions?.collections?.[collection.slug]?.readVersions?.permission ? (
<DocumentInfoProvider collection={collection} idFromParams>
<Version collection={collection} />
</DocumentInfoProvider>
) : (
<Unauthorized />
)}
</Route>,
)
}
return routesToReturn
}, [])
}

View File

@@ -8,8 +8,6 @@ import type { Permissions, User } from '../../../../auth'
import type { SanitizedGlobalConfig } from '../../../../exports/types'
import { DocumentInfoProvider } from '../../utilities/DocumentInfo'
import Version from '../Version'
import Versions from '../Versions'
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
const EditGlobal = lazy(() => import('../Global'))
@@ -35,12 +33,11 @@ export const globalRoutes = (props: {
?.filter(({ admin: { hidden } }) => !(typeof hidden === 'function' ? hidden({ user }) : hidden))
.reduce((acc, global) => {
const canReadGlobal = permissions?.globals?.[global.slug]?.read?.permission
const canReadVersions = permissions?.globals?.[global.slug]?.readVersions?.permission
// Default routes
const routesToReturn = [
...acc,
<Route exact key={global.slug} path={`${match.url}/globals/${global.slug}`}>
<Route key={global.slug} path={`${match.url}/globals/${global.slug}`}>
{canReadGlobal ? (
<DocumentInfoProvider global={global} idFromParams key={`${global.slug}-${locale}`}>
<EditGlobal global={global} />
@@ -51,29 +48,6 @@ export const globalRoutes = (props: {
</Route>,
]
// Version routes
if (global.versions) {
routesToReturn.push(
<Route
exact
key={`${global.slug}-versions`}
path={`${match.url}/globals/${global.slug}/versions`}
>
{canReadVersions ? <Versions global={global} /> : <Unauthorized />}
</Route>,
)
routesToReturn.push(
<Route
exact
key={`${global.slug}-view-version`}
path={`${match.url}/globals/${global.slug}/versions/:versionID`}
>
{canReadVersions ? <Version global={global} /> : <Unauthorized />}
</Route>,
)
}
return routesToReturn
}, [])
}

View File

@@ -11,7 +11,6 @@ import { fieldAffectsData } from '../../../../fields/config/types'
import { getTranslation } from '../../../../utilities/getTranslation'
import usePayloadAPI from '../../../hooks/usePayloadAPI'
import { formatDate } from '../../../utilities/formatDate'
import Eyebrow from '../../elements/Eyebrow'
import { Gutter } from '../../elements/Gutter'
import { useStepNav } from '../../elements/StepNav'
import { useAuth } from '../../utilities/Auth'
@@ -197,7 +196,6 @@ const VersionView: React.FC<Props> = ({ collection, global }) => {
<React.Fragment>
<div className={baseClass}>
<Meta description={metaDesc} title={metaTitle} />
<Eyebrow />
<Gutter className={`${baseClass}__wrap`}>
<div className={`${baseClass}__intro`}>
{t('versionCreatedOn', { version: t(doc?.autosave ? 'autosavedVersion' : 'version') })}

View File

@@ -2,14 +2,18 @@
.view-version {
width: 100%;
margin-bottom: base(2);
margin-bottom: calc(var(--base) * 2);
&__wrap {
padding-top: base(1);
padding-top: var(--base);
}
&__intro {
margin-bottom: calc(var(--base) / 2);
}
&__header {
margin-bottom: $baseline;
margin-bottom: var(--base);
display: flex;
align-items: center;
flex-wrap: wrap;
@@ -21,13 +25,10 @@
&__controls {
display: flex;
margin-bottom: $baseline;
margin-left: base(-0.5);
margin-right: base(-0.5);
margin-bottom: var(--base);
gap: var(--base);
> * {
margin-left: base(0.5);
margin-right: base(0.5);
flex-basis: 100%;
}
}

View File

@@ -5,9 +5,7 @@ import type { StepNavItem } from '../../elements/StepNav/types'
import type { Props } from './types'
import { getTranslation } from '../../../../utilities/getTranslation'
import Eyebrow from '../../elements/Eyebrow'
import { Gutter } from '../../elements/Gutter'
import IDLabel from '../../elements/IDLabel'
import { LoadingOverlayToggle } from '../../elements/Loading'
import Paginator from '../../elements/Paginator'
import PerPage from '../../elements/PerPage'
@@ -22,7 +20,7 @@ import './index.scss'
const baseClass = 'versions'
export const DefaultVersionsView: React.FC<Props> = (props) => {
const { collection, data, editURL, entityLabel, global, id, isLoadingVersions, versionsData } =
const { id, collection, data, editURL, entityLabel, global, isLoadingVersions, versionsData } =
props
const {
@@ -85,38 +83,37 @@ export const DefaultVersionsView: React.FC<Props> = (props) => {
setStepNav(nav)
}, [setStepNav, collection, global, useAsTitle, data, admin, id, editURL, t, i18n])
let useIDLabel = data[useAsTitle] === data?.id
let heading: string
let metaDesc: string
let metaTitle: string
if (collection) {
metaTitle = `${t('versions')} - ${data[useAsTitle]} - ${entityLabel}`
metaDesc = t('viewingVersions', { documentTitle: data[useAsTitle], entityLabel })
heading = data?.[useAsTitle] || `[${t('general:untitled')}]`
}
if (global) {
metaTitle = `${t('versions')} - ${entityLabel}`
metaDesc = t('viewingVersionsGlobal', { entityLabel })
heading = entityLabel
useIDLabel = false
}
const versionCount = versionsData?.totalDocs || 0
return (
<React.Fragment>
<LoadingOverlayToggle name="versions" show={isLoadingVersions} />
<div className={baseClass}>
<Meta description={metaDesc} title={metaTitle} />
<Eyebrow />
<Gutter className={`${baseClass}__wrap`}>
<header className={`${baseClass}__header`}>
<div className={`${baseClass}__intro`}>{t('showingVersionsFor')}</div>
{useIDLabel && <IDLabel id={data?.id} />}
{!useIDLabel && <h1>{heading}</h1>}
</header>
{versionsData?.totalDocs > 0 && (
{versionCount === 0 && (
<div className={`${baseClass}__no-versions`}>{t('noFurtherVersionsFound')}</div>
)}
{versionCount > 0 && (
<React.Fragment>
{/* <div className={`${baseClass}__version-count`}>
{t(versionCount === 1 ? 'versionCount_one' : 'versionCount_many', {
count: versionCount,
})}
</div> */}
<Table
columns={buildVersionColumns(collection, global, t)}
data={versionsData?.docs}
@@ -150,9 +147,6 @@ export const DefaultVersionsView: React.FC<Props> = (props) => {
</div>
</React.Fragment>
)}
{versionsData?.totalDocs === 0 && (
<div className={`${baseClass}__no-versions`}>{t('noFurtherVersionsFound')}</div>
)}
</Gutter>
</div>
</React.Fragment>

View File

@@ -5,15 +5,16 @@
margin-bottom: base(2);
&__wrap {
padding-top: base(1);
padding-top: 0;
padding-bottom: base(4);
}
&__header {
margin-bottom: $baseline;
margin-bottom: var(--base);
}
&__intro {
margin-bottom: base(0.5);
&__no-version {
margin-top: calc(var(--base) * 2);
}
&__parent-doc {

View File

@@ -1,6 +1,5 @@
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useRouteMatch } from 'react-router-dom'
import type { IndexProps } from './types'
@@ -14,7 +13,7 @@ import { useSearchParams } from '../../utilities/SearchParams'
import { DefaultVersionsView } from './Default'
const VersionsView: React.FC<IndexProps> = (props) => {
const { collection, global } = props
const { id, collection, global } = props
const { permissions, user } = useAuth()
@@ -29,10 +28,6 @@ const VersionsView: React.FC<IndexProps> = (props) => {
const { limit, page, sort } = useSearchParams()
const {
params: { id },
} = useRouteMatch<{ id: string }>()
let CustomVersionsView: React.ComponentType | null = null
let docURL: string
let entityLabel: string
@@ -125,6 +120,7 @@ const VersionsView: React.FC<IndexProps> = (props) => {
CustomComponent={CustomVersionsView}
DefaultComponent={DefaultVersionsView}
componentProps={{
id,
canAccessAdmin: permissions?.canAccessAdmin,
collection,
data,
@@ -132,7 +128,6 @@ const VersionsView: React.FC<IndexProps> = (props) => {
entityLabel,
fetchURL,
global,
id,
isLoading,
isLoadingVersions,
user,

View File

@@ -6,6 +6,18 @@ import type { Version } from '../../utilities/DocumentInfo/types'
export type IndexProps = {
collection?: SanitizedCollectionConfig
global?: SanitizedGlobalConfig
id?: string
}
export type Props = IndexProps & {
data: Version
editURL: string
entityLabel: string
fetchURL: string
id: string
isLoading: boolean
isLoadingVersions: boolean
versionsData: PaginatedDocs<Version>
}
export type Props = IndexProps & {

View File

@@ -1,7 +1,6 @@
@import '../../../../../scss/styles.scss';
.auth-fields {
margin: base(1.5) 0 base(2);
padding: base(2) base(2) base(1.5);
background: var(--theme-elevation-50);

View File

@@ -1,48 +1,21 @@
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import type { Props } from './types'
import { getTranslation } from '../../../../../utilities/getTranslation'
import { formatDate } from '../../../../utilities/formatDate'
import Autosave from '../../../elements/Autosave'
import CopyToClipboard from '../../../elements/CopyToClipboard'
import DeleteDocument from '../../../elements/DeleteDocument'
import DuplicateDocument from '../../../elements/DuplicateDocument'
import Eyebrow from '../../../elements/Eyebrow'
import { Gutter } from '../../../elements/Gutter'
import { DocumentHeader } from '../../../elements/DocumentHeader'
import { FormLoadingOverlayToggle } from '../../../elements/Loading'
import PreviewButton from '../../../elements/PreviewButton'
import { Publish } from '../../../elements/Publish'
import RenderTitle from '../../../elements/RenderTitle'
import { Save } from '../../../elements/Save'
import { SaveDraft } from '../../../elements/SaveDraft'
import Status from '../../../elements/Status'
import VersionsCount from '../../../elements/VersionsCount'
import Form from '../../../forms/Form'
import RenderFields from '../../../forms/RenderFields'
import fieldTypes from '../../../forms/field-types'
import LeaveWithoutSaving from '../../../modals/LeaveWithoutSaving'
import { useAuth } from '../../../utilities/Auth'
import { useConfig } from '../../../utilities/Config'
import { useDocumentInfo } from '../../../utilities/DocumentInfo'
import Meta from '../../../utilities/Meta'
import { OperationContext } from '../../../utilities/OperationProvider'
import Auth from './Auth'
import { SetStepNav } from './SetStepNav'
import Upload from './Upload'
import { CollectionRoutes } from './Routes'
import './index.scss'
const baseClass = 'collection-edit'
const DefaultEditView: React.FC<Props> = (props) => {
const {
admin: { dateFormat },
routes: { admin },
} = useConfig()
const { publishedDoc } = useDocumentInfo()
const { i18n, t } = useTranslation('general')
const { i18n } = useTranslation('general')
const { refreshCookieAsync, user } = useAuth()
const {
@@ -51,28 +24,15 @@ const DefaultEditView: React.FC<Props> = (props) => {
collection,
customHeader,
data,
disableActions,
disableEyebrow,
disableLeaveWithoutSaving,
hasSavePermission,
id,
internalState,
isEditing,
isLoading,
onSave: onSaveFromProps,
permissions,
updatedAt,
} = props
const {
admin: { disableDuplicate, hideAPIURL, preview, useAsTitle },
auth,
fields,
slug,
timestamps,
upload,
versions,
} = collection
const { auth } = collection
const classes = [baseClass, isEditing && `${baseClass}--is-editing`].filter(Boolean).join(' ')
@@ -110,238 +70,24 @@ const DefaultEditView: React.FC<Props> = (props) => {
action={isLoading ? 'loading' : operation}
formIsLoading={isLoading}
loadingSuffix={getTranslation(collection.labels.singular, i18n)}
name={`collection-edit--${collection.labels.singular}`}
name={`collection-edit--${
typeof collection?.labels?.singular === 'string'
? collection.labels.singular
: 'document'
}`}
type="withoutNav"
/>
{!isLoading && (
<React.Fragment>
{!disableEyebrow && (
<SetStepNav collection={collection} id={data?.id} isEditing={isEditing} />
)}
<div className={`${baseClass}__main`}>
<Meta
description={`${isEditing ? t('editing') : t('creating')} - ${getTranslation(
collection.labels.singular,
i18n,
)}`}
keywords={`${getTranslation(collection.labels.singular, i18n)}, Payload, CMS`}
title={`${isEditing ? t('editing') : t('creating')} - ${getTranslation(
collection.labels.singular,
i18n,
)}`}
/>
{!disableEyebrow && <Eyebrow />}
{!(collection.versions?.drafts && collection.versions?.drafts?.autosave) &&
!disableLeaveWithoutSaving && <LeaveWithoutSaving />}
<Gutter className={`${baseClass}__edit`}>
<header className={`${baseClass}__header`}>
{customHeader && customHeader}
{!customHeader && (
<h1>
<RenderTitle
<DocumentHeader
apiURL={apiURL}
collection={collection}
customHeader={customHeader}
data={data}
fallback={`[${t('untitled')}]`}
useAsTitle={useAsTitle}
/>
</h1>
)}
</header>
{auth && (
<Auth
collection={collection}
email={data?.email}
operation={operation}
readOnly={!hasSavePermission}
requirePassword={!isEditing}
useAPIKey={auth.useAPIKey}
verify={auth.verify}
/>
)}
{upload && (
<Upload collection={collection} data={data} internalState={internalState} />
)}
<RenderFields
fieldSchema={fields}
fieldTypes={fieldTypes}
filter={(field) =>
!field?.admin?.position || field?.admin?.position !== 'sidebar'
}
permissions={permissions.fields}
readOnly={!hasSavePermission}
/>
</Gutter>
</div>
<div className={`${baseClass}__sidebar-wrap`}>
<div className={`${baseClass}__sidebar`}>
<div className={`${baseClass}__sidebar-sticky-wrap`}>
{!disableActions && (
<ul className={`${baseClass}__collection-actions`}>
{permissions?.create?.permission && (
<React.Fragment>
<li>
<Link id="action-create" to={`${admin}/collections/${slug}/create`}>
{t('createNew')}
</Link>
</li>
{!disableDuplicate && isEditing && (
<li>
<DuplicateDocument collection={collection} id={id} slug={slug} />
</li>
)}
</React.Fragment>
)}
{permissions?.delete?.permission && (
<li>
<DeleteDocument
buttonId="action-delete"
collection={collection}
id={id}
isEditing={isEditing}
/>
</li>
)}
</ul>
)}
<div
className={[
`${baseClass}__document-actions`,
((collection.versions?.drafts &&
!collection.versions?.drafts?.autosave) ||
(isEditing && preview)) &&
`${baseClass}__document-actions--has-2`,
]
.filter(Boolean)
.join(' ')}
>
{isEditing &&
preview &&
(!collection.versions?.drafts ||
collection.versions?.drafts?.autosave) && (
<PreviewButton
CustomComponent={collection?.admin?.components?.edit?.PreviewButton}
generatePreviewURL={preview}
/>
)}
{hasSavePermission && (
<React.Fragment>
{collection.versions?.drafts ? (
<React.Fragment>
{!collection.versions.drafts.autosave && (
<SaveDraft
CustomComponent={
collection?.admin?.components?.edit?.SaveDraftButton
}
/>
)}
<Publish
CustomComponent={
collection?.admin?.components?.edit?.PublishButton
}
/>
</React.Fragment>
) : (
<Save
CustomComponent={collection?.admin?.components?.edit?.SaveButton}
/>
)}
</React.Fragment>
)}
</div>
<div className={`${baseClass}__sidebar-fields`}>
{isEditing &&
preview &&
collection.versions?.drafts &&
!collection.versions?.drafts?.autosave && (
<PreviewButton
CustomComponent={collection?.admin?.components?.edit?.PreviewButton}
generatePreviewURL={preview}
/>
)}
{collection.versions?.drafts && (
<React.Fragment>
<Status />
{collection.versions?.drafts.autosave && hasSavePermission && (
<Autosave
collection={collection}
id={id}
publishedDocUpdatedAt={publishedDoc?.updatedAt || data?.createdAt}
/>
)}
</React.Fragment>
)}
<RenderFields
fieldSchema={fields}
fieldTypes={fieldTypes}
filter={(field) => field?.admin?.position === 'sidebar'}
permissions={permissions.fields}
readOnly={!hasSavePermission}
/>
</div>
{isEditing && (
<ul className={`${baseClass}__meta`}>
{!hideAPIURL && (
<li className={`${baseClass}__api-url`}>
<span className={`${baseClass}__label`}>
API URL <CopyToClipboard value={apiURL} />
</span>
<a href={apiURL} rel="noopener noreferrer" target="_blank">
{apiURL}
</a>
</li>
)}
{versions && (
<li>
<div className={`${baseClass}__label`}>{t('version:versions')}</div>
<VersionsCount collection={collection} id={id} />
</li>
)}
{timestamps && (
<React.Fragment>
{updatedAt && (
<li>
<div className={`${baseClass}__label`}>{t('lastModified')}</div>
<div>
{formatDate(data.updatedAt, dateFormat, i18n?.language)}
</div>
</li>
)}
{(publishedDoc?.createdAt || data?.createdAt) && (
<li>
<div className={`${baseClass}__label`}>{t('created')}</div>
<div>
{formatDate(
publishedDoc?.createdAt || data?.createdAt,
dateFormat,
i18n?.language,
)}
</div>
</li>
)}
</React.Fragment>
)}
</ul>
)}
</div>
</div>
</div>
<CollectionRoutes {...props} />
</React.Fragment>
)}
</Form>

View File

@@ -0,0 +1,118 @@
@import '../../../../../scss/styles.scss';
.collection-edit {
width: 100%;
&__wrapper {
display: flex;
}
&__main {
width: calc(100% - #{base(15)});
display: flex;
flex-direction: column;
min-height: 100%;
}
&__edit {
padding-top: base(1);
padding-bottom: base(4);
flex-grow: 1;
[dir='ltr'] & {
top: 0;
right: 0;
border-right: 1px solid var(--theme-elevation-100);
}
[dir='rtl'] & {
top: 0;
left: 0;
border-left: 1px solid var(--theme-elevation-100);
}
}
&__sidebar-wrap {
position: sticky;
top: 0;
width: base(15);
}
&__sidebar {
width: 100%;
height: 100%;
overflow-y: auto;
}
&__sidebar-sticky-wrap {
display: flex;
flex-direction: column;
min-height: 100%;
}
&__sidebar-fields {
display: flex;
flex-direction: column;
gap: var(--base);
padding: var(--base);
.render-fields {
& > *:last-child {
margin-bottom: 0;
}
}
}
&__label {
color: var(--theme-elevation-400);
}
&--is-editing {
.collection-edit__sidebar {
padding-top: 0;
}
}
@include mid-break {
&__main {
width: 100%;
min-height: initial;
}
&__sidebar-wrap {
position: static;
width: 100%;
height: initial;
border-left: 0;
}
&__form {
display: block;
}
&__edit {
padding-top: 0;
padding-bottom: 0;
}
&__sidebar-fields {
padding-top: 0;
padding-left: var(--gutter-h);
padding-right: var(--gutter-h);
gap: base(0.5);
[dir='ltr'] & {
padding-right: var(--gutter-h);
}
[dir='rtl'] & {
padding-left: var(--gutter-h);
}
}
&__sidebar {
padding-bottom: base(3.5);
overflow: visible;
}
}
}

View File

@@ -0,0 +1,108 @@
import React, { Fragment } from 'react'
import { useTranslation } from 'react-i18next'
import type { Props } from '../types'
import { getTranslation } from '../../../../../../utilities/getTranslation'
import { DocumentControls } from '../../../../elements/DocumentControls'
import { Gutter } from '../../../../elements/Gutter'
import RenderFields from '../../../../forms/RenderFields'
import fieldTypes from '../../../../forms/field-types'
import LeaveWithoutSaving from '../../../../modals/LeaveWithoutSaving'
import Meta from '../../../../utilities/Meta'
import Auth from '../Auth'
import { SetStepNav } from '../SetStepNav'
import Upload from '../Upload'
import './index.scss'
const baseClass = 'collection-edit'
export const DefaultEdit: React.FC<Props> = (props) => {
const { i18n, t } = useTranslation('general')
const {
apiURL,
collection,
data,
disableActions,
disableLeaveWithoutSaving,
hasSavePermission,
id,
internalState,
isEditing,
permissions,
} = props
const { auth, fields, upload } = collection
const operation = isEditing ? 'update' : 'create'
return (
<Fragment>
<SetStepNav collection={collection} id={id} isEditing={isEditing} />
<DocumentControls
apiURL={apiURL}
collection={collection}
data={data}
disableActions={disableActions}
hasSavePermission={hasSavePermission}
id={id}
isEditing={isEditing}
permissions={permissions}
/>
<div className={`${baseClass}__wrapper`}>
<div className={`${baseClass}__main`}>
<Meta
description={`${isEditing ? t('editing') : t('creating')} - ${getTranslation(
collection.labels.singular,
i18n,
)}`}
keywords={`${getTranslation(collection.labels.singular, i18n)}, Payload, CMS`}
title={`${isEditing ? t('editing') : t('creating')} - ${getTranslation(
collection.labels.singular,
i18n,
)}`}
/>
{!(collection.versions?.drafts && collection.versions?.drafts?.autosave) &&
!disableLeaveWithoutSaving && <LeaveWithoutSaving />}
<Gutter className={`${baseClass}__edit`}>
{auth && (
<Auth
collection={collection}
email={data?.email}
operation={operation}
readOnly={!hasSavePermission}
requirePassword={!isEditing}
useAPIKey={auth.useAPIKey}
verify={auth.verify}
/>
)}
{upload && <Upload collection={collection} data={data} internalState={internalState} />}
<RenderFields
fieldSchema={fields}
fieldTypes={fieldTypes}
filter={(field) => !field?.admin?.position || field?.admin?.position !== 'sidebar'}
permissions={permissions.fields}
readOnly={!hasSavePermission}
/>
</Gutter>
</div>
<div className={`${baseClass}__sidebar-wrap`}>
<div className={`${baseClass}__sidebar`}>
<div className={`${baseClass}__sidebar-sticky-wrap`}>
<div className={`${baseClass}__sidebar-fields`}>
<RenderFields
fieldSchema={fields}
fieldTypes={fieldTypes}
filter={(field) => field?.admin?.position === 'sidebar'}
permissions={permissions.fields}
readOnly={!hasSavePermission}
/>
</div>
</div>
</div>
</div>
</div>
</Fragment>
)
}

View File

@@ -0,0 +1,61 @@
import type { match } from 'react-router-dom'
import React from 'react'
import { Route } from 'react-router-dom'
import type { CollectionPermission, User } from '../../../../../../auth'
import type { SanitizedCollectionConfig } from '../../../../../../exports/types'
import Unauthorized from '../../../Unauthorized'
export const collectionCustomRoutes = (props: {
collection?: SanitizedCollectionConfig
match: match<{
[key: string]: string | undefined
}>
permissions: CollectionPermission
user: User
}): React.ReactElement[] => {
const { collection, match, permissions, user } = props
let customViews = []
const internalViews = ['Default', 'Versions']
const BaseEdit = collection?.admin?.components?.views?.Edit
if (typeof BaseEdit !== 'function' && typeof BaseEdit === 'object') {
customViews = Object.entries(BaseEdit)
.filter(([viewKey, view]) => {
// Remove internal views from the list of custom views
// This way we can easily iterate over the remaining views
return Boolean(
!internalViews.includes(viewKey) &&
typeof view !== 'function' &&
typeof view === 'object',
)
})
?.map(([, view]) => view)
}
return customViews?.reduce((acc, { Component, path }) => {
const routesToReturn = [...acc]
if (collection) {
routesToReturn.push(
<Route
exact
key={`${collection.slug}-${path}`}
path={`${match.url}/collections/${collection.slug}/:id${path}`}
>
{permissions?.read?.permission ? (
<Component collection={collection} user={user} />
) : (
<Unauthorized />
)}
</Route>,
)
}
return routesToReturn
}, [])
}

View File

@@ -0,0 +1,62 @@
import { lazy } from 'react'
import React from 'react'
import { Route, Switch, useRouteMatch } from 'react-router-dom'
import { useAuth } from '../../../../utilities/Auth'
import { useConfig } from '../../../../utilities/Config'
import Version from '../../../Version'
import VersionsView from '../../../Versions'
import { DefaultEdit } from '../Default/index'
import { type Props } from '../types'
import { collectionCustomRoutes } from './custom'
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
const Unauthorized = lazy(() => import('../../../Unauthorized'))
export const CollectionRoutes: React.FC<Props> = (props) => {
const { collection, id, permissions } = props
const match = useRouteMatch()
const {
routes: { admin: adminRoute },
} = useConfig()
const { user } = useAuth()
return (
<Switch>
<Route
exact
key={`${collection.slug}-versions`}
path={`${adminRoute}/collections/${collection.slug}/:id/versions`}
>
{permissions?.readVersions?.permission ? (
<VersionsView collection={collection} id={id} />
) : (
<Unauthorized />
)}
</Route>
<Route
exact
key={`${collection.slug}-view-version`}
path={`${adminRoute}/collections/${collection.slug}/:id/versions/:versionID`}
>
{permissions?.readVersions?.permission ? (
<Version collection={collection} />
) : (
<Unauthorized />
)}
</Route>
{collectionCustomRoutes({
collection,
match,
permissions,
user,
})}
<Route>
<DefaultEdit {...props} />
</Route>
</Switch>
)
}

View File

@@ -6,299 +6,4 @@
&__form {
height: 100%;
}
&__main {
width: calc(100% - #{base(15)});
display: flex;
flex-direction: column;
min-height: 100%;
}
&__header {
h1 {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.25;
}
}
&__collection-actions {
list-style: none;
margin: 0;
padding: base(1.5) 0 base(0.5);
display: flex;
li {
[dir='ltr'] & {
margin-right: base(0.75);
}
[dir='rtl'] & {
margin-left: base(0.75);
}
}
}
&__edit {
padding-top: base(1);
padding-bottom: base(2);
flex-grow: 1;
}
&__sidebar-wrap {
position: fixed;
width: base(15);
height: 100%;
overflow: visible;
[dir='ltr'] & {
top: 0;
right: 0;
border-left: 1px solid var(--theme-elevation-100);
}
[dir='rtl'] & {
top: 0;
left: 0;
border-right: 1px solid var(--theme-elevation-100);
}
}
&__sidebar {
width: 100%;
height: 100%;
overflow-y: auto;
}
&__sidebar-sticky-wrap {
display: flex;
flex-direction: column;
min-height: 100%;
}
&__collection-actions,
&__document-actions,
&__meta,
&__sidebar-fields {
[dir='ltr'] & {
padding-left: base(1.5);
}
[dir='rtl'] & {
padding-right: base(1.5);
}
}
&__document-actions {
[dir='ltr'] & {
padding-right: $baseline;
}
[dir='rtl'] & {
padding-left: $baseline;
}
position: sticky;
top: 0;
z-index: var(--z-nav);
> * {
position: relative;
z-index: 1;
}
@include mid-break {
@include blur-bg;
}
}
&__document-actions--has-2 {
display: flex;
> * {
width: calc(50% - #{base(0.5)});
}
> *:first-child {
[dir='ltr'] & {
margin-right: base(0.5);
}
[dir='rtl'] & {
margin-left: base(0.5);
}
}
> *:last-child {
[dir='ltr'] & {
margin-left: base(0.5);
}
[dir='rtl'] & {
margin-right: base(0.5);
}
}
.form-submit {
.btn {
width: 100%;
padding-left: base(0.5);
padding-right: base(0.5);
}
}
}
&__api-url {
margin-bottom: base(1.5);
a {
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
}
&__sidebar-fields {
[dir='ltr'] & {
padding-right: $baseline;
}
[dir='rtl'] & {
padding-left: $baseline;
}
display: flex;
flex-direction: column;
gap: $baseline;
.preview-btn {
display: inline-block;
margin: 0;
width: calc(50% - #{base(0.5)});
}
.render-fields {
& > *:last-child {
margin-bottom: 0;
}
}
}
&__meta {
margin: auto 0 $baseline 0;
padding-top: $baseline;
[dir='ltr'] & {
padding-right: $baseline;
}
[dir='rtl'] & {
padding-left: $baseline;
}
list-style: none;
li {
margin-bottom: base(0.5);
&:last-child {
margin-bottom: 0;
}
}
}
&__label {
color: var(--theme-elevation-400);
}
&__collection-actions,
&__api-url {
a,
button {
cursor: pointer;
font-weight: 600;
text-decoration: none;
&:hover,
&:focus-visible {
text-decoration: underline;
}
}
}
&--is-editing {
.collection-edit__sidebar {
padding-top: 0;
}
}
@include mid-break {
&__main {
width: 100%;
min-height: initial;
}
&__sidebar-wrap {
position: static;
width: 100%;
height: initial;
}
&__meta {
border-top: 1px solid var(--theme-elevation-100);
[dir='ltr'] & {
padding-right: var(--gutter-h);
}
[dir='rtl'] & {
padding-left: var(--gutter-h);
}
}
&__form {
display: block;
}
&__edit {
padding-top: 0;
padding-bottom: 0;
}
&__document-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
top: auto;
z-index: var(--z-nav);
}
&__document-actions,
&__meta,
&__sidebar-fields {
padding-left: var(--gutter-h);
padding-right: var(--gutter-h);
}
&__sidebar-wrap {
border-left: 0;
}
&__sidebar-fields {
padding-top: 0;
[dir='ltr'] & {
padding-right: var(--gutter-h);
}
[dir='rtl'] & {
padding-left: var(--gutter-h);
}
gap: base(0.5);
.preview-btn {
width: 100%;
}
}
&__collection-actions {
border-top: 1px solid var(--theme-elevation-100);
padding: base(1) 0 0 var(--gutter-h);
order: 1;
li {
margin: 0 base(0.5) 0 0;
}
}
&__sidebar {
padding-bottom: base(3.5);
overflow: visible;
}
}
}

View File

@@ -17,7 +17,6 @@ export type Props = IndexProps & {
customHeader?: React.ReactNode
data: Document
disableActions?: boolean
disableEyebrow?: boolean
disableLeaveWithoutSaving?: boolean
hasSavePermission: boolean
id?: string

View File

@@ -9,7 +9,6 @@ import { getTranslation } from '../../../../../utilities/getTranslation'
import Button from '../../../elements/Button'
import DeleteMany from '../../../elements/DeleteMany'
import EditMany from '../../../elements/EditMany'
import Eyebrow from '../../../elements/Eyebrow'
import { Gutter } from '../../../elements/Gutter'
import ListControls from '../../../elements/ListControls'
import ListSelection from '../../../elements/ListSelection'
@@ -40,7 +39,6 @@ const DefaultList: React.FC<Props> = (props) => {
collection,
customHeader,
data,
disableEyebrow,
handlePageChange,
handlePerPageChange,
handleSortChange,
@@ -74,7 +72,6 @@ const DefaultList: React.FC<Props> = (props) => {
<Meta title={getTranslation(collection.labels.plural, i18n)} />
<SelectionProvider docs={data.docs} totalDocs={data.totalDocs}>
{!disableEyebrow && <Eyebrow />}
<Gutter className={`${baseClass}__wrap`}>
<header className={`${baseClass}__header`}>
{customHeader && customHeader}

View File

@@ -5,8 +5,7 @@
margin-bottom: base(2);
&__wrap {
padding-top: $baseline;
padding-bottom: $baseline;
padding-bottom: base(4);
}
&__header {
@@ -25,7 +24,9 @@
}
.pill {
margin: base(0.5) 0 base(0.25);
position: relative;
top: -8px;
margin: 0;
}
}
@@ -111,12 +112,6 @@
padding-bottom: 0;
}
&__header {
.pill {
margin-bottom: base(0.0625);
}
}
&__search-input {
margin: 0;
}
@@ -137,5 +132,11 @@
@include small-break {
margin-bottom: base(3);
&__header {
.pill {
top: -2px;
}
}
}
}

View File

@@ -9,7 +9,6 @@ export type Props = {
collection: SanitizedCollectionConfig
customHeader?: React.ReactNode
data: PaginatedDocs<any>
disableEyebrow?: boolean
handleDelete?: () => void
handlePageChange?: PaginatorProps['onChange']
handlePerPageChange?: PerPageProps['handleChange']

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "المغادرة بدون حفظ",
"light": "فاتح",
"loading": "يتمّ التّحميل",
"locale": "اللّغة",
"locales": "اللّغات",
"moveDown": "التّحريك إلى الأسفل",
"moveUp": "التّحريك إلى الأعلى",

View File

@@ -199,6 +199,7 @@
"leaveWithoutSaving": "Saxlamadan çıx",
"light": "Açıq",
"loading": "Yüklənir",
"locale": "Lokal",
"locales": "Dillər",
"moveDown": "Aşağı hərəkət et",
"moveUp": "Yuxarı hərəkət et",

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "Напусни без да запазиш",
"light": "Светла",
"loading": "Зарежда се",
"locale": "Локализация",
"locales": "Локализации",
"moveDown": "Надолу",
"moveUp": "Нагоре",

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "Odejít bez uložení",
"light": "Světlé",
"loading": "Načítání",
"locale": "Místní verze",
"locales": "Lokality",
"moveDown": "Posunout dolů",
"moveUp": "Posunout nahoru",

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "Ohne speichern verlassen",
"light": "Hell",
"loading": "Lädt",
"locale": "Sprachumgebung",
"locales": "Sprachumgebungen",
"moveDown": "Nach unten bewegen",
"moveUp": "Nach oben bewegen",

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "Leave without saving",
"light": "Light",
"loading": "Loading",
"locale": "Locale",
"locales": "Locales",
"moveDown": "Move Down",
"moveUp": "Move Up",

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "Salir sin guardar",
"light": "Claro",
"loading": "Cargando",
"locale": "Regional",
"locales": "Locales",
"moveDown": "Mover abajo",
"moveUp": "Mover arriba",

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "ترک کردن بدون ذخیره",
"light": "روشن",
"loading": "در حال بارگذاری",
"locale": "زبان",
"locales": "زبان‌ها",
"moveDown": "حرکت به پایین",
"moveUp": "حرکت به بالا",

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "Quitter sans sauvegarder",
"light": "Lumière ou Jour",
"loading": "Chargement en cours",
"locale": "Paramètres régionaux",
"locales": "Paramètres régionaux",
"moveDown": "Déplacer vers le bas",
"moveUp": "Déplacer vers le haut",

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "Napusti bez spremanja",
"light": "Svijetlo",
"loading": "Učitavanje",
"locale": "Jezik",
"locales": "Prijevodi",
"moveDown": "Pomakni dolje",
"moveUp": "Pomakni gore",

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "Távozás mentés nélkül",
"light": "Világos",
"loading": "Betöltés",
"locale": "Nyelv",
"locales": "Nyelvek",
"moveDown": "Mozgatás lefelé",
"moveUp": "Mozgatás felfelé",

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "Esci senza salvare",
"light": "Chiaro",
"loading": "Caricamento",
"locale": "Locale",
"locales": "Localizzazioni",
"moveDown": "Sposta sotto",
"moveUp": "Sposta sopra",

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "内容が保存されていません",
"light": "ライトモード",
"loading": "ローディング中",
"locale": "ロケール",
"locales": "ロケール",
"moveDown": "下へ移動",
"moveUp": "上へ移動",

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "မသိမ်းဘဲ ထွက်မည်။",
"light": "အလင်း",
"loading": "ဖွင့်နေသည်",
"locale": "ဒေသ",
"locales": "Locales",
"moveDown": "Move Down",
"moveUp": "Move Up",

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "Forlat uten å lagre",
"light": "Lys",
"loading": "Laster",
"locale": "Lokalitet",
"locales": "Språk",
"moveDown": "Flytt ned",
"moveUp": "Flytt opp",

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "Verlaten zonder op te slaan",
"light": "Licht",
"loading": "Laden",
"locale": "Taal",
"locales": "Landinstellingen",
"moveDown": "Verplaats naar beneden",
"moveUp": "Verplaats naar boven",

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "Wyjdź bez zapisywania",
"light": "Jasny",
"loading": "Ładowanie",
"locale": "Lokalizacja",
"locales": "Lokalne",
"moveDown": "Przesuń niżej",
"moveUp": "Przesuń wyżej",

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "Sair sem salvar",
"light": "Claro",
"loading": "Carregando",
"locale": "Local",
"locales": "Localizações",
"moveDown": "Mover para Baixo",
"moveUp": "Mover para Cima",

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "Plecare fără a salva",
"light": "Light",
"loading": "Încărcare",
"locale": "Localitate",
"locales": "Localuri",
"moveDown": "Mutați în jos",
"moveUp": "Mutați în sus",

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "Выход без сохранения",
"light": "Светлая",
"loading": "Загрузка",
"locale": "Локаль",
"locales": "Локали",
"moveDown": "Сдвинуть вниз",
"moveUp": "Сдвинуть вверх",

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "Lämna utan att spara",
"light": "Ljus",
"loading": "Läser in",
"locale": "Lokal",
"locales": "Språk",
"moveDown": "Flytta Ner",
"moveUp": "Flytta Upp",

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "ออกโดยไม่บันทึก",
"light": "สว่าง",
"loading": "กำลังโหลด",
"locale": "ตำแหน่งที่ตั้ง",
"locales": "ภาษา",
"moveDown": "ขยับขึ้น",
"moveUp": "ขยับลง",

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "Kaydetmeden ayrıl",
"light": "Aydınlık",
"loading": "Yükleniyor",
"locale": "Yerel ayar",
"locales": "Diller",
"moveDown": "Aşağı taşı",
"moveUp": "Yukarı taşı",

View File

@@ -736,6 +736,9 @@
"loading": {
"type": "string"
},
"locale": {
"type": "string"
},
"locales": {
"type": "string"
},

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "Вийти без збереження",
"light": "Світла",
"loading": "Загрузка",
"locale": "Локаль",
"locales": "Переклади",
"moveDown": "Перемістити нижче",
"moveUp": "Перемістити вище",

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "Thay đổi chưa được lưu",
"light": "Nền sáng",
"loading": "Đang tải",
"locale": "Ngôn ngữ",
"locales": "Khu vực",
"moveDown": "Di chuyển xuống",
"moveUp": "Di chuyển lên",

View File

@@ -198,6 +198,7 @@
"leaveWithoutSaving": "离开而不保存",
"light": "亮色",
"loading": "加载中...",
"locale": "语言环境",
"locales": "语言环境",
"moveDown": "向下移动",
"moveUp": "向上移动",

View File

@@ -6,7 +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 { openDocControls, openMainMenu } from '../helpers'
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
import { initPayloadE2E } from '../helpers/configHelpers'
import {
@@ -217,14 +217,16 @@ describe('access control', () => {
await page.goto(docLevelAccessURL.edit(existingDoc.id))
// validate that the delete action is not displayed
const duplicateAction = page.locator('.collection-edit__collection-actions >> li').last()
await expect(duplicateAction).toContainText('Duplicate')
await openDocControls(page)
const deleteAction = page.locator('#action-delete')
await expect(deleteAction).toBeHidden()
await page.locator('#field-approvedForRemoval').check()
await page.locator('#action-save').click()
const deleteAction = page.locator('.collection-edit__collection-actions >> li').last()
await expect(deleteAction).toContainText('Delete')
await openDocControls(page)
const deleteAction2 = page.locator('#action-delete')
await expect(deleteAction2).toBeVisible()
})
})

View File

@@ -1,10 +1,9 @@
import React, { useEffect } from 'react'
import { Redirect } from 'react-router-dom'
import type { AdminView } from '../../../../../packages/payload/src/config/types'
import type { CustomAdminView } from '../../../../../packages/payload/src/config/types'
import Button from '../../../../../packages/payload/src/admin/components/elements/Button'
import Eyebrow from '../../../../../packages/payload/src/admin/components/elements/Eyebrow'
import { useStepNav } from '../../../../../packages/payload/src/admin/components/elements/StepNav'
// As this is the demo project, we import our dependencies from the `src` directory.
import DefaultTemplate from '../../../../../packages/payload/src/admin/components/templates/Default'
@@ -18,7 +17,7 @@ import Meta from '../../../../../packages/payload/src/admin/components/utilities
// import { useStepNav } from 'payload/components/hooks';
// import { useConfig, Meta } from 'payload/components/utilities';
const CustomDefaultRoute: AdminView = ({ canAccessAdmin, user }) => {
const CustomDefaultRoute: CustomAdminView = ({ canAccessAdmin, user }) => {
const {
routes: { admin: adminRoute },
} = useConfig()
@@ -48,11 +47,10 @@ const CustomDefaultRoute: AdminView = ({ canAccessAdmin, user }) => {
keywords="Custom React Components, Payload, CMS"
title="Custom Route with Default Template"
/>
<Eyebrow />
<div
style={{
paddingRight: 'var(--gutter-h)',
paddingLeft: 'var(--gutter-h)',
paddingRight: 'var(--gutter-h)',
}}
>
<h1>Custom Route</h1>

View File

@@ -2,7 +2,6 @@ import React, { Fragment, useEffect } from 'react'
import { Redirect, useParams } from 'react-router-dom'
import Button from '../../../../../packages/payload/src/admin/components/elements/Button'
import Eyebrow from '../../../../../packages/payload/src/admin/components/elements/Eyebrow'
import { useStepNav } from '../../../../../packages/payload/src/admin/components/elements/StepNav'
import { useConfig } from '../../../../../packages/payload/src/admin/components/utilities/Config'
import { type CustomAdminView } from '../../../../../packages/payload/src/config/types'
@@ -48,7 +47,6 @@ const CustomEditView: CustomAdminView = ({ canAccessAdmin, collection, global, u
return (
<Fragment>
<Eyebrow />
<div
style={{
paddingLeft: 'var(--gutter-h)',

View File

@@ -2,7 +2,6 @@ import React, { Fragment, useEffect } from 'react'
import { Redirect, useParams } from 'react-router-dom'
import Button from '../../../../../packages/payload/src/admin/components/elements/Button'
import Eyebrow from '../../../../../packages/payload/src/admin/components/elements/Eyebrow'
import { useStepNav } from '../../../../../packages/payload/src/admin/components/elements/StepNav'
import { useConfig } from '../../../../../packages/payload/src/admin/components/utilities/Config'
import { type CustomAdminView } from '../../../../../packages/payload/src/config/types'
@@ -45,7 +44,6 @@ const CustomVersionsView: CustomAdminView = ({ canAccessAdmin, collection, globa
return (
<Fragment>
<Eyebrow />
<div
style={{
paddingLeft: 'var(--gutter-h)',

View File

@@ -2,7 +2,6 @@ import React, { Fragment, useEffect } from 'react'
import { useParams } from 'react-router-dom'
import Button from '../../../../../packages/payload/src/admin/components/elements/Button'
import Eyebrow from '../../../../../packages/payload/src/admin/components/elements/Eyebrow'
import { useStepNav } from '../../../../../packages/payload/src/admin/components/elements/StepNav'
import { useConfig } from '../../../../../packages/payload/src/admin/components/utilities/Config'
import { type CustomAdminView } from '../../../../../packages/payload/src/config/types'
@@ -42,7 +41,6 @@ const CustomView: CustomAdminView = ({ collection, global }) => {
return (
<Fragment>
<Eyebrow />
<div
style={{
paddingLeft: 'var(--gutter-h)',

View File

@@ -60,10 +60,16 @@ export default buildConfigWithDefaults({
},
},
},
localization: {
locales: ['en', 'es'],
},
collections: [
{
slug: 'users',
auth: true,
admin: {
useAsTitle: 'email',
},
fields: [],
},
{
@@ -80,30 +86,20 @@ export default buildConfigWithDefaults({
},
{
slug,
labels: {
singular: {
en: 'Post en',
es: 'Post es',
},
plural: {
en: 'Posts en',
es: 'Posts es',
},
},
admin: {
description: { en: 'Description en', es: 'Description es' },
description: 'Description',
listSearchableFields: ['title', 'description', 'number'],
group: { en: 'One', es: 'Una' },
group: 'One',
useAsTitle: 'title',
defaultColumns: ['id', 'number', 'title', 'description', 'demoUIField'],
preview: () => 'https://payloadcms.com',
},
versions: {
drafts: true,
},
fields: [
{
name: 'title',
label: {
en: 'Title en',
es: 'Title es',
},
type: 'text',
},
{
@@ -124,7 +120,7 @@ export default buildConfigWithDefaults({
{
type: 'ui',
name: 'demoUIField',
label: { en: 'Demo UI Field', de: 'Demo UI Field de' },
label: 'Demo UI Field',
admin: {
components: {
Field: DemoUIFieldField,
@@ -179,7 +175,7 @@ export default buildConfigWithDefaults({
{
slug: 'group-one-collection-ones',
admin: {
group: { en: 'One', es: 'Una' },
group: 'One',
},
fields: [
{
@@ -191,7 +187,7 @@ export default buildConfigWithDefaults({
{
slug: 'group-one-collection-twos',
admin: {
group: { en: 'One', es: 'Una' },
group: 'One',
},
fields: [
{
@@ -249,13 +245,12 @@ export default buildConfigWithDefaults({
},
{
slug: globalSlug,
label: {
en: 'Global en',
es: 'Global es',
},
admin: {
group: 'Group',
},
versions: {
drafts: true,
},
fields: [
{
name: 'title',

Some files were not shown because too many files have changed in this diff Show More