chore(next): ssr versions view (#5085)

This commit is contained in:
Jacob Fletcher
2024-02-15 10:42:55 -05:00
committed by GitHub
parent f11f3fdee1
commit c512693b9d
43 changed files with 993 additions and 788 deletions

View File

@@ -0,0 +1,232 @@
'use client'
import * as React from 'react'
import {
CopyToClipboard,
Gutter,
Checkbox,
SetDocumentStepNav as SetStepNav,
Form,
Select,
Number as NumberInput,
EditViewProps,
useConfig,
MinimizeMaximize,
useActions,
useTranslation,
useLocale,
} from '@payloadcms/ui'
import { RenderJSON } from './RenderJSON'
import { useSearchParams } from 'next/navigation'
import qs from 'qs'
import { toast } from 'react-toastify'
import './index.scss'
const baseClass = 'query-inspector'
export const APIViewClient: React.FC<EditViewProps> = (props) => {
const { data: initialData } = props
const searchParams = useSearchParams()
const { setViewActions } = useActions()
const { i18n } = useTranslation()
const { code } = useLocale()
const {
localization,
routes: { api: apiRoute },
serverURL,
collections,
globals,
} = useConfig()
const collectionConfig =
'collectionSlug' in props &&
collections.find((collection) => collection.slug === props.collectionSlug)
const globalConfig =
'globalSlug' in props && globals.find((global) => global.slug === props.globalSlug)
const id = 'id' in props ? props.id : undefined
const collectionSlug = collectionConfig?.slug
const globalSlug = globalConfig?.slug
const localeOptions =
localization &&
localization.locales.map((locale) => ({ label: locale.label, value: locale.code }))
const isEditing = Boolean(globalSlug || (collectionSlug && !!id))
let draftsEnabled: boolean = false
let docEndpoint: string = ''
if (collectionConfig) {
draftsEnabled = Boolean(collectionConfig.versions?.drafts)
docEndpoint = `/${collectionSlug}/${id}`
}
if (globalConfig) {
draftsEnabled = Boolean(globalConfig.versions?.drafts)
docEndpoint = `/globals/${globalSlug}`
}
const [data, setData] = React.useState<any>(initialData)
const [draft, setDraft] = React.useState<boolean>(searchParams.get('draft') === 'true')
const [locale, setLocale] = React.useState<string>(searchParams?.get('locale') || code)
const [depth, setDepth] = React.useState<string>(searchParams.get('depth') || '1')
const [authenticated, setAuthenticated] = React.useState<boolean>(true)
const [fullscreen, setFullscreen] = React.useState<boolean>(false)
const fetchURL = `${serverURL}${apiRoute}${docEndpoint}${qs.stringify(
{
locale,
draft,
depth,
},
{ addQueryPrefix: true },
)}`
React.useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(fetchURL, {
method: 'GET',
credentials: authenticated ? 'include' : 'omit',
headers: {
'Accept-Language': i18n.language,
},
})
try {
const json = await res.json()
setData(json)
} catch (error) {
toast.error('Error parsing response')
console.error(error)
}
} catch (error) {
toast.error('Error making request')
console.error(error)
}
}
fetchData()
}, [i18n.language, fetchURL, authenticated])
React.useEffect(() => {
const editConfig = (collectionConfig || globalConfig)?.admin?.components?.views?.Edit
const apiActions =
editConfig && 'API' in editConfig && 'actions' in editConfig.API ? editConfig.API.actions : []
setViewActions(apiActions)
return () => {
setViewActions([])
}
}, [collectionConfig, globalConfig, setViewActions])
return (
<Gutter
className={[baseClass, fullscreen && `${baseClass}--fullscreen`].filter(Boolean).join(' ')}
right={false}
>
<SetStepNav
collectionSlug={collectionSlug}
useAsTitle={collectionConfig?.admin?.useAsTitle}
pluralLabel={collectionConfig?.labels.plural}
globalLabel={globalConfig?.label}
globalSlug={globalSlug}
id={id}
isEditing={isEditing}
view="API"
/>
<div className={`${baseClass}__configuration`}>
<div className={`${baseClass}__api-url`}>
<span className={`${baseClass}__label`}>
API URL <CopyToClipboard value={fetchURL} />
</span>
<a href={fetchURL} rel="noopener noreferrer" target="_blank">
{fetchURL}
</a>
</div>
<Form
initialState={{
authenticated: {
value: authenticated || false,
initialValue: authenticated || false,
valid: true,
},
draft: {
value: draft || false,
initialValue: draft || false,
valid: true,
},
depth: {
value: Number(depth || 0),
initialValue: Number(depth || 0),
valid: true,
},
locale: {
value: locale,
initialValue: locale,
valid: true,
},
}}
>
<div className={`${baseClass}__form-fields`}>
<div className={`${baseClass}__filter-query-checkboxes`}>
{draftsEnabled && (
<Checkbox
name="draft"
path="draft"
label="Draft"
onChange={() => setDraft(!draft)}
/>
)}
<Checkbox
name="authenticated"
path="authenticated"
label="Authenticated"
onChange={() => setAuthenticated(!authenticated)}
/>
</div>
{localeOptions && (
<Select
label="Locale"
name="locale"
options={localeOptions}
path="locale"
onChange={(value) => setLocale(value)}
/>
)}
<NumberInput
label="Depth"
name="depth"
path="depth"
min={0}
max={10}
step={1}
onChange={(value) => setDepth(value.toString())}
/>
</div>
</Form>
</div>
<div className={`${baseClass}__results-wrapper`}>
<div className={`${baseClass}__toggle-fullscreen-button-container`}>
<button
aria-label="toggle fullscreen"
className={`${baseClass}__toggle-fullscreen-button`}
onClick={() => setFullscreen(!fullscreen)}
type="button"
>
<MinimizeMaximize isMinimized={!fullscreen} />
</button>
</div>
<div className={`${baseClass}__results`}>
<RenderJSON object={data} />
</div>
</div>
</Gutter>
)
}

View File

@@ -1,232 +1,9 @@
'use client'
import * as React from 'react'
import React from 'react'
import { ServerSideEditViewProps } from '../../../../ui/src/views/types'
import { APIViewClient } from './index.client'
import { sanitizedEditViewProps } from '../Edit/sanitizedEditViewProps'
import {
CopyToClipboard,
Gutter,
Checkbox,
SetDocumentStepNav as SetStepNav,
Form,
Select,
Number as NumberInput,
EditViewProps,
useConfig,
MinimizeMaximize,
useActions,
useTranslation,
useLocale,
} from '@payloadcms/ui'
import { RenderJSON } from './RenderJSON'
import { useSearchParams } from 'next/navigation'
import qs from 'qs'
import { toast } from 'react-toastify'
import './index.scss'
const baseClass = 'query-inspector'
export const APIView: React.FC<EditViewProps> = (props) => {
const { data: initialData } = props
const searchParams = useSearchParams()
const { setViewActions } = useActions()
const { i18n } = useTranslation()
const { code } = useLocale()
const {
localization,
routes: { api: apiRoute },
serverURL,
collections,
globals,
} = useConfig()
const collectionConfig =
'collectionSlug' in props &&
collections.find((collection) => collection.slug === props.collectionSlug)
const globalConfig =
'globalSlug' in props && globals.find((global) => global.slug === props.globalSlug)
const id = 'id' in props ? props.id : undefined
const collectionSlug = collectionConfig?.slug
const globalSlug = globalConfig?.slug
const localeOptions =
localization &&
localization.locales.map((locale) => ({ label: locale.label, value: locale.code }))
const isEditing = Boolean(globalSlug || (collectionSlug && !!id))
let draftsEnabled: boolean = false
let docEndpoint: string = ''
if (collectionConfig) {
draftsEnabled = Boolean(collectionConfig.versions?.drafts)
docEndpoint = `/${collectionSlug}/${id}`
}
if (globalConfig) {
draftsEnabled = Boolean(globalConfig.versions?.drafts)
docEndpoint = `/globals/${globalSlug}`
}
const [data, setData] = React.useState<any>(initialData)
const [draft, setDraft] = React.useState<boolean>(searchParams.get('draft') === 'true')
const [locale, setLocale] = React.useState<string>(searchParams?.get('locale') || code)
const [depth, setDepth] = React.useState<string>(searchParams.get('depth') || '1')
const [authenticated, setAuthenticated] = React.useState<boolean>(true)
const [fullscreen, setFullscreen] = React.useState<boolean>(false)
const fetchURL = `${serverURL}${apiRoute}${docEndpoint}${qs.stringify(
{
locale,
draft,
depth,
},
{ addQueryPrefix: true },
)}`
React.useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(fetchURL, {
method: 'GET',
credentials: authenticated ? 'include' : 'omit',
headers: {
'Accept-Language': i18n.language,
},
})
try {
const json = await res.json()
setData(json)
} catch (error) {
toast.error('Error parsing response')
console.error(error)
}
} catch (error) {
toast.error('Error making request')
console.error(error)
}
}
fetchData()
}, [i18n.language, fetchURL, authenticated])
React.useEffect(() => {
const editConfig = (collectionConfig || globalConfig)?.admin?.components?.views?.Edit
const apiActions =
editConfig && 'API' in editConfig && 'actions' in editConfig.API ? editConfig.API.actions : []
setViewActions(apiActions)
return () => {
setViewActions([])
}
}, [collectionConfig, globalConfig, setViewActions])
return (
<Gutter
className={[baseClass, fullscreen && `${baseClass}--fullscreen`].filter(Boolean).join(' ')}
right={false}
>
<SetStepNav
collectionSlug={collectionSlug}
useAsTitle={collectionConfig?.admin?.useAsTitle}
pluralLabel={collectionConfig?.labels.plural}
globalLabel={globalConfig?.label}
globalSlug={globalSlug}
id={id}
isEditing={isEditing}
view="API"
/>
<div className={`${baseClass}__configuration`}>
<div className={`${baseClass}__api-url`}>
<span className={`${baseClass}__label`}>
API URL <CopyToClipboard value={fetchURL} />
</span>
<a href={fetchURL} rel="noopener noreferrer" target="_blank">
{fetchURL}
</a>
</div>
<Form
initialState={{
authenticated: {
value: authenticated || false,
initialValue: authenticated || false,
valid: true,
},
draft: {
value: draft || false,
initialValue: draft || false,
valid: true,
},
depth: {
value: Number(depth || 0),
initialValue: Number(depth || 0),
valid: true,
},
locale: {
value: locale,
initialValue: locale,
valid: true,
},
}}
>
<div className={`${baseClass}__form-fields`}>
<div className={`${baseClass}__filter-query-checkboxes`}>
{draftsEnabled && (
<Checkbox
name="draft"
path="draft"
label="Draft"
onChange={() => setDraft(!draft)}
/>
)}
<Checkbox
name="authenticated"
path="authenticated"
label="Authenticated"
onChange={() => setAuthenticated(!authenticated)}
/>
</div>
{localeOptions && (
<Select
label="Locale"
name="locale"
options={localeOptions}
path="locale"
onChange={(value) => setLocale(value)}
/>
)}
<NumberInput
label="Depth"
name="depth"
path="depth"
min={0}
max={10}
step={1}
onChange={(value) => setDepth(value.toString())}
/>
</div>
</Form>
</div>
<div className={`${baseClass}__results-wrapper`}>
<div className={`${baseClass}__toggle-fullscreen-button-container`}>
<button
aria-label="toggle fullscreen"
className={`${baseClass}__toggle-fullscreen-button`}
onClick={() => setFullscreen(!fullscreen)}
type="button"
>
<MinimizeMaximize isMinimized={!fullscreen} />
</button>
</div>
<div className={`${baseClass}__results`}>
<RenderJSON object={data} />
</div>
</div>
</Gutter>
)
export const APIView: React.FC<ServerSideEditViewProps> = async (props) => {
const clientSideProps = sanitizedEditViewProps(props)
return <APIViewClient {...clientSideProps} />
}

View File

@@ -9,7 +9,7 @@ export const getCustomViewByPath = (
): AdminViewComponent => {
if (typeof views?.Edit === 'object' && typeof views?.Edit !== 'function') {
const foundViewConfig = Object.entries(views.Edit).find(([, view]) => {
if (typeof view === 'object' && typeof view !== 'function') {
if (typeof view === 'object' && typeof view !== 'function' && 'path' in view) {
return view.path === path
}
return false

View File

@@ -43,7 +43,7 @@ export const getViewsFromConfig = async ({
if ('create' in docPermissions && docPermissions?.create?.permission) {
CustomView = getCustomViewByKey(views, 'Default')
DefaultView = lazy(() =>
import('@payloadcms/ui').then((module) => ({ default: module.DefaultEditView })),
import('../Edit/index.tsx').then((module) => ({ default: module.EditView })),
)
}
break
@@ -53,7 +53,7 @@ export const getViewsFromConfig = async ({
if (docPermissions?.read?.permission) {
CustomView = getCustomViewByKey(views, 'Default')
DefaultView = lazy(() =>
import('@payloadcms/ui').then((module) => ({ default: module.DefaultEditView })),
import('../Edit/index.tsx').then((module) => ({ default: module.EditView })),
)
}
}
@@ -120,7 +120,7 @@ export const getViewsFromConfig = async ({
if (docPermissions?.read?.permission) {
CustomView = getCustomViewByKey(views, 'Default')
DefaultView = lazy(() =>
import('@payloadcms/ui').then((module) => ({ default: module.DefaultEditView })),
import('../Edit/index.tsx').then((module) => ({ default: module.EditView })),
)
}
} else if (routeSegments?.length === 1) {
@@ -161,7 +161,7 @@ export const getViewsFromConfig = async ({
if (docPermissions?.read?.permission) {
CustomView = getCustomViewByKey(views, 'Default')
DefaultView = lazy(() =>
import('@payloadcms/ui').then((module) => ({ default: module.DefaultEditView })),
import('../Edit/index.tsx').then((module) => ({ default: module.EditView })),
)
}
break

View File

@@ -16,12 +16,12 @@ import {
HydrateClientUser,
DocumentInfoProvider,
} from '@payloadcms/ui'
import type { EditViewProps } from '@payloadcms/ui'
import queryString from 'qs'
import { notFound } from 'next/navigation'
import { AdminViewComponent } from 'payload/config'
import { getViewsFromConfig } from './getViewsFromConfig'
import type { DocumentPermissions } from 'payload/types'
import { ServerSideEditViewProps } from '../../../../ui/src/views/types'
export const Document = async ({
params,
@@ -167,7 +167,7 @@ export const Document = async ({
uploadEdits: undefined,
}
const componentProps: EditViewProps = {
const componentProps: ServerSideEditViewProps = {
id,
action: `${action}?${queryString.stringify(formQueryParams)}`,
apiURL,
@@ -183,6 +183,14 @@ export const Document = async ({
updatedAt: data?.updatedAt?.toString(),
user,
locale,
payload,
config,
searchParams,
i18n,
collectionConfig,
globalConfig,
params,
permissions,
}
return (

View File

@@ -0,0 +1,9 @@
import React from 'react'
import { DefaultEditView } from '@payloadcms/ui'
import { ServerSideEditViewProps } from '../../../../ui/src/views/types'
import { sanitizedEditViewProps } from './sanitizedEditViewProps'
export const EditView: React.FC<ServerSideEditViewProps> = async (props) => {
const clientSideProps = sanitizedEditViewProps(props)
return <DefaultEditView {...clientSideProps} />
}

View File

@@ -0,0 +1,13 @@
import { EditViewProps, ServerSideEditViewProps } from '@payloadcms/ui'
export const sanitizedEditViewProps = (props: ServerSideEditViewProps) => {
const clientSideProps = { ...props }
delete clientSideProps.payload
delete clientSideProps.config
delete clientSideProps.searchParams
delete clientSideProps.i18n
delete clientSideProps.collectionConfig
delete clientSideProps.globalConfig
return clientSideProps as EditViewProps
}

View File

@@ -0,0 +1,114 @@
import { getTranslation } from '@payloadcms/translations'
import {
FieldMap,
StepNavItem,
formatDate,
useConfig,
useLocale,
useStepNav,
useTranslation,
} from '@payloadcms/ui'
import { FieldAffectingData, SanitizedCollectionConfig } from 'payload/types'
import React, { useEffect } from 'react'
export const SetStepNav: React.FC<{
collectionSlug?: string
globalSlug?: string
mostRecentDoc: any
doc: any
id?: string | number
fieldMap: FieldMap
collectionConfig?: SanitizedCollectionConfig
}> = ({ collectionSlug, globalSlug, mostRecentDoc, doc, id, fieldMap, collectionConfig }) => {
const config = useConfig()
const { setStepNav } = useStepNav()
const { i18n, t } = useTranslation()
const locale = useLocale()
useEffect(() => {
let nav: StepNavItem[] = []
const {
admin: { dateFormat },
routes: { admin: adminRoute },
} = config
if (collectionSlug) {
let docLabel = ''
const useAsTitle = collectionConfig?.admin?.useAsTitle || 'id'
const pluralLabel = collectionConfig?.labels?.plural
if (mostRecentDoc) {
if (useAsTitle !== 'id') {
const titleField = fieldMap.find(
({ isFieldAffectingData, name: fieldName }) =>
isFieldAffectingData && fieldName === useAsTitle,
) as FieldAffectingData
if (titleField && mostRecentDoc[useAsTitle]) {
if (titleField.localized) {
docLabel = mostRecentDoc[useAsTitle]?.[locale.code]
} else {
docLabel = mostRecentDoc[useAsTitle]
}
} else {
docLabel = `[${t('general:untitled')}]`
}
} else {
docLabel = mostRecentDoc.id
}
}
nav = [
{
label: getTranslation(pluralLabel, i18n),
url: `${adminRoute}/collections/${collectionSlug}`,
},
{
label: docLabel,
url: `${adminRoute}/collections/${collectionSlug}/${id}`,
},
{
label: 'Versions',
url: `${adminRoute}/collections/${collectionSlug}/${id}/versions`,
},
{
label: doc?.createdAt ? formatDate(doc.createdAt, dateFormat, i18n?.language) : '',
},
]
}
if (globalSlug) {
nav = [
{
label: global.label,
url: `${adminRoute}/globals/${global.slug}`,
},
{
label: 'Versions',
url: `${adminRoute}/globals/${global.slug}/versions`,
},
{
label: doc?.createdAt ? formatDate(doc.createdAt, dateFormat, i18n?.language) : '',
},
]
}
setStepNav(nav)
}, [
config,
setStepNav,
collectionSlug,
globalSlug,
doc,
mostRecentDoc,
id,
locale,
t,
i18n,
collectionConfig,
])
return null
}

View File

@@ -1,15 +1,24 @@
import React from 'react'
import type { FieldAffectingData } from 'payload/types'
import type { StepNavItem } from '@payloadcms/ui'
import type { DefaultVersionsViewProps } from './types'
import { fieldAffectsData } from 'payload/types'
import { Gutter, SetStepNav, formatDate } from '@payloadcms/ui'
import RenderFieldsToDiff from '../RenderFieldsToDiff'
import fieldComponents from '../RenderFieldsToDiff/fields'
'use client'
import React, { useState } from 'react'
import type { CompareOption, DefaultVersionsViewProps } from './types'
import {
Gutter,
Option,
formatDate,
useComponentMap,
useConfig,
usePayloadAPI,
useTranslation,
} from '@payloadcms/ui'
import Restore from '../Restore'
import './index.scss'
import { mostRecentVersionOption } from '../shared'
import { getTranslation } from '@payloadcms/translations'
import diffComponents from '../RenderFieldsToDiff/fields'
import RenderFieldsToDiff from '../RenderFieldsToDiff'
import { SetStepNav } from './SetStepNav'
import { SelectLocales } from '../SelectLocales'
import { SelectComparison } from '../SelectComparison'
import './index.scss'
const baseClass = 'view-version'
@@ -17,85 +26,38 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
doc,
mostRecentDoc,
publishedDoc,
compareDoc,
locales,
initialComparisonDoc,
localeOptions,
docPermissions,
fields,
config,
collectionConfig,
globalConfig,
collectionSlug,
globalSlug,
id,
versionID,
locale,
i18n,
}) => {
const config = useConfig()
const { i18n } = useTranslation()
const { getFieldMap } = useComponentMap()
const [fieldMap] = useState(() => getFieldMap({ collectionSlug, globalSlug }))
const [collectionConfig] = useState(() =>
config.collections.find((collection) => collection.slug === collectionSlug),
)
const [globalConfig] = useState(() => config.globals.find((global) => global.slug === globalSlug))
const [locales, setLocales] = useState<Option[]>(localeOptions)
const [compareValue, setCompareValue] = useState<CompareOption>(mostRecentVersionOption)
const {
routes: { admin },
admin: { dateFormat },
routes: { api: apiRoute },
localization,
serverURL,
} = config
let nav: StepNavItem[] = []
if (collectionConfig) {
let docLabel = ''
if (mostRecentDoc) {
const { useAsTitle } = collectionConfig.admin
if (useAsTitle !== 'id') {
const titleField = collectionConfig.fields.find(
(field) => fieldAffectsData(field) && field.name === useAsTitle,
) as FieldAffectingData
if (titleField && mostRecentDoc[useAsTitle]) {
if (titleField.localized) {
docLabel = mostRecentDoc[useAsTitle]?.[locale]
} else {
docLabel = mostRecentDoc[useAsTitle]
}
} else {
docLabel = `[${i18n.t('general:untitled')}]`
}
} else {
docLabel = mostRecentDoc.id
}
}
nav = [
{
label: getTranslation(collectionConfig.labels.plural, i18n),
url: `${admin}/collections/${collectionConfig.slug}`,
},
{
label: docLabel,
url: `${admin}/collections/${collectionConfig.slug}/${id}`,
},
{
label: 'Versions',
url: `${admin}/collections/${collectionConfig.slug}/${id}/versions`,
},
{
label: doc?.createdAt ? formatDate(doc.createdAt, dateFormat, i18n.language) : '',
},
]
}
if (globalConfig) {
nav = [
{
label: globalConfig.label,
url: `${admin}/globals/${globalConfig.slug}`,
},
{
label: i18n.t('version:versions'),
url: `${admin}/globals/${globalConfig.slug}/versions`,
},
{
label: doc?.createdAt ? formatDate(doc.createdAt, dateFormat, i18n.language) : '',
},
]
}
// useEffect(() => {
// const editConfig = (collectionConfig || globalConfig)?.admin?.components?.views?.Edit
// const versionActions =
@@ -110,30 +72,49 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
? formatDate(doc.createdAt, dateFormat, i18n.language)
: ''
// TODO: this value should ultimately be dynamic based on the user's selection
// This will come from URL params
let compareValue = mostRecentVersionOption
const originalDocFetchURL = `${serverURL}${apiRoute}${globalSlug ? 'globals/' : ''}/${
collectionSlug || globalSlug
}${collectionSlug ? `/${id}` : ''}`
let comparison = compareDoc?.version
const compareBaseURL = `${serverURL}${apiRoute}/${globalSlug ? 'globals/' : ''}${
collectionSlug || globalSlug
}/versions`
if (compareValue?.value === 'mostRecent') {
comparison = mostRecentDoc
}
const compareFetchURL =
compareValue?.value === 'mostRecent' || compareValue?.value === 'published'
? originalDocFetchURL
: `${compareBaseURL}/${compareValue.value}`
if (compareValue?.value === 'published') {
comparison = publishedDoc
}
const [{ data: currentComparisonDoc }] = usePayloadAPI(compareFetchURL, {
initialParams: { depth: 1, draft: 'true', locale: '*' },
initialData: initialComparisonDoc,
})
const comparison =
compareValue?.value === 'mostRecent'
? mostRecentDoc
: compareValue?.value === 'published'
? publishedDoc
: currentComparisonDoc?.version // the `version` key is only present on `versions` documents
const canUpdate = docPermissions?.update?.permission
return (
<main className={baseClass}>
<SetStepNav nav={nav} />
<SetStepNav
collectionSlug={collectionSlug}
globalSlug={globalSlug}
mostRecentDoc={mostRecentDoc}
doc={doc}
id={id}
fieldMap={fieldMap}
collectionConfig={collectionConfig}
/>
<Gutter className={`${baseClass}__wrap`}>
<div className={`${baseClass}__header-wrap`}>
<p className={`${baseClass}__created-at`}>
{i18n.t('version:versionCreatedOn', {
version: i18n.t(doc?.autosave ? 'autosavedVersion' : 'version'),
version: i18n.t(doc?.autosave ? 'version:autosavedVersion' : 'version:version'),
})}
</p>
<header className={`${baseClass}__header`}>
@@ -141,8 +122,8 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
{canUpdate && (
<Restore
className={`${baseClass}__restore`}
collectionSlug={collectionConfig?.slug}
globalSlug={globalConfig?.slug}
collectionSlug={collectionSlug}
globalSlug={globalSlug}
label={collectionConfig?.labels.singular || globalConfig?.label}
originalDocID={id}
versionDate={formattedCreatedAt}
@@ -152,24 +133,23 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
</header>
</div>
<div className={`${baseClass}__controls`}>
{/* <CompareVersion
<SelectComparison
baseURL={compareBaseURL}
onChange={setCompareValue}
parentID={parentID}
parentID={id}
publishedDoc={publishedDoc}
value={compareValue}
versionID={versionID}
/> */}
{/* {localization && (
/>
{localization && (
<SelectLocales onChange={setLocales} options={localeOptions} value={locales} />
)} */}
)}
</div>
{doc?.version && (
<RenderFieldsToDiff
comparison={comparison}
fieldComponents={fieldComponents}
fieldPermissions={docPermissions?.fields}
fields={fields}
fieldMap={fieldMap}
locales={
locales
? locales.map(({ label }) => (typeof label === 'string' ? label : undefined))
@@ -177,8 +157,7 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
}
version={doc?.version}
i18n={i18n}
locale={locale}
config={config}
diffComponents={diffComponents}
/>
)}
</Gutter>

View File

@@ -1,18 +1,7 @@
import { I18n } from '@payloadcms/translations'
import {
CollectionPermission,
FieldPermissions,
GlobalPermission,
Permissions,
User,
} from 'payload/auth'
import {
Document,
Field,
SanitizedCollectionConfig,
SanitizedConfig,
SanitizedGlobalConfig,
} from 'payload/types'
import { Option } from '@payloadcms/ui'
import { CollectionPermission, GlobalPermission, Permissions, User } from 'payload/auth'
import { Document, SanitizedCollectionConfig } from 'payload/types'
export type CompareOption = {
label: string
@@ -25,17 +14,13 @@ export type DefaultVersionsViewProps = {
doc: Document
mostRecentDoc: Document
publishedDoc: Document
compareDoc: Document
fields: Field[]
locales: CompareOption[]
initialComparisonDoc: Document
localeOptions: Option[]
user: User
permissions: Permissions
config: SanitizedConfig
collectionConfig?: SanitizedCollectionConfig
globalConfig?: SanitizedGlobalConfig
id?: string
id?: string | number
versionID?: string
docPermissions: CollectionPermission | GlobalPermission
locale: string
i18n: I18n
collectionSlug?: SanitizedCollectionConfig['slug']
globalSlug?: SanitizedCollectionConfig['slug']
}

View File

@@ -1,27 +1,27 @@
import { getTranslation } from '@payloadcms/translations'
import React from 'react'
import type { ArrayField, BlockField, Field } from 'payload/types'
import type { Field } from 'payload/types'
import type { Props } from '../types'
import RenderFieldsToDiff from '../..'
import { fieldAffectsData } from 'payload/types'
import { getUniqueListBy } from 'payload/utilities'
import Label from '../../Label'
import { MappedField } from '@payloadcms/ui'
import './index.scss'
const baseClass = 'iterable-diff'
const Iterable: React.FC<Props & { field: ArrayField | BlockField }> = ({
const Iterable: React.FC<Props> = ({
comparison,
field,
fieldComponents,
locale,
locales,
permissions,
version,
i18n,
config,
diffComponents,
}) => {
const versionRowCount = Array.isArray(version) ? version.length : 0
const comparisonRowCount = Array.isArray(comparison) ? comparison.length : 0
@@ -41,28 +41,30 @@ const Iterable: React.FC<Props & { field: ArrayField | BlockField }> = ({
const versionRow = version?.[i] || {}
const comparisonRow = comparison?.[i] || {}
let subFields: Field[] = []
let subFields: MappedField[] = []
if (field.type === 'array') subFields = field.fields
if (field.type === 'array') subFields = field.subfields
if (field.type === 'blocks') {
subFields = [
{
name: 'blockType',
label: i18n.t('fields:blockType'),
type: 'text',
},
// {
// name: 'blockType',
// label: i18n.t('fields:blockType'),
// type: 'text',
// },
]
if (versionRow?.blockType === comparisonRow?.blockType) {
const matchedBlock = field.blocks.find(
(block) => block.slug === versionRow?.blockType,
) || { fields: [] }
subFields = [...subFields, ...matchedBlock.fields]
} else {
const matchedVersionBlock = field.blocks.find(
(block) => block.slug === versionRow?.blockType,
) || { fields: [] }
const matchedComparisonBlock = field.blocks.find(
(block) => block.slug === comparisonRow?.blockType,
) || { fields: [] }
@@ -78,17 +80,12 @@ const Iterable: React.FC<Props & { field: ArrayField | BlockField }> = ({
<div className={`${baseClass}__wrap`} key={i}>
<RenderFieldsToDiff
comparison={comparisonRow}
fieldComponents={fieldComponents}
fieldMap={subFields}
fieldPermissions={permissions}
fields={subFields.filter(
(subField) =>
!(fieldAffectsData(subField) && 'name' in subField && subField.name === 'id'),
)}
locales={locales}
version={versionRow}
i18n={i18n}
locale={locale}
config={config}
diffComponents={diffComponents}
/>
</div>
)

View File

@@ -1,7 +1,6 @@
import React from 'react'
import { getTranslation } from '@payloadcms/translations'
import type { FieldWithSubFields } from 'payload/types'
import type { Props } from '../types'
import RenderFieldsToDiff from '../..'
@@ -10,17 +9,17 @@ import './index.scss'
const baseClass = 'nested-diff'
const Nested: React.FC<Props & { field: FieldWithSubFields }> = ({
const Nested: React.FC<Props> = ({
comparison,
disableGutter = false,
field,
fieldComponents,
locale,
locales,
permissions,
version,
i18n,
config,
fieldMap,
diffComponents,
}) => {
return (
<div className={baseClass}>
@@ -37,14 +36,12 @@ const Nested: React.FC<Props & { field: FieldWithSubFields }> = ({
>
<RenderFieldsToDiff
comparison={comparison}
fieldComponents={fieldComponents}
fieldMap={fieldMap}
fieldPermissions={permissions}
fields={field.fields}
locales={locales}
version={version}
i18n={i18n}
locale={locale}
config={config}
diffComponents={diffComponents}
/>
</div>
</div>

View File

@@ -71,7 +71,6 @@ const Relationship: React.FC<Props & { field: RelationshipField }> = ({
version,
i18n,
locale,
config: { collections },
}) => {
let placeholder = ''

View File

@@ -1,6 +1,5 @@
import { getTranslation, I18n } from '@payloadcms/translations'
import React from 'react'
import { DiffMethod } from 'react-diff-viewer-continued'
import type { OptionObject, SelectField } from 'payload/types'
import type { Props } from '../types'
@@ -52,6 +51,7 @@ const Select: React.FC<Props> = ({ comparison, diffMethod, field, locale, versio
typeof comparison !== 'undefined'
? getTranslatedOptions(getOptionsToRender(comparison, field.options, field.hasMany), i18n)
: placeholder
const versionToRender =
typeof version !== 'undefined'
? getTranslatedOptions(getOptionsToRender(version, field.options, field.hasMany), i18n)
@@ -61,10 +61,10 @@ const Select: React.FC<Props> = ({ comparison, diffMethod, field, locale, versio
<div className={baseClass}>
<Label>
{locale && <span className={`${baseClass}__locale-label`}>{locale}</span>}
{getTranslation(field.label, i18n)}
{getTranslation(field.label || '', i18n)}
</Label>
<DiffViewer
diffMethod={diffMethod as DiffMethod}
diffMethod={diffMethod}
versionToRender={versionToRender}
comparisonToRender={comparisonToRender}
placeholder={placeholder}

View File

@@ -1,6 +1,5 @@
import React from 'react'
import type { TabsField } from 'payload/types'
import type { Props } from '../types'
import RenderFieldsToDiff from '../..'
@@ -8,54 +7,53 @@ import Nested from '../Nested'
const baseClass = 'tabs-diff'
const Tabs: React.FC<Props & { field: TabsField }> = ({
const Tabs: React.FC<Props> = ({
comparison,
field,
fieldComponents,
locales,
permissions,
version,
i18n,
config,
locale,
}) => (
<div className={baseClass}>
<div className={`${baseClass}__wrap`}>
{field.tabs.map((tab, i) => {
if ('name' in tab) {
diffComponents,
}) => {
return (
<div className={baseClass}>
<div className={`${baseClass}__wrap`}>
{field.tabs.map((tab, i) => {
if ('name' in tab) {
return (
<Nested
comparison={comparison?.[tab.name]}
fieldMap={tab.subfields}
key={i}
locales={locales}
permissions={permissions}
version={version?.[tab.name]}
i18n={i18n}
locale={locale}
field={field}
diffComponents={diffComponents}
/>
)
}
return (
<Nested
comparison={comparison?.[tab.name]}
field={tab}
fieldComponents={fieldComponents}
<RenderFieldsToDiff
comparison={comparison}
fieldMap={tab.subfields}
fieldPermissions={permissions}
key={i}
locales={locales}
permissions={permissions}
version={version?.[tab.name]}
version={version}
i18n={i18n}
config={config}
locale={locale}
diffComponents={diffComponents}
/>
)
}
return (
<RenderFieldsToDiff
comparison={comparison}
fieldComponents={fieldComponents}
fieldPermissions={permissions}
fields={tab.fields}
key={i}
locales={locales}
version={version}
i18n={i18n}
config={config}
locale={locale}
/>
)
})}
})}
</div>
</div>
</div>
)
)
}
export default Tabs

View File

@@ -5,9 +5,10 @@ import type { Props } from '../types'
import Label from '../../Label'
import { diffStyles } from '../styles'
import './index.scss'
import { DiffViewer } from './DiffViewer'
import './index.scss'
const baseClass = 'text-diff'
const Text: React.FC<Props> = ({
@@ -35,8 +36,7 @@ const Text: React.FC<Props> = ({
<div className={baseClass}>
<Label>
{locale && <span className={`${baseClass}__locale-label`}>{locale}</span>}
{typeof field.label === 'string' ? field.label : '[field-label]' /* TODO */}
{getTranslation(field.label, i18n)}
{getTranslation(field?.label || '', i18n)}
</Label>
<DiffViewer
comparisonToRender={comparisonToRender}

View File

@@ -18,12 +18,12 @@ export default {
number: Text,
point: Text,
radio: Select,
relationship: Relationship,
relationship: null,
richText: Text,
row: Nested,
select: Select,
tabs: Tabs,
text: Text,
textarea: Text,
upload: Relationship,
upload: null,
}

View File

@@ -2,22 +2,22 @@ import type React from 'react'
import type { DiffMethod } from 'react-diff-viewer-continued'
import type { FieldPermissions } from 'payload/auth'
import type { I18n } from '@payloadcms/translations'
import { SanitizedConfig } from 'payload/types'
import { FieldMap, MappedField } from '@payloadcms/ui'
import { I18n } from '@payloadcms/translations/types'
export type FieldComponents = Record<string, React.FC<Props>>
export type DiffComponents = Record<string, React.FC<Props>>
export type Props = {
comparison: any
diffMethod?: DiffMethod
disableGutter?: boolean
field: any
fieldComponents: FieldComponents
field: MappedField
isRichText?: boolean
locale: string
locale?: string
locales?: string[]
permissions?: Record<string, FieldPermissions>
version: any
fieldMap: FieldMap
i18n: I18n
config: SanitizedConfig
diffComponents: DiffComponents
}

View File

@@ -1,71 +1,84 @@
'use client'
import type { DiffMethod } from 'react-diff-viewer-continued'
import React from 'react'
import type { Props } from './types'
import type { Props, FieldDiffProps } from './types'
import { fieldAffectsData, fieldHasSubFields } from 'payload/types'
import Nested from './fields/Nested'
import { diffMethods } from './fields/diffMethods'
import './index.scss'
const baseClass = 'render-field-diffs'
const RenderFieldsToDiff: React.FC<Props> = ({
comparison,
fieldComponents,
fieldPermissions,
fields,
fieldMap,
locales,
version,
i18n,
locale,
config,
diffComponents,
}) => {
return (
<div className={baseClass}>
{fields.map((field, i) => {
const Component = fieldComponents[field.type]
{fieldMap?.map((field, i) => {
if (field.name === 'id') return null
const Component = diffComponents[field.type]
const isRichText = field.type === 'richText'
const diffMethod: DiffMethod = diffMethods[field.type] || 'CHARS'
if (Component) {
if (fieldAffectsData(field)) {
if (field.isFieldAffectingData) {
const valueIsObject = field.type === 'code' || field.type === 'json'
const versionValue = valueIsObject
? JSON.stringify(version?.[field.name])
: version?.[field.name]
const comparisonValue = valueIsObject
? JSON.stringify(comparison?.[field.name])
: comparison?.[field.name]
const hasPermission = fieldPermissions?.[field.name]?.read?.permission
const subFieldPermissions = fieldPermissions?.[field.name]?.fields
if (hasPermission === false) return null
const baseCellProps: FieldDiffProps = {
diffMethod,
field,
isRichText,
locales: locales,
fieldPermissions: subFieldPermissions,
i18n,
fieldMap: 'subfields' in field ? field.subfields : fieldMap,
diffComponents,
comparison: comparisonValue,
version: versionValue,
}
if (field.localized) {
return (
<div className={`${baseClass}__field`} key={i}>
{locales.map((locale, index) => {
const versionLocaleValue = versionValue?.[locale]
const comparisonLocaleValue = comparisonValue?.[locale]
const cellProps = {
...baseCellProps,
version: versionLocaleValue,
comparison: comparisonLocaleValue,
}
return (
<div className={`${baseClass}__locale`} key={[locale, index].join('-')}>
<div className={`${baseClass}__locale-value`}>
<Component
comparison={comparisonLocaleValue}
diffMethod={diffMethod}
field={field}
fieldComponents={fieldComponents}
isRichText={isRichText}
locale={locale}
locales={locales}
permissions={subFieldPermissions}
version={versionLocaleValue}
i18n={i18n}
config={config}
/>
<Component {...cellProps} locale={locale} />
</div>
</div>
)
@@ -76,56 +89,42 @@ const RenderFieldsToDiff: React.FC<Props> = ({
return (
<div className={`${baseClass}__field`} key={i}>
<Component
comparison={comparisonValue}
diffMethod={diffMethod}
field={field}
fieldComponents={fieldComponents}
isRichText={isRichText}
locales={locales}
permissions={subFieldPermissions}
version={versionValue}
i18n={i18n}
locale={locale}
config={config}
/>
<Component {...baseCellProps} />
</div>
)
}
if (field.type === 'tabs') {
const Tabs = fieldComponents.tabs
const Tabs = diffComponents.tabs
return (
<Tabs
comparison={comparison}
field={field}
fieldComponents={fieldComponents}
key={i}
locales={locales}
version={version}
i18n={i18n}
locale={locale}
config={config}
field={field}
fieldMap={field.subfields}
diffComponents={diffComponents}
/>
)
}
// At this point, we are dealing with a `row` or similar
if (fieldHasSubFields(field)) {
// At this point, we are dealing with a `row`, etc
if (field.subfields) {
return (
<Nested
comparison={comparison}
disableGutter
field={field}
fieldComponents={fieldComponents}
fieldMap={field.subfields}
key={i}
locales={locales}
permissions={fieldPermissions}
version={version}
i18n={i18n}
locale={locale}
config={config}
diffComponents={diffComponents}
/>
)
}

View File

@@ -1,16 +1,21 @@
import type { FieldPermissions } from 'payload/auth'
import type { Field, SanitizedConfig } from 'payload/types'
import type { FieldComponents } from './fields/types'
import type { I18n } from '@payloadcms/translations'
import { FieldMap, MappedField } from '@payloadcms/ui'
import { I18n } from '@payloadcms/translations/types'
import { DiffComponents } from './fields/types'
import type { DiffMethod } from 'react-diff-viewer-continued'
export type Props = {
comparison: Record<string, any>
fieldComponents: FieldComponents
fieldPermissions: Record<string, FieldPermissions>
fields: Field[]
fieldMap: FieldMap
locales: string[]
version: Record<string, any>
i18n: I18n
config: SanitizedConfig
locale: string
diffComponents: DiffComponents
}
export type FieldDiffProps = Props & {
field: MappedField
diffMethod: DiffMethod
isRichText: boolean
}

View File

@@ -5,7 +5,7 @@ export type Props = {
collectionSlug?: SanitizedCollectionConfig['slug']
globalSlug?: SanitizedGlobalConfig['slug']
label: SanitizedCollectionConfig['labels']['singular'] | SanitizedGlobalConfig['label']
originalDocID: string
originalDocID: string | number
versionDate: string
versionID: string
}

View File

@@ -17,7 +17,7 @@ const maxResultsPerRequest = 10
const baseOptions = [mostRecentVersionOption]
const CompareVersion: React.FC<Props> = (props) => {
export const SelectComparison: React.FC<Props> = (props) => {
const { baseURL, onChange, parentID, publishedDoc, value, versionID } = props
const {
@@ -58,6 +58,7 @@ const CompareVersion: React.FC<Props> = (props) => {
}
const search = qs.stringify(query)
const response = await fetch(`${baseURL}?${search}`, {
credentials: 'include',
headers: {
@@ -67,6 +68,7 @@ const CompareVersion: React.FC<Props> = (props) => {
if (response.ok) {
const data: PaginatedDocs = await response.json()
if (data.docs.length > 0) {
setOptions((existingOptions) => [
...existingOptions,
@@ -75,6 +77,7 @@ const CompareVersion: React.FC<Props> = (props) => {
value: doc.id,
})),
])
setLastLoadedPage(data.page)
}
} else {
@@ -117,5 +120,3 @@ const CompareVersion: React.FC<Props> = (props) => {
</div>
)
}
export default CompareVersion

View File

@@ -5,7 +5,7 @@ import type { CompareOption } from '../Default/types'
export type Props = {
baseURL: string
onChange: (val: CompareOption) => void
parentID?: string
parentID?: string | number
publishedDoc: any
value: CompareOption
versionID: string

View File

@@ -8,7 +8,7 @@ import './index.scss'
const baseClass = 'select-version-locales'
const SelectLocales: React.FC<Props> = ({ onChange, options, value }) => {
export const SelectLocales: React.FC<Props> = ({ onChange, options, value }) => {
const { t } = useTranslation()
const { code } = useLocale()
@@ -37,5 +37,3 @@ const SelectLocales: React.FC<Props> = ({ onChange, options, value }) => {
</div>
)
}
export default SelectLocales

View File

@@ -1,13 +1,13 @@
import React from 'react'
import { DefaultVersionView } from './Default'
import { Document, Field } from 'payload/types'
import type { EditViewProps } from '@payloadcms/ui'
import { Document } from 'payload/types'
import type { Option, ServerSideEditViewProps } from '@payloadcms/ui'
import { CollectionPermission, GlobalPermission } from 'payload/auth'
import { notFound } from 'next/navigation'
export const VersionView: React.FC<EditViewProps> = async (props) => {
const { config, permissions, payload, user, params, i18n } = props
export const VersionView: React.FC<ServerSideEditViewProps> = async (props) => {
const { config, permissions, payload, user, params } = props
const versionID = params.segments[2]
@@ -21,17 +21,14 @@ export const VersionView: React.FC<EditViewProps> = async (props) => {
const { localization } = config
let docPermissions: CollectionPermission | GlobalPermission
let fields: Field[]
let slug: string
let doc: Document
let publishedDoc: Document
let mostRecentDoc: Document
let compareDoc: Document
if (collectionSlug) {
slug = collectionSlug
fields = collectionConfig.fields
docPermissions = permissions.collections[collectionSlug]
try {
@@ -57,16 +54,6 @@ export const VersionView: React.FC<EditViewProps> = async (props) => {
draft: true,
locale: '*',
})
// TODO: this `id` will be dynamic based on the user's selection
// Use URL params to achieve this
compareDoc = await payload.findByID({
collection: slug,
id,
depth: 1,
draft: true,
locale: '*',
})
} catch (error) {
return notFound()
}
@@ -74,7 +61,6 @@ export const VersionView: React.FC<EditViewProps> = async (props) => {
if (globalSlug) {
slug = globalSlug
fields = globalConfig.fields
docPermissions = permissions.globals[globalSlug]
try {
@@ -98,26 +84,12 @@ export const VersionView: React.FC<EditViewProps> = async (props) => {
draft: true,
locale: '*',
})
// TODO: this `slug` will be dynamic based on the user's selection
// Use URL params to achieve this
compareDoc = payload.findGlobal({
slug,
depth: 1,
draft: true,
locale: '*',
})
} catch (error) {
return notFound()
}
}
// const compareFetchURL =
// compareValue?.value === 'mostRecent' || compareValue?.value === 'published'
// ? originalDocFetchURL
// : `${compareBaseURL}/${compareValue.value}`
const locales =
const localeOptions: Option[] =
localization &&
localization?.locales &&
localization.locales.map(({ code, label }) => ({
@@ -131,13 +103,11 @@ export const VersionView: React.FC<EditViewProps> = async (props) => {
return (
<DefaultVersionView
collectionConfig={collectionConfig}
compareDoc={compareDoc}
config={config}
collectionSlug={collectionSlug}
globalSlug={globalSlug}
initialComparisonDoc={mostRecentDoc}
doc={doc}
fields={fields}
globalConfig={globalConfig}
locales={locales}
localeOptions={localeOptions}
mostRecentDoc={mostRecentDoc}
id={id}
permissions={permissions}
@@ -145,8 +115,6 @@ export const VersionView: React.FC<EditViewProps> = async (props) => {
user={user}
versionID={versionID}
docPermissions={docPermissions}
locale="" // TODO
i18n={i18n}
/>
)
}

View File

@@ -0,0 +1,66 @@
import React from 'react'
import type {
SanitizedCollectionConfig,
SanitizedConfig,
SanitizedGlobalConfig,
} from 'payload/types'
import type { Column } from '@payloadcms/ui'
import { SortColumn } from '@payloadcms/ui'
import { I18n } from '@payloadcms/translations'
import { CreatedAtCell } from './cells/CreatedAt'
import { IDCell } from './cells/ID'
import { AutosaveCell } from './cells/AutosaveCell'
export const buildVersionColumns = ({
config,
collectionConfig,
globalConfig,
docID,
i18n: { t },
i18n,
}: {
config: SanitizedConfig
collectionConfig?: SanitizedCollectionConfig
globalConfig?: SanitizedGlobalConfig
docID?: string | number
i18n: I18n
}): Column[] => [
{
name: '',
accessor: 'updatedAt',
active: true,
components: {
Heading: <SortColumn label={t('general:updatedAt')} name="updatedAt" />,
Cell: (
<CreatedAtCell
docID={docID}
collectionSlug={collectionConfig?.slug}
globalSlug={globalConfig?.slug}
/>
),
},
label: '',
},
{
name: '',
accessor: 'id',
active: true,
components: {
Heading: <SortColumn disable label={t('version:versionID')} name="id" />,
Cell: <IDCell />,
},
label: '',
},
{
name: '',
accessor: 'autosave',
active: true,
components: {
Heading: <SortColumn disable label={t('version:type')} name="autosave" />,
Cell: <AutosaveCell />,
},
label: '',
},
]

View File

@@ -0,0 +1,30 @@
'use client'
import React, { Fragment } from 'react'
import { useTranslation, useTableCell, Pill } from '@payloadcms/ui'
export const AutosaveCell: React.FC = () => {
const { t } = useTranslation()
const { rowData } = useTableCell()
return (
<Fragment>
{rowData?.autosave && (
<React.Fragment>
<Pill>
Autosave
{t('version:autosave')}
</Pill>
&nbsp;&nbsp;
</React.Fragment>
)}
{rowData?.version._status === 'published' && (
<React.Fragment>
<Pill pillStyle="success">{t('version:published')}</Pill>
&nbsp;&nbsp;
</React.Fragment>
)}
{rowData?.version._status === 'draft' && <Pill>{t('version:draft')}</Pill>}
</Fragment>
)
}

View File

@@ -0,0 +1,35 @@
'use client'
import React from 'react'
import { formatDate, useConfig, useTranslation, useTableCell } from '@payloadcms/ui'
import Link from 'next/link'
type CreatedAtCellProps = {
collectionSlug?: string
globalSlug?: string
docID?: string | number
}
export const CreatedAtCell: React.FC<CreatedAtCellProps> = ({
collectionSlug,
globalSlug,
docID,
}) => {
const {
routes: { admin },
admin: { dateFormat },
} = useConfig()
const { i18n } = useTranslation()
const { cellData, rowData } = useTableCell()
const versionID = rowData.id
let to: string
if (collectionSlug) to = `${admin}/collections/${collectionSlug}/${docID}/versions/${versionID}`
if (globalSlug) to = `${admin}/globals/${globalSlug}/versions/${versionID}`
return <Link href={to}>{cellData && formatDate(cellData, dateFormat, i18n.language)}</Link>
}

View File

@@ -0,0 +1,8 @@
'use client'
import React, { Fragment } from 'react'
import { useTableCell } from '@payloadcms/ui'
export const IDCell: React.FC = () => {
const { cellData } = useTableCell()
return <Fragment>{cellData}</Fragment>
}

View File

@@ -1,122 +0,0 @@
import React from 'react'
import type {
SanitizedCollectionConfig,
SanitizedConfig,
SanitizedGlobalConfig,
} from 'payload/types'
import type { Column } from '@payloadcms/ui'
import { Pill, formatDate, SortColumn } from '@payloadcms/ui'
import Link from 'next/link'
import { I18n } from '@payloadcms/translations'
type CreatedAtCellProps = {
config: SanitizedConfig
collectionConfig?: SanitizedCollectionConfig
globalConfig?: SanitizedGlobalConfig
date: string
versionID: string
docID: string
i18n: I18n
}
const CreatedAtCell: React.FC<CreatedAtCellProps> = ({
versionID,
docID,
config,
collectionConfig,
date,
globalConfig,
i18n,
}) => {
const {
routes: { admin },
admin: { dateFormat },
} = config
let to: string
if (collectionConfig)
to = `${admin}/collections/${collectionConfig.slug}/${docID}/versions/${versionID}`
if (globalConfig) to = `${admin}/globals/${globalConfig.slug}/versions/${versionID}`
return <Link href={to}>{date && formatDate(date, dateFormat, i18n.language)}</Link>
}
const TextCell: React.FC<{ children?: React.ReactNode }> = ({ children }) => <span>{children}</span>
export const buildVersionColumns = ({
config,
collectionConfig,
globalConfig,
docID,
i18n: { t },
i18n,
}: {
config: SanitizedConfig
collectionConfig?: SanitizedCollectionConfig
globalConfig?: SanitizedGlobalConfig
docID?: string
i18n: I18n
}): Column[] => [
{
name: '',
accessor: 'updatedAt',
active: true,
components: {
Heading: <SortColumn label={t('general:updatedAt')} name="updatedAt" />,
renderCell: (row, data) => (
<CreatedAtCell
config={config}
collectionConfig={collectionConfig}
date={data}
globalConfig={globalConfig}
versionID={row?.id}
docID={docID}
i18n={i18n}
/>
),
},
label: '',
},
{
name: '',
accessor: 'id',
active: true,
components: {
Heading: <SortColumn disable label={t('version:versionID')} name="id" />,
renderCell: (row, data) => <TextCell>{data}</TextCell>,
},
label: '',
},
{
name: '',
accessor: 'autosave',
active: true,
components: {
Heading: <SortColumn disable label={t('version:type')} name="autosave" />,
renderCell: (row) => (
<TextCell>
{row?.autosave && (
<React.Fragment>
<Pill>
Autosave
{t('version:autosave')}
</Pill>
&nbsp;&nbsp;
</React.Fragment>
)}
{row?.version._status === 'published' && (
<React.Fragment>
<Pill pillStyle="success">{t('version:published')}</Pill>
&nbsp;&nbsp;
</React.Fragment>
)}
{row?.version._status === 'draft' && <Pill>{t('version:draft')}</Pill>}
</TextCell>
),
},
label: '',
},
]

View File

@@ -0,0 +1,132 @@
'use client'
import {
Column,
LoadingOverlayToggle,
Pagination,
PerPage,
Table,
usePayloadAPI,
useTranslation,
} from '@payloadcms/ui'
import { useSearchParams } from 'next/navigation'
import { PaginatedDocs } from 'payload/database'
import { SanitizedCollectionConfig } from 'payload/types'
import React, { Fragment, useEffect, useRef } from 'react'
export const VersionsViewClient: React.FC<{
initialData: PaginatedDocs
columns: Column[]
baseClass: string
fetchURL: string
collectionSlug?: string
globalSlug?: string
id?: string | number
paginationLimits?: SanitizedCollectionConfig['admin']['pagination']['limits']
}> = (props) => {
const { initialData, columns, baseClass, collectionSlug, fetchURL, id, paginationLimits } = props
const searchParams = useSearchParams()
const limit = searchParams.get('limit')
const { i18n } = useTranslation()
const [{ data, isLoading }, { setParams }] = usePayloadAPI(fetchURL, {
initialData,
initialParams: {
depth: 1,
limit,
page: undefined,
sort: undefined,
where: {
parent: {
equals: id,
},
},
},
})
const hasInitialized = useRef(false)
useEffect(() => {
if (initialData && !hasInitialized.current) {
hasInitialized.current = true
return
}
const page = searchParams.get('page')
const sort = searchParams.get('sort')
const params = {
depth: 1,
limit,
page: undefined,
sort: undefined,
where: {},
}
if (page) params.page = page
if (sort) params.sort = sort
if (collectionSlug) {
params.where = {
parent: {
equals: id,
},
}
}
setParams(params)
}, [id, collectionSlug, searchParams, limit])
// useEffect(() => {
// const editConfig = (collection || global)?.admin?.components?.views?.Edit
// const versionsActions =
// editConfig && 'Versions' in editConfig && 'actions' in editConfig.Versions
// ? editConfig.Versions.actions
// : []
// setViewActions(versionsActions)
// }, [collection, global, setViewActions])
const versionCount = data?.totalDocs || 0
return (
<Fragment>
<LoadingOverlayToggle name="versions" show={isLoading} />
{versionCount === 0 && (
<div className={`${baseClass}__no-versions`}>
{i18n.t('version:noFurtherVersionsFound')}
</div>
)}
{versionCount > 0 && (
<React.Fragment>
<Table columns={columns} data={data?.docs} />
<div className={`${baseClass}__page-controls`}>
<Pagination
hasNextPage={data.hasNextPage}
hasPrevPage={data.hasPrevPage}
limit={data.limit}
nextPage={data.nextPage}
numberOfNeighbors={1}
page={data.page}
prevPage={data.prevPage}
totalPages={data.totalPages}
/>
{data?.totalDocs > 0 && (
<React.Fragment>
<div className={`${baseClass}__page-info`}>
{data.page * data.limit - (data.limit - 1)}-
{data.totalPages > 1 && data.totalPages !== data.page
? data.limit * data.page
: data.totalDocs}{' '}
{i18n.t('general:of')} {data.totalDocs}
</div>
<PerPage limit={limit ? Number(limit) : 10} limits={paginationLimits} />
</React.Fragment>
)}
</div>
</React.Fragment>
)}
</Fragment>
)
}

View File

@@ -1,23 +1,17 @@
import React from 'react'
import {
Gutter,
Pagination,
PerPage,
Table,
SetDocumentStepNav as SetStepNav,
EditViewProps,
} from '@payloadcms/ui'
import { buildVersionColumns } from './columns'
import './index.scss'
import { Gutter, SetDocumentStepNav as SetStepNav, ServerSideEditViewProps } from '@payloadcms/ui'
import { buildVersionColumns } from './buildColumns'
import { notFound } from 'next/navigation'
import { getTranslation } from '@payloadcms/translations'
import { VersionsViewClient } from './index.client'
const baseClass = 'versions'
import './index.scss'
export const VersionsView: React.FC<EditViewProps> = async (props) => {
const { config, searchParams, payload, user, i18n } = props
export const baseClass = 'versions'
export const VersionsView: React.FC<ServerSideEditViewProps> = async (props) => {
const { user, payload, config, searchParams, i18n } = props
const id = 'id' in props ? props.id : undefined
const collectionConfig = 'collectionConfig' in props && props?.collectionConfig
@@ -28,7 +22,7 @@ export const VersionsView: React.FC<EditViewProps> = async (props) => {
const { limit, page, sort } = searchParams
const {
routes: { admin, api },
routes: { admin: adminRoute, api: apiRoute },
serverURL,
} = config
@@ -44,23 +38,22 @@ export const VersionsView: React.FC<EditViewProps> = async (props) => {
collection: collectionSlug,
depth: 0,
user,
page: page ? parseInt(page as string, 10) : undefined,
page: page ? parseInt(page.toString(), 10) : undefined,
sort: sort as string,
// TODO: why won't this work?!
// throws an `unsupported BSON` error
// where: {
// parent: {
// equals: id,
// },
// },
limit: limit ? parseInt(limit?.toString(), 10) : undefined,
where: {
parent: {
equals: id,
},
},
})
} catch (error) {
console.error(error)
}
docURL = `${serverURL}${api}/${slug}/${id}`
docURL = `${serverURL}${apiRoute}/${slug}/${id}`
entityLabel = getTranslation(collectionConfig.labels.singular, i18n)
editURL = `${admin}/collections/${collectionSlug}/${id}`
editURL = `${adminRoute}/collections/${collectionSlug}/${id}`
}
if (globalSlug) {
@@ -85,22 +78,24 @@ export const VersionsView: React.FC<EditViewProps> = async (props) => {
return notFound()
}
docURL = `${serverURL}${api}/globals/${globalSlug}`
docURL = `${serverURL}${apiRoute}/globals/${globalSlug}`
entityLabel = getTranslation(globalConfig.label, i18n)
editURL = `${admin}/globals/${globalSlug}`
editURL = `${adminRoute}/globals/${globalSlug}`
}
// useEffect(() => {
// const editConfig = (collection || global)?.admin?.components?.views?.Edit
// const versionsActions =
// editConfig && 'Versions' in editConfig && 'actions' in editConfig.Versions
// ? editConfig.Versions.actions
// : []
const columns = buildVersionColumns({
config,
collectionConfig,
globalConfig,
docID: id,
i18n,
})
// setViewActions(versionsActions)
// }, [collection, global, setViewActions])
const versionCount = versionsData?.totalDocs || 0
const fetchURL = collectionSlug
? `${serverURL}${apiRoute}/${collectionSlug}/versions`
: globalSlug
? `${serverURL}${apiRoute}/globals/${globalSlug}/versions`
: ''
return (
<React.Fragment>
@@ -112,64 +107,18 @@ export const VersionsView: React.FC<EditViewProps> = async (props) => {
pluralLabel={collectionConfig?.labels?.plural}
view={i18n.t('version:versions')}
/>
{/* <LoadingOverlayToggle name="versions" show={isLoadingVersions} /> */}
<main className={baseClass}>
{/* <Meta description={metaDesc} title={metaTitle} /> */}
<Gutter className={`${baseClass}__wrap`}>
{versionCount === 0 && (
<div className={`${baseClass}__no-versions`}>
{i18n.t('version:noFurtherVersionsFound')}
</div>
)}
{versionCount > 0 && (
<React.Fragment>
<div className={`${baseClass}__version-count`}>
{i18n.t(
versionCount === 1 ? 'version:versionCount_one' : 'version:versionCount_many',
{
count: versionCount,
},
)}
</div>
<Table
columns={buildVersionColumns({
config,
collectionConfig,
globalConfig,
docID: id,
i18n,
})}
data={versionsData?.docs}
/>
<div className={`${baseClass}__page-controls`}>
<Pagination
hasNextPage={versionsData.hasNextPage}
hasPrevPage={versionsData.hasPrevPage}
limit={versionsData.limit}
nextPage={versionsData.nextPage}
numberOfNeighbors={1}
page={versionsData.page}
prevPage={versionsData.prevPage}
totalPages={versionsData.totalPages}
/>
{versionsData?.totalDocs > 0 && (
<React.Fragment>
<div className={`${baseClass}__page-info`}>
{versionsData.page * versionsData.limit - (versionsData.limit - 1)}-
{versionsData.totalPages > 1 && versionsData.totalPages !== versionsData.page
? versionsData.limit * versionsData.page
: versionsData.totalDocs}{' '}
{i18n.t('general:of')} {versionsData.totalDocs}
</div>
<PerPage
limit={limit ? Number(limit) : 10}
limits={collectionConfig?.admin?.pagination?.limits}
/>
</React.Fragment>
)}
</div>
</React.Fragment>
)}
<VersionsViewClient
initialData={versionsData}
columns={columns}
fetchURL={fetchURL}
baseClass={baseClass}
collectionSlug={collectionSlug}
globalSlug={globalSlug}
id={id}
paginationLimits={collectionConfig?.admin?.pagination?.limits}
/>
</Gutter>
</main>
</React.Fragment>

View File

@@ -4,7 +4,6 @@ import type {
SanitizedGlobalConfig,
} from 'payload/types'
import type { PaginatedDocs } from 'payload/database'
import type { Version } from '@payloadcms/ui'
import { User } from 'payload/auth'
import { I18n } from '@payloadcms/translations'
@@ -13,11 +12,11 @@ export type DefaultVersionsViewProps = {
config: SanitizedConfig
collectionConfig?: SanitizedCollectionConfig
globalConfig?: SanitizedGlobalConfig
data: Version
versionsData: PaginatedDocs<Version>
data: Document
versionsData: PaginatedDocs<Document>
editURL: string
entityLabel: string
id: string
id: string | number
user: User
limit: number
i18n: I18n

View File

@@ -1,7 +1,6 @@
'use client'
import queryString from 'qs'
import React from 'react'
import { useHistory } from 'react-router-dom'
import type { Node, Props } from './types'
@@ -9,6 +8,7 @@ import { useSearchParams } from '../../providers/SearchParams'
import ClickableArrow from './ClickableArrow'
import Page from './Page'
import Separator from './Separator'
import { usePathname, useRouter } from 'next/navigation'
import './index.scss'
const nodeTypes = {
@@ -20,8 +20,9 @@ const nodeTypes = {
const baseClass = 'paginator'
export const Pagination: React.FC<Props> = (props) => {
const history = useHistory()
const router = useRouter()
const params = useSearchParams()
const pathname = usePathname()
const {
disableHistoryChange = false,
@@ -37,7 +38,6 @@ export const Pagination: React.FC<Props> = (props) => {
if (!totalPages || totalPages <= 1) return null
// uses react router to set the current page
const updatePage = (page) => {
if (!disableHistoryChange) {
const newParams = {
@@ -45,7 +45,7 @@ export const Pagination: React.FC<Props> = (props) => {
}
newParams.page = page
history.push({ search: queryString.stringify(newParams, { addQueryPrefix: true }) })
router.push(pathname + queryString.stringify(newParams, { addQueryPrefix: true }))
}
if (typeof onChange === 'function') onChange(page)

View File

@@ -35,3 +35,4 @@ export { default as Popup } from '../elements/Popup'
// export { useThumbnail } from '../elements/Upload'
export { Translation } from '../elements/Translation'
export { Tooltip } from '../elements/Tooltip'
export { useTableCell } from '../elements/Table/TableCellProvider'

View File

@@ -4,3 +4,4 @@ export { formatDate } from '../utilities/formatDate'
export type { EntityToGroup, Group } from '../utilities/groupNavItems'
export { EntityType, groupNavItems } from '../utilities/groupNavItems'
export { withMergedProps } from '../utilities/withMergedProps'
export type { FieldMap, MappedField } from '../utilities/buildComponentMap/types'

View File

@@ -1,6 +1,6 @@
export { DefaultList } from '../views/List'
export { DefaultEditView } from '../views/Edit'
export type { DefaultEditViewProps } from '../views/Edit/types'
export type { DefaultListViewProps } from '../views/List/types'
export { default as Auth } from '../views/Edit/Auth'
export type { EditViewProps } from '../views/types'
export type { ServerSideEditViewProps } from '../views/types'

View File

@@ -1,6 +1,6 @@
'use client'
import queryString from 'qs'
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from '../providers/Translation'
import { requests } from '../utilities/api'
@@ -25,14 +25,15 @@ type Options = {
type UsePayloadAPI = (url: string, options?: Options) => Result
const usePayloadAPI: UsePayloadAPI = (url, options = {}) => {
const { initialData = {}, initialParams = {} } = options
const { initialData, initialParams = {} } = options
const { i18n } = useTranslation()
const [data, setData] = useState(initialData)
const [data, setData] = useState(initialData || {})
const [params, setParams] = useState(initialParams)
const [isLoading, setIsLoading] = useState(true)
const [isError, setIsError] = useState(false)
const { code: locale } = useLocale()
const hasInitialized = useRef(false)
const search = queryString.stringify(
{
@@ -45,6 +46,11 @@ const usePayloadAPI: UsePayloadAPI = (url, options = {}) => {
)
useEffect(() => {
if (initialData && !hasInitialized.current) {
hasInitialized.current = true
return
}
const abortController = new AbortController()
const fetchData = async () => {

View File

@@ -1,14 +1,14 @@
'use client'
import { useSearchParams as useNextSearchParams } from 'next/navigation'
import qs from 'qs'
import React, { createContext, useContext } from 'react'
import { useLocation } from 'react-router-dom'
const Context = createContext({})
export const SearchParamsProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const location = useLocation()
const searchParams = useNextSearchParams()
const params = qs.parse(location.search, { depth: 10, ignoreQueryPrefix: true })
const params = qs.parse(searchParams.toString(), { depth: 10, ignoreQueryPrefix: true })
return <Context.Provider value={params}>{children}</Context.Provider>
}

View File

@@ -250,6 +250,7 @@ export const mapFields = (args: {
const reducedField: MappedField = {
name: 'name' in field ? field.name : '',
label: 'label' in field && typeof field.label !== 'function' ? field.label : undefined,
labels: 'labels' in field ? field.labels : undefined,
type: field.type,
Field,
Cell: (
@@ -283,6 +284,9 @@ export const mapFields = (args: {
isSidebar: field.admin?.position === 'sidebar',
subfields: nestedFieldMap,
tabs,
options: 'options' in field ? field.options : undefined,
hasMany: 'hasMany' in field ? field.hasMany : undefined,
localized: 'localized' in field ? field.localized : false,
}
if (FieldComponent) {
@@ -294,7 +298,8 @@ export const mapFields = (args: {
return acc
}, [])
const hasID = result.findIndex((field) => fieldAffectsData(field) && field.name === 'id') > -1
const hasID =
result.findIndex(({ isFieldAffectingData, name }) => isFieldAffectingData && name === 'id') > -1
if (!hasID) {
result.push({

View File

@@ -5,6 +5,8 @@ import {
SanitizedCollectionConfig,
SanitizedGlobalConfig,
TabsField,
Option,
Labels,
} from 'payload/types'
import { fieldTypes } from '../../forms/fields'
@@ -34,6 +36,9 @@ export type MappedField = {
readOnly: boolean
isSidebar: boolean
label: FieldBase['label']
labels: Labels
fieldMap?: FieldMap
localized: boolean
/**
* On `array`, `blocks`, `group`, `collapsible`, and `tabs` fields only
*/
@@ -42,7 +47,11 @@ export type MappedField = {
* On `tabs` fields only
*/
tabs?: MappedTab[]
fieldMap?: FieldMap
/**
* On `select` fields only
*/
options?: Option[]
hasMany?: boolean
}
export type FieldMap = MappedField[]

View File

@@ -1,16 +1,8 @@
import type { CollectionPermission, GlobalPermission, Permissions, User } from 'payload/auth'
import type {
Document,
SanitizedCollectionConfig,
SanitizedConfig,
SanitizedGlobalConfig,
Payload,
DocumentPreferences,
} from 'payload/types'
import type { I18n } from '@payloadcms/translations'
import type { Document, DocumentPreferences, Payload, SanitizedConfig } from 'payload/types'
import type { FormState } from '../forms/Form/types'
import type { FieldTypes, Locale } from 'payload/config'
import { FieldMap } from '../utilities/buildComponentMap/types'
import type { Locale } from 'payload/config'
import { I18n } from '@payloadcms/translations'
export type EditViewProps = (
| {
@@ -43,3 +35,18 @@ export type EditViewProps = (
AfterDocument?: React.ReactNode
AfterFields?: React.ReactNode
}
export type ServerSideEditViewProps = EditViewProps & {
payload: Payload
config: SanitizedConfig
searchParams: { [key: string]: string | string[] | undefined }
i18n: I18n
collectionConfig?: SanitizedConfig['collections'][0]
globalConfig?: SanitizedConfig['globals'][0]
params: {
segments: string[]
collection?: string
global?: string
}
permissions: Permissions
}