chore: overhauls admin navigation (#3339)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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')}: </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')}: </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>
|
||||
)
|
||||
}
|
||||
@@ -135,7 +135,6 @@ const Content: React.FC<DocumentDrawerProps> = ({
|
||||
),
|
||||
data,
|
||||
disableActions: true,
|
||||
disableEyebrow: true,
|
||||
disableLeaveWithoutSaving: true,
|
||||
hasSavePermission,
|
||||
id,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
.doc-drawer {
|
||||
&__header {
|
||||
width: 100%;
|
||||
margin-top: base(2.5);
|
||||
margin-bottom: base(1);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
<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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
{id}
|
||||
|
||||
@@ -239,7 +239,6 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
</header>
|
||||
),
|
||||
data,
|
||||
disableEyebrow: true,
|
||||
handlePageChange: setPage,
|
||||
handlePerPageChange: setLimit,
|
||||
handleSortChange: setSort,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
<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
|
||||
/>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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`}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
@import '../../../scss/styles.scss';
|
||||
|
||||
.render-title {
|
||||
&__id {
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
top: -3px;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ const DefaultSaveDraftButton: React.FC<DefaultSaveDraftButtonProps> = ({
|
||||
disabled={disabled}
|
||||
onClick={saveDraft}
|
||||
ref={ref}
|
||||
size="small"
|
||||
type="button"
|
||||
>
|
||||
{label}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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')}: </span>
|
||||
<span className={`${baseClass}__value`}>{t(statusToRender)}</span>
|
||||
{canUpdate && statusToRender === 'published' && (
|
||||
<React.Fragment>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
@import '../../../scss/styles.scss';
|
||||
|
||||
.versions-count__button {
|
||||
font-weight: 600;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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} />)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}, [])
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}${
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}, [])
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}, [])
|
||||
}
|
||||
|
||||
@@ -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') })}
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 & {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}, [])
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ export type Props = IndexProps & {
|
||||
customHeader?: React.ReactNode
|
||||
data: Document
|
||||
disableActions?: boolean
|
||||
disableEyebrow?: boolean
|
||||
disableLeaveWithoutSaving?: boolean
|
||||
hasSavePermission: boolean
|
||||
id?: string
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
"leaveWithoutSaving": "المغادرة بدون حفظ",
|
||||
"light": "فاتح",
|
||||
"loading": "يتمّ التّحميل",
|
||||
"locale": "اللّغة",
|
||||
"locales": "اللّغات",
|
||||
"moveDown": "التّحريك إلى الأسفل",
|
||||
"moveUp": "التّحريك إلى الأعلى",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
"leaveWithoutSaving": "Напусни без да запазиш",
|
||||
"light": "Светла",
|
||||
"loading": "Зарежда се",
|
||||
"locale": "Локализация",
|
||||
"locales": "Локализации",
|
||||
"moveDown": "Надолу",
|
||||
"moveUp": "Нагоре",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
"leaveWithoutSaving": "Leave without saving",
|
||||
"light": "Light",
|
||||
"loading": "Loading",
|
||||
"locale": "Locale",
|
||||
"locales": "Locales",
|
||||
"moveDown": "Move Down",
|
||||
"moveUp": "Move Up",
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
"leaveWithoutSaving": "Salir sin guardar",
|
||||
"light": "Claro",
|
||||
"loading": "Cargando",
|
||||
"locale": "Regional",
|
||||
"locales": "Locales",
|
||||
"moveDown": "Mover abajo",
|
||||
"moveUp": "Mover arriba",
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
"leaveWithoutSaving": "ترک کردن بدون ذخیره",
|
||||
"light": "روشن",
|
||||
"loading": "در حال بارگذاری",
|
||||
"locale": "زبان",
|
||||
"locales": "زبانها",
|
||||
"moveDown": "حرکت به پایین",
|
||||
"moveUp": "حرکت به بالا",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
"leaveWithoutSaving": "Napusti bez spremanja",
|
||||
"light": "Svijetlo",
|
||||
"loading": "Učitavanje",
|
||||
"locale": "Jezik",
|
||||
"locales": "Prijevodi",
|
||||
"moveDown": "Pomakni dolje",
|
||||
"moveUp": "Pomakni gore",
|
||||
|
||||
@@ -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é",
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
"leaveWithoutSaving": "Esci senza salvare",
|
||||
"light": "Chiaro",
|
||||
"loading": "Caricamento",
|
||||
"locale": "Locale",
|
||||
"locales": "Localizzazioni",
|
||||
"moveDown": "Sposta sotto",
|
||||
"moveUp": "Sposta sopra",
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
"leaveWithoutSaving": "内容が保存されていません",
|
||||
"light": "ライトモード",
|
||||
"loading": "ローディング中",
|
||||
"locale": "ロケール",
|
||||
"locales": "ロケール",
|
||||
"moveDown": "下へ移動",
|
||||
"moveUp": "上へ移動",
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
"leaveWithoutSaving": "မသိမ်းဘဲ ထွက်မည်။",
|
||||
"light": "အလင်း",
|
||||
"loading": "ဖွင့်နေသည်",
|
||||
"locale": "ဒေသ",
|
||||
"locales": "Locales",
|
||||
"moveDown": "Move Down",
|
||||
"moveUp": "Move Up",
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
"leaveWithoutSaving": "Forlat uten å lagre",
|
||||
"light": "Lys",
|
||||
"loading": "Laster",
|
||||
"locale": "Lokalitet",
|
||||
"locales": "Språk",
|
||||
"moveDown": "Flytt ned",
|
||||
"moveUp": "Flytt opp",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
"leaveWithoutSaving": "Wyjdź bez zapisywania",
|
||||
"light": "Jasny",
|
||||
"loading": "Ładowanie",
|
||||
"locale": "Lokalizacja",
|
||||
"locales": "Lokalne",
|
||||
"moveDown": "Przesuń niżej",
|
||||
"moveUp": "Przesuń wyżej",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
"leaveWithoutSaving": "Выход без сохранения",
|
||||
"light": "Светлая",
|
||||
"loading": "Загрузка",
|
||||
"locale": "Локаль",
|
||||
"locales": "Локали",
|
||||
"moveDown": "Сдвинуть вниз",
|
||||
"moveUp": "Сдвинуть вверх",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
"leaveWithoutSaving": "ออกโดยไม่บันทึก",
|
||||
"light": "สว่าง",
|
||||
"loading": "กำลังโหลด",
|
||||
"locale": "ตำแหน่งที่ตั้ง",
|
||||
"locales": "ภาษา",
|
||||
"moveDown": "ขยับขึ้น",
|
||||
"moveUp": "ขยับลง",
|
||||
|
||||
@@ -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şı",
|
||||
|
||||
@@ -736,6 +736,9 @@
|
||||
"loading": {
|
||||
"type": "string"
|
||||
},
|
||||
"locale": {
|
||||
"type": "string"
|
||||
},
|
||||
"locales": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
"leaveWithoutSaving": "Вийти без збереження",
|
||||
"light": "Світла",
|
||||
"loading": "Загрузка",
|
||||
"locale": "Локаль",
|
||||
"locales": "Переклади",
|
||||
"moveDown": "Перемістити нижче",
|
||||
"moveUp": "Перемістити вище",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
"leaveWithoutSaving": "离开而不保存",
|
||||
"light": "亮色",
|
||||
"loading": "加载中...",
|
||||
"locale": "语言环境",
|
||||
"locales": "语言环境",
|
||||
"moveDown": "向下移动",
|
||||
"moveUp": "向上移动",
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user