chore(ui): ssr document tabs (#5116)

This commit is contained in:
Jacob Fletcher
2024-02-19 14:27:37 -05:00
committed by GitHub
parent 17eb760928
commit 3e3f223bb2
18 changed files with 149 additions and 81 deletions

View File

@@ -204,7 +204,6 @@ export const Document = async ({
collectionSlug={collectionConfig?.slug}
globalSlug={globalConfig?.slug}
id={id}
versionsConfig={collectionConfig?.versions || globalConfig?.versions}
/>
<EditDepthProvider depth={1} key={`${collectionSlug || globalSlug}-${locale}`}>
<FormQueryParamsProvider formQueryParams={formQueryParams}>

View File

@@ -0,0 +1,34 @@
'use client'
import React from 'react'
import { DefaultEditView, EditViewProps, useDocumentInfo } from '@payloadcms/ui'
import { useCallback } from 'react'
export const DefaultEditViewClient: React.FC<EditViewProps> = (props) => {
const id = 'id' in props ? props.id : undefined
const collectionSlug = 'collectionSlug' in props ? props.collectionSlug : undefined
const isEditing = Boolean(id && collectionSlug)
const { getVersions, getDocPermissions } = useDocumentInfo()
const onSave = useCallback(
async (json: { doc }) => {
getVersions()
getDocPermissions()
if (!isEditing) {
// setRedirect(`${admin}/collections/${collection.slug}/${json?.doc?.id}`)
} else {
// buildState(json.doc, {
// fieldSchema: collection.fields,
// })
// setFormQueryParams((params) => ({
// ...params,
// uploadEdits: undefined,
// }))
}
},
[getVersions, isEditing, getDocPermissions, collectionSlug],
)
return <DefaultEditView {...props} onSave={onSave} />
}

View File

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

View File

@@ -3,8 +3,6 @@ import type { I18n } from '@payloadcms/translations'
import type { SanitizedCollectionConfig } from '../../collections/config/types'
import type { SanitizedConfig } from '../../config/types'
import type { SanitizedGlobalConfig } from '../../globals/config/types'
import type { Document } from '../../types'
import type { DocumentInfoContext } from '../providers/DocumentInfo'
export type DocumentTabProps = {
apiURL?: string
@@ -17,13 +15,13 @@ export type DocumentTabProps = {
export type DocumentTabCondition = (args: {
collectionConfig: SanitizedCollectionConfig
config: SanitizedConfig
documentInfo: DocumentInfoContext
globalConfig: SanitizedGlobalConfig
}) => boolean
// Everything is optional because we merge in the defaults
// i.e. the config may override the `Default` view with a `label` but not an `href`
export type DocumentTabConfig = {
Pill?: React.ComponentType
condition?: DocumentTabCondition
href?:
| ((args: {
@@ -34,17 +32,13 @@ export type DocumentTabConfig = {
routes: SanitizedConfig['routes']
}) => string)
| string
isActive?: boolean
// isActive?: ((args: { href: string }) => boolean) | boolean
isActive?: ((args: { href: string }) => boolean) | boolean
label?: ((args: { t: (key: string) => string }) => string) | string
newTab?: boolean
pillLabel?: ((args: { versions: Document }) => string) | string
}
export type DocumentTabComponent = React.ComponentType<
DocumentTabProps & {
path: string
}
>
export type DocumentTabComponent = React.ComponentType<{
path: string
}>
export type DocumentTab = DocumentTabComponent | DocumentTabConfig

View File

@@ -9,6 +9,7 @@ import type { SanitizedGlobalConfig } from '../../globals/config/types'
export type DocumentInfoContext = {
collectionSlug?: SanitizedCollectionConfig['slug']
docConfig?: SanitizedCollectionConfig | SanitizedGlobalConfig
docPermissions: DocumentPermissions
getDocPermissions: () => Promise<void>
getDocPreferences: () => Promise<{ [key: string]: unknown }>
@@ -23,7 +24,6 @@ export type DocumentInfoContext = {
collectionSlug: SanitizedCollectionConfig['slug']
globalSlug: SanitizedGlobalConfig['slug']
id: number | string
versionsConfig: SanitizedCollectionConfig['versions'] | SanitizedGlobalConfig['versions']
}>,
) => void
setDocumentTitle: (title: string) => void
@@ -31,6 +31,5 @@ export type DocumentInfoContext = {
title?: string
unpublishedVersions?: PaginatedDocs<TypeWithVersion<any>>
versions?: PaginatedDocs<TypeWithVersion<any>>
versionsConfig?: SanitizedCollectionConfig['versions'] | SanitizedGlobalConfig['versions']
versionsCount?: PaginatedDocs<TypeWithVersion<any>>
}

View File

@@ -23,7 +23,9 @@ const Autosave: React.FC<Props> = ({ id, collection, global, publishedDocUpdated
routes: { admin, api },
serverURL,
} = useConfig()
const { getVersions, versionsConfig, versions } = useDocumentInfo()
const { getVersions, docConfig, versions } = useDocumentInfo()
const versionsConfig = docConfig?.versions
const [fields] = useAllFormFields()
const modified = useFormModified()
const { code: locale } = useLocale()

View File

@@ -1,17 +1,13 @@
'use client'
import React from 'react'
import { useParams } from '../../../providers/Params'
import { useDocumentInfo } from '../../../providers/DocumentInfo'
export const ShouldRenderTabs: React.FC<{
children: React.ReactNode
}> = ({ children }) => {
const {
collection: collectionSlug,
global: globalSlug,
segments: [idFromParam] = [],
} = useParams()
const { collectionSlug, globalSlug, id: idFromContext } = useDocumentInfo()
const id = idFromParam !== 'create' ? idFromParam : null
const id = idFromContext !== 'create' ? idFromContext : null
// Don't show tabs when creating new documents
if ((collectionSlug && id) || globalSlug) {

View File

@@ -2,35 +2,34 @@ import React, { Fragment } from 'react'
import { DocumentTabLink } from './TabLink'
import './index.scss'
import { DocumentTabConfig, DocumentTabProps } from 'payload/types'
const baseClass = 'doc-tab'
import './index.scss'
export const baseClass = 'doc-tab'
export const DocumentTab: React.FC<DocumentTabProps & DocumentTabConfig> = (props) => {
const {
id,
apiURL,
config,
collectionConfig,
condition,
globalConfig,
href: tabHref,
isActive: checkIsActive,
isActive: tabIsActive,
label,
newTab,
pillLabel,
Pill,
i18n,
} = props
const { routes } = config
// const { versions } = documentInfo
let href = typeof tabHref === 'string' ? tabHref : ''
let isActive = typeof tabIsActive === 'boolean' ? tabIsActive : false
if (typeof tabHref === 'function') {
href = tabHref({
id,
apiURL,
collection: collectionConfig,
global: globalConfig,
@@ -38,10 +37,13 @@ export const DocumentTab: React.FC<DocumentTabProps & DocumentTabConfig> = (prop
})
}
if (
!condition ||
(condition && condition({ collectionConfig, config, documentInfo: undefined, globalConfig }))
) {
if (typeof tabIsActive === 'function') {
isActive = tabIsActive({
href,
})
}
if (!condition || (condition && condition({ collectionConfig, config, globalConfig }))) {
const labelToRender =
typeof label === 'function'
? label({
@@ -49,24 +51,21 @@ export const DocumentTab: React.FC<DocumentTabProps & DocumentTabConfig> = (prop
})
: label
const pillToRender =
typeof pillLabel === 'function' ? pillLabel({ versions: undefined }) : pillLabel
return (
<DocumentTabLink
href={href}
newTab={newTab}
baseClass={baseClass}
isActive={checkIsActive}
isActive={isActive}
adminRoute={routes.admin}
isCollection={!!collectionConfig && !globalConfig}
>
<span className={`${baseClass}__label`}>
{labelToRender}
{pillToRender && (
{Pill && (
<Fragment>
&nbsp;
<span className={`${baseClass}__count`}>{pillToRender}</span>
<Pill />
</Fragment>
)}
</span>

View File

@@ -3,16 +3,22 @@ import React from 'react'
import { DocumentTab } from './Tab'
import { getCustomViews } from './getCustomViews'
import { getViewConfig } from './getViewConfig'
import { tabs as defaultViews } from './tabs'
import { DocumentTabProps } from 'payload/types'
import { tabs as defaultTabs } from './tabs'
import { ShouldRenderTabs } from './ShouldRenderTabs'
import { SanitizedCollectionConfig, SanitizedConfig, SanitizedGlobalConfig } from 'payload/types'
import { I18n } from '@payloadcms/translations'
import './index.scss'
const baseClass = 'doc-tabs'
export const DocumentTabs: React.FC<DocumentTabProps> = (props) => {
const { collectionConfig, globalConfig } = props
export const DocumentTabs: React.FC<{
config: SanitizedConfig
collectionConfig: SanitizedCollectionConfig
globalConfig: SanitizedGlobalConfig
i18n: I18n
}> = (props) => {
const { collectionConfig, globalConfig, config } = props
const customViews = getCustomViews({ collectionConfig, globalConfig })
@@ -21,7 +27,7 @@ export const DocumentTabs: React.FC<DocumentTabProps> = (props) => {
<div className={baseClass}>
<div className={`${baseClass}__tabs-container`}>
<ul className={`${baseClass}__tabs`}>
{Object.entries(defaultViews)
{Object.entries(defaultTabs)
// sort `defaultViews` based on `order` property from smallest to largest
// if no `order`, append the view to the end
// TODO: open `order` to the config and merge `defaultViews` with `customViews`
@@ -31,20 +37,30 @@ export const DocumentTabs: React.FC<DocumentTabProps> = (props) => {
else if (b.order === undefined) return -1
return a.order - b.order
})
?.map(([name, Tab], index) => {
?.map(([name, tab], index) => {
const viewConfig = getViewConfig({ name, collectionConfig, globalConfig })
const tabOverrides = viewConfig && 'Tab' in viewConfig ? viewConfig.Tab : undefined
const tabFromConfig = viewConfig && 'Tab' in viewConfig ? viewConfig.Tab : undefined
const tabConfig = typeof tabFromConfig === 'object' ? tabFromConfig : undefined
return (
<DocumentTab
{...{
...props,
...(Tab || {}),
...(tabOverrides || {}),
}}
key={`tab-${index}`}
/>
)
const { condition } = tabConfig || {}
const meetsCondition =
!condition || (condition && condition({ collectionConfig, config, globalConfig }))
if (meetsCondition) {
return (
<DocumentTab
key={`tab-${index}`}
{...{
...props,
...(tab || {}),
...(tabFromConfig || {}),
}}
/>
)
}
return null
})}
{customViews?.map((CustomView, index) => {
if ('Tab' in CustomView) {
@@ -56,11 +72,11 @@ export const DocumentTabs: React.FC<DocumentTabProps> = (props) => {
return (
<DocumentTab
key={`tab-custom-${index}`}
{...{
...props,
...Tab,
}}
key={`tab-custom-${index}`}
/>
)
}

View File

@@ -0,0 +1,9 @@
'use client'
import React from 'react'
import { useDocumentInfo } from '../../../../../providers/DocumentInfo'
import { baseClass } from '../../Tab'
export const VersionsPill: React.FC = () => {
const { versions } = useDocumentInfo()
return <span className={`${baseClass}__count`}>{versions?.totalDocs?.toString()}</span>
}

View File

@@ -1,4 +1,5 @@
import type { DocumentTabConfig } from 'payload/types'
import { VersionsPill } from './VersionsPill'
export const documentViewKeys = [
'API',
@@ -70,9 +71,6 @@ export const tabs: Record<
href: '/versions',
label: ({ t }) => t('version:versions'),
order: 200,
pillLabel: ({ versions }) =>
typeof versions?.totalDocs === 'number' && versions?.totalDocs > 0
? versions?.totalDocs.toString()
: '',
Pill: VersionsPill,
},
}

View File

@@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useReducer, useState } from 'react'
import { useTranslation } from '../../providers/Translation'
import type { SanitizedCollectionConfig } from 'payload/types'
import type { Where, Field } from 'payload/types'
import type { Where } from 'payload/types'
import type { ListDrawerProps } from './types'
import { baseClass } from '.'
@@ -305,10 +305,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
]}
collectionSlug={selectedCollectionConfig.slug}
>
<DocumentInfoProvider
collectionSlug={selectedCollectionConfig.slug}
versionsConfig={selectedCollectionConfig?.versions}
>
<DocumentInfoProvider collectionSlug={selectedCollectionConfig.slug}>
<RenderCustomComponent
CustomComponent={ListToRender}
DefaultComponent={DefaultList}

View File

@@ -0,0 +1,19 @@
'use client'
import { useEffect } from 'react'
import { useAuth } from '..'
import { Permissions, User } from 'payload/auth'
export const HydrateClientUser: React.FC<{ user: User; permissions: Permissions }> = ({
user,
permissions,
}) => {
const { setUser, setPermissions } = useAuth()
useEffect(() => {
setUser(user)
setPermissions(permissions)
}, [user, permissions, setUser, setPermissions])
return null
}

View File

@@ -7,13 +7,12 @@ export const SetDocumentInfo: React.FC<{
collectionSlug: string
globalSlug: string
id: string
versionsConfig?: any
}> = ({ collectionSlug, globalSlug, id, versionsConfig }) => {
}> = ({ collectionSlug, globalSlug, id }) => {
const { setDocumentInfo } = useDocumentInfo()
useEffect(() => {
setDocumentInfo({ collectionSlug, globalSlug, id, versionsConfig })
}, [collectionSlug, globalSlug, id, setDocumentInfo, versionsConfig])
setDocumentInfo({ collectionSlug, globalSlug, id })
}, [collectionSlug, globalSlug, id, setDocumentInfo])
return null
}

View File

@@ -24,11 +24,24 @@ const Context = createContext({} as DocumentInfoContext)
export const useDocumentInfo = (): DocumentInfoContext => useContext(Context)
export const DocumentInfoProvider: React.FC<Props> = ({ children, ...rest }) => {
const [propsToUse, setPropsToUse] = useState<Props>({
...rest,
})
const { globalSlug, collectionSlug, id } = propsToUse
const {
routes: { api },
serverURL,
collections,
globals,
} = useConfig()
const collectionConfig = collections.find((c) => c.slug === collectionSlug)
const globalConfig = globals.find((g) => g.slug === globalSlug)
const docConfig = collectionConfig || globalConfig
const versionsConfig = docConfig?.versions
const { getPreference, setPreference } = usePreferences()
const { i18n } = useTranslation()
const { permissions } = useAuth()
@@ -41,14 +54,8 @@ export const DocumentInfoProvider: React.FC<Props> = ({ children, ...rest }) =>
const [docPermissions, setDocPermissions] = useState<DocumentPermissions>(null)
const [propsToUse, setPropsToUse] = useState<Props>({
...rest,
})
const [title, setTitle] = useState<string>('')
const { globalSlug, collectionSlug, id, versionsConfig } = propsToUse
const baseURL = `${serverURL}${api}`
let slug: string
let pluralType: 'collections' | 'globals'
@@ -280,7 +287,7 @@ export const DocumentInfoProvider: React.FC<Props> = ({ children, ...rest }) =>
setDocFieldPreferences,
slug,
unpublishedVersions,
versionsConfig,
docConfig,
versions,
setDocumentInfo: setPropsToUse,
setDocumentTitle,

View File

@@ -8,5 +8,4 @@ export type Props = {
globalSlug?: SanitizedGlobalConfig['slug']
id?: number | string
idFromParams?: boolean
versionsConfig?: SanitizedCollectionConfig['versions'] | SanitizedGlobalConfig['versions']
}

View File

@@ -22,7 +22,7 @@ import { ComponentMap } from '../../utilities/buildComponentMap/types'
import { ComponentMapProvider } from '../ComponentMapProvider'
import { SearchParamsProvider } from '../SearchParams'
import { ParamsProvider } from '../Params'
import { DocumentInfoProvider } from '../..'
import { DocumentInfoProvider } from '../DocumentInfo'
type Props = {
config: ClientConfig

View File

@@ -20,5 +20,6 @@ export const PostsCollection: CollectionConfig = {
type: 'upload',
},
],
versions: true,
slug: postsSlug,
}