refactor(next): simplify document tab rendering logic (#12795)
Simplifies the rendering logic around document tabs in the following ways: - Merges default tabs with custom tabs in a more predictable way, now there is only a single array of tabs to iterate over, no more concept of "custom" tabs vs "default" tabs, there's now just "tabs" - Deduplicates rendering conditions for all tabs, now any changes to default tabs would also apply to custom tabs with half the code - Removes unnecessary `getCustomViews` function, this is a relic of the past - Removes unnecessary `getViewConfig` function, this is a relic of the past - Removes unused `references`, `relationships`, and `version` key placeholders, these are relics of the past - Prevents tab conditions from running twice unnecessarily - Other misc. cleanup like unnecessarily casting the tab conditions result to a boolean, etc.
This commit is contained in:
@@ -15,7 +15,6 @@ export const DocumentTab: React.FC<
|
||||
const {
|
||||
apiURL,
|
||||
collectionConfig,
|
||||
condition,
|
||||
globalConfig,
|
||||
href: tabHref,
|
||||
i18n,
|
||||
@@ -49,48 +48,40 @@ export const DocumentTab: React.FC<
|
||||
})
|
||||
}
|
||||
|
||||
const meetsCondition =
|
||||
!condition ||
|
||||
(condition && Boolean(condition({ collectionConfig, config, globalConfig, permissions })))
|
||||
const labelToRender =
|
||||
typeof label === 'function'
|
||||
? label({
|
||||
t: i18n.t,
|
||||
})
|
||||
: label
|
||||
|
||||
if (meetsCondition) {
|
||||
const labelToRender =
|
||||
typeof label === 'function'
|
||||
? label({
|
||||
t: i18n.t,
|
||||
})
|
||||
: label
|
||||
|
||||
return (
|
||||
<DocumentTabLink
|
||||
adminRoute={routes.admin}
|
||||
ariaLabel={labelToRender}
|
||||
baseClass={baseClass}
|
||||
href={href}
|
||||
isActive={isActive}
|
||||
newTab={newTab}
|
||||
>
|
||||
<span className={`${baseClass}__label`}>
|
||||
{labelToRender}
|
||||
{Pill || Pill_Component ? (
|
||||
<Fragment>
|
||||
|
||||
{RenderServerComponent({
|
||||
Component: Pill,
|
||||
Fallback: Pill_Component,
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
payload,
|
||||
permissions,
|
||||
} satisfies ServerProps,
|
||||
})}
|
||||
</Fragment>
|
||||
) : null}
|
||||
</span>
|
||||
</DocumentTabLink>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
return (
|
||||
<DocumentTabLink
|
||||
adminRoute={routes.admin}
|
||||
ariaLabel={labelToRender}
|
||||
baseClass={baseClass}
|
||||
href={href}
|
||||
isActive={isActive}
|
||||
newTab={newTab}
|
||||
>
|
||||
<span className={`${baseClass}__label`}>
|
||||
{labelToRender}
|
||||
{Pill || Pill_Component ? (
|
||||
<Fragment>
|
||||
|
||||
{RenderServerComponent({
|
||||
Component: Pill,
|
||||
Fallback: Pill_Component,
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
payload,
|
||||
permissions,
|
||||
} satisfies ServerProps,
|
||||
})}
|
||||
</Fragment>
|
||||
) : null}
|
||||
</span>
|
||||
</DocumentTabLink>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import type { DocumentViewConfig, SanitizedCollectionConfig, SanitizedGlobalConfig } from 'payload'
|
||||
|
||||
import { documentViewKeys } from './tabs/index.js'
|
||||
|
||||
export const getCustomViews = (args: {
|
||||
collectionConfig: SanitizedCollectionConfig
|
||||
globalConfig: SanitizedGlobalConfig
|
||||
}): DocumentViewConfig[] => {
|
||||
const { collectionConfig, globalConfig } = args
|
||||
|
||||
let customViews: DocumentViewConfig[]
|
||||
|
||||
if (collectionConfig) {
|
||||
const collectionViewsConfig =
|
||||
typeof collectionConfig?.admin?.components?.views?.edit === 'object' &&
|
||||
typeof collectionConfig?.admin?.components?.views?.edit !== 'function'
|
||||
? collectionConfig?.admin?.components?.views?.edit
|
||||
: undefined
|
||||
|
||||
customViews = Object.entries(collectionViewsConfig || {}).reduce((prev, [key, view]) => {
|
||||
if (documentViewKeys.includes(key)) {
|
||||
return prev
|
||||
}
|
||||
|
||||
return [...prev, { ...view, key }]
|
||||
}, [])
|
||||
}
|
||||
|
||||
if (globalConfig) {
|
||||
const globalViewsConfig =
|
||||
typeof globalConfig?.admin?.components?.views?.edit === 'object' &&
|
||||
typeof globalConfig?.admin?.components?.views?.edit !== 'function'
|
||||
? globalConfig?.admin?.components?.views?.edit
|
||||
: undefined
|
||||
|
||||
customViews = Object.entries(globalViewsConfig || {}).reduce((prev, [key, view]) => {
|
||||
if (documentViewKeys.includes(key)) {
|
||||
return prev
|
||||
}
|
||||
|
||||
return [...prev, { ...view, key }]
|
||||
}, [])
|
||||
}
|
||||
|
||||
return customViews
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { DocumentViewConfig, SanitizedCollectionConfig, SanitizedGlobalConfig } from 'payload'
|
||||
|
||||
export const getViewConfig = (args: {
|
||||
collectionConfig: SanitizedCollectionConfig
|
||||
globalConfig: SanitizedGlobalConfig
|
||||
name: string
|
||||
}): DocumentViewConfig => {
|
||||
const { name, collectionConfig, globalConfig } = args
|
||||
|
||||
if (collectionConfig) {
|
||||
const collectionConfigViewsConfig =
|
||||
typeof collectionConfig?.admin?.components?.views?.edit === 'object' &&
|
||||
typeof collectionConfig?.admin?.components?.views?.edit !== 'function'
|
||||
? collectionConfig?.admin?.components?.views?.edit
|
||||
: undefined
|
||||
|
||||
return collectionConfigViewsConfig?.[name]
|
||||
}
|
||||
|
||||
if (globalConfig) {
|
||||
const globalConfigViewsConfig =
|
||||
typeof globalConfig?.admin?.components?.views?.edit === 'object' &&
|
||||
typeof globalConfig?.admin?.components?.views?.edit !== 'function'
|
||||
? globalConfig?.admin?.components?.views?.edit
|
||||
: undefined
|
||||
|
||||
return globalConfigViewsConfig?.[name]
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -11,12 +11,10 @@ import type {
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import React from 'react'
|
||||
|
||||
import { getCustomViews } from './getCustomViews.js'
|
||||
import { getViewConfig } from './getViewConfig.js'
|
||||
import './index.scss'
|
||||
import { ShouldRenderTabs } from './ShouldRenderTabs.js'
|
||||
import { DocumentTab } from './Tab/index.js'
|
||||
import { tabs as defaultTabs } from './tabs/index.js'
|
||||
import { getTabs } from './tabs/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'doc-tabs'
|
||||
|
||||
@@ -30,111 +28,54 @@ export const DocumentTabs: React.FC<{
|
||||
const { collectionConfig, globalConfig, i18n, payload, permissions } = props
|
||||
const { config } = payload
|
||||
|
||||
const customViews = getCustomViews({ collectionConfig, globalConfig })
|
||||
const tabs = getTabs({
|
||||
collectionConfig,
|
||||
globalConfig,
|
||||
})
|
||||
|
||||
return (
|
||||
<ShouldRenderTabs>
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__tabs-container`}>
|
||||
<ul className={`${baseClass}__tabs`}>
|
||||
{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`
|
||||
?.sort(([, a], [, b]) => {
|
||||
if (a.order === undefined && b.order === undefined) {
|
||||
return 0
|
||||
} else if (a.order === undefined) {
|
||||
return 1
|
||||
} else if (b.order === undefined) {
|
||||
return -1
|
||||
}
|
||||
return a.order - b.order
|
||||
})
|
||||
?.map(([name, tab], index) => {
|
||||
const viewConfig = getViewConfig({ name, collectionConfig, globalConfig })
|
||||
const tabFromConfig = viewConfig && 'tab' in viewConfig ? viewConfig.tab : undefined
|
||||
{tabs?.map(({ tab, viewPath }, index) => {
|
||||
const { condition } = tab || {}
|
||||
|
||||
const { condition } = tabFromConfig || {}
|
||||
|
||||
const meetsCondition =
|
||||
!condition ||
|
||||
(condition &&
|
||||
Boolean(condition({ collectionConfig, config, globalConfig, permissions })))
|
||||
|
||||
const path = viewConfig && 'path' in viewConfig ? viewConfig.path : ''
|
||||
|
||||
if (meetsCondition) {
|
||||
if (tabFromConfig?.Component) {
|
||||
return RenderServerComponent({
|
||||
clientProps: {
|
||||
path,
|
||||
} satisfies DocumentTabClientProps,
|
||||
Component: tabFromConfig.Component,
|
||||
importMap: payload.importMap,
|
||||
key: `tab-${index}`,
|
||||
serverProps: {
|
||||
collectionConfig,
|
||||
globalConfig,
|
||||
i18n,
|
||||
payload,
|
||||
permissions,
|
||||
} satisfies DocumentTabServerPropsOnly,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<DocumentTab
|
||||
key={`tab-${index}`}
|
||||
path={path}
|
||||
{...{
|
||||
...props,
|
||||
...(tab || {}),
|
||||
...(tabFromConfig || {}),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const meetsCondition =
|
||||
!condition || condition({ collectionConfig, config, globalConfig, permissions })
|
||||
|
||||
if (!meetsCondition) {
|
||||
return null
|
||||
})}
|
||||
{customViews?.map((customViewConfig, index) => {
|
||||
if ('tab' in customViewConfig) {
|
||||
const { tab } = customViewConfig
|
||||
|
||||
const path = 'path' in customViewConfig ? customViewConfig.path : ''
|
||||
|
||||
if (tab.Component) {
|
||||
return RenderServerComponent({
|
||||
clientProps: {
|
||||
path,
|
||||
} satisfies DocumentTabClientProps,
|
||||
Component: tab.Component,
|
||||
importMap: payload.importMap,
|
||||
key: `tab-custom-${index}`,
|
||||
serverProps: {
|
||||
collectionConfig,
|
||||
globalConfig,
|
||||
i18n,
|
||||
payload,
|
||||
permissions,
|
||||
} satisfies DocumentTabServerPropsOnly,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<DocumentTab
|
||||
key={`tab-custom-${index}`}
|
||||
path={path}
|
||||
{...{
|
||||
...props,
|
||||
...tab,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
if (tab?.Component) {
|
||||
return RenderServerComponent({
|
||||
clientProps: {
|
||||
path: viewPath,
|
||||
} satisfies DocumentTabClientProps,
|
||||
Component: tab.Component,
|
||||
importMap: payload.importMap,
|
||||
key: `tab-${index}`,
|
||||
serverProps: {
|
||||
collectionConfig,
|
||||
globalConfig,
|
||||
i18n,
|
||||
payload,
|
||||
permissions,
|
||||
} satisfies DocumentTabServerPropsOnly,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<DocumentTab
|
||||
key={`tab-${index}`}
|
||||
path={viewPath}
|
||||
{...{
|
||||
...props,
|
||||
...tab,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -1,83 +1,97 @@
|
||||
import type { DocumentTabConfig } from 'payload'
|
||||
import type React from 'react'
|
||||
import type { DocumentTabConfig, SanitizedCollectionConfig, SanitizedGlobalConfig } from 'payload'
|
||||
|
||||
import { VersionsPill } from './VersionsPill/index.js'
|
||||
|
||||
export const documentViewKeys = [
|
||||
'api',
|
||||
'default',
|
||||
'livePreview',
|
||||
'references',
|
||||
'relationships',
|
||||
'version',
|
||||
'versions',
|
||||
]
|
||||
export const documentViewKeys = ['api', 'default', 'livePreview', 'versions']
|
||||
|
||||
export type DocumentViewKey = (typeof documentViewKeys)[number]
|
||||
|
||||
export const tabs: Record<
|
||||
DocumentViewKey,
|
||||
{
|
||||
order?: number // TODO: expose this to the globalConfig config
|
||||
Pill_Component?: React.FC
|
||||
} & DocumentTabConfig
|
||||
> = {
|
||||
api: {
|
||||
condition: ({ collectionConfig, globalConfig }) =>
|
||||
(collectionConfig && !collectionConfig?.admin?.hideAPIURL) ||
|
||||
(globalConfig && !globalConfig?.admin?.hideAPIURL),
|
||||
href: '/api',
|
||||
label: 'API',
|
||||
order: 1000,
|
||||
},
|
||||
default: {
|
||||
href: '',
|
||||
// isActive: ({ href, location }) =>
|
||||
// location.pathname === href || location.pathname === `${href}/create`,
|
||||
label: ({ t }) => t('general:edit'),
|
||||
order: 0,
|
||||
},
|
||||
livePreview: {
|
||||
condition: ({ collectionConfig, config, globalConfig }) => {
|
||||
if (collectionConfig) {
|
||||
return Boolean(
|
||||
config?.admin?.livePreview?.collections?.includes(collectionConfig.slug) ||
|
||||
collectionConfig?.admin?.livePreview,
|
||||
)
|
||||
}
|
||||
export const getTabs = ({
|
||||
collectionConfig,
|
||||
globalConfig,
|
||||
}: {
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
globalConfig?: SanitizedGlobalConfig
|
||||
}): { tab: DocumentTabConfig; viewPath: string }[] => {
|
||||
const customViews =
|
||||
collectionConfig?.admin?.components?.views?.edit ||
|
||||
globalConfig?.admin?.components?.views?.edit ||
|
||||
{}
|
||||
|
||||
if (globalConfig) {
|
||||
return Boolean(
|
||||
config?.admin?.livePreview?.globals?.includes(globalConfig.slug) ||
|
||||
globalConfig?.admin?.livePreview,
|
||||
)
|
||||
}
|
||||
|
||||
return false
|
||||
return [
|
||||
{
|
||||
tab: {
|
||||
href: '',
|
||||
label: ({ t }) => t('general:edit'),
|
||||
...(customViews?.['default']?.tab || {}),
|
||||
},
|
||||
viewPath: '/',
|
||||
},
|
||||
href: '/preview',
|
||||
label: ({ t }) => t('general:livePreview'),
|
||||
order: 100,
|
||||
},
|
||||
references: {
|
||||
condition: () => false,
|
||||
},
|
||||
relationships: {
|
||||
condition: () => false,
|
||||
},
|
||||
version: {
|
||||
condition: () => false,
|
||||
},
|
||||
versions: {
|
||||
condition: ({ collectionConfig, globalConfig, permissions }) =>
|
||||
Boolean(
|
||||
(collectionConfig?.versions &&
|
||||
permissions?.collections?.[collectionConfig?.slug]?.readVersions) ||
|
||||
(globalConfig?.versions && permissions?.globals?.[globalConfig?.slug]?.readVersions),
|
||||
),
|
||||
href: '/versions',
|
||||
label: ({ t }) => t('version:versions'),
|
||||
order: 200,
|
||||
Pill_Component: VersionsPill,
|
||||
},
|
||||
{
|
||||
tab: {
|
||||
condition: ({ collectionConfig, config, globalConfig }) => {
|
||||
if (collectionConfig) {
|
||||
return Boolean(
|
||||
config?.admin?.livePreview?.collections?.includes(collectionConfig.slug) ||
|
||||
collectionConfig?.admin?.livePreview,
|
||||
)
|
||||
}
|
||||
|
||||
if (globalConfig) {
|
||||
return Boolean(
|
||||
config?.admin?.livePreview?.globals?.includes(globalConfig.slug) ||
|
||||
globalConfig?.admin?.livePreview,
|
||||
)
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
href: '/preview',
|
||||
label: ({ t }) => t('general:livePreview'),
|
||||
...(customViews?.['livePreview']?.tab || {}),
|
||||
},
|
||||
viewPath: '/preview',
|
||||
},
|
||||
{
|
||||
tab: {
|
||||
condition: ({ collectionConfig, globalConfig, permissions }) =>
|
||||
Boolean(
|
||||
(collectionConfig?.versions &&
|
||||
permissions?.collections?.[collectionConfig?.slug]?.readVersions) ||
|
||||
(globalConfig?.versions && permissions?.globals?.[globalConfig?.slug]?.readVersions),
|
||||
),
|
||||
href: '/versions',
|
||||
label: ({ t }) => t('version:versions'),
|
||||
Pill_Component: VersionsPill,
|
||||
...(customViews?.['versions']?.tab || {}),
|
||||
},
|
||||
viewPath: '/versions',
|
||||
},
|
||||
{
|
||||
tab: {
|
||||
condition: ({ collectionConfig, globalConfig }) =>
|
||||
(collectionConfig && !collectionConfig?.admin?.hideAPIURL) ||
|
||||
(globalConfig && !globalConfig?.admin?.hideAPIURL),
|
||||
href: '/api',
|
||||
label: 'API',
|
||||
...(customViews?.['api']?.tab || {}),
|
||||
},
|
||||
viewPath: '/api',
|
||||
},
|
||||
].concat(
|
||||
Object.entries(customViews).reduce((acc, [key, value]) => {
|
||||
if (documentViewKeys.includes(key)) {
|
||||
return acc
|
||||
}
|
||||
|
||||
if (value?.tab) {
|
||||
acc.push({
|
||||
tab: value.tab,
|
||||
viewPath: 'path' in value ? value.path : '',
|
||||
})
|
||||
}
|
||||
|
||||
return acc
|
||||
}, []),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import type {
|
||||
import { Gutter, RenderTitle } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
import './index.scss'
|
||||
import { DocumentTabs } from './Tabs/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = `doc-header`
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ export interface Config {
|
||||
with300documents: With300Document;
|
||||
'with-list-drawer': WithListDrawer;
|
||||
placeholder: Placeholder;
|
||||
'use-as-title-group-field': UseAsTitleGroupField;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
@@ -119,6 +120,7 @@ export interface Config {
|
||||
with300documents: With300DocumentsSelect<false> | With300DocumentsSelect<true>;
|
||||
'with-list-drawer': WithListDrawerSelect<false> | WithListDrawerSelect<true>;
|
||||
placeholder: PlaceholderSelect<false> | PlaceholderSelect<true>;
|
||||
'use-as-title-group-field': UseAsTitleGroupFieldSelect<false> | UseAsTitleGroupFieldSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
@@ -523,6 +525,16 @@ export interface Placeholder {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "use-as-title-group-field".
|
||||
*/
|
||||
export interface UseAsTitleGroupField {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
@@ -621,6 +633,10 @@ export interface PayloadLockedDocument {
|
||||
| ({
|
||||
relationTo: 'placeholder';
|
||||
value: string | Placeholder;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'use-as-title-group-field';
|
||||
value: string | UseAsTitleGroupField;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
@@ -987,6 +1003,15 @@ export interface PlaceholderSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "use-as-title-group-field_select".
|
||||
*/
|
||||
export interface UseAsTitleGroupFieldSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
|
||||
Reference in New Issue
Block a user