chore(ui): ssr document tabs (#5116)
This commit is contained in:
@@ -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}>
|
||||
|
||||
34
packages/next/src/pages/Edit/index.client.tsx
Normal file
34
packages/next/src/pages/Edit/index.client.tsx
Normal 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} />
|
||||
}
|
||||
@@ -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} />
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>>
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
<span className={`${baseClass}__count`}>{pillToRender}</span>
|
||||
<Pill />
|
||||
</Fragment>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -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}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
19
packages/ui/src/providers/Auth/HydrateClientUser/index.tsx
Normal file
19
packages/ui/src/providers/Auth/HydrateClientUser/index.tsx
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -8,5 +8,4 @@ export type Props = {
|
||||
globalSlug?: SanitizedGlobalConfig['slug']
|
||||
id?: number | string
|
||||
idFromParams?: boolean
|
||||
versionsConfig?: SanitizedCollectionConfig['versions'] | SanitizedGlobalConfig['versions']
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -20,5 +20,6 @@ export const PostsCollection: CollectionConfig = {
|
||||
type: 'upload',
|
||||
},
|
||||
],
|
||||
versions: true,
|
||||
slug: postsSlug,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user