feat: allow passing false as PayloadComponent which signals that the component should not be rendered (#7682)

If it's undefined/null => Fallback Component may be rendered
If it's false => No component should be rendered - as if an empty
component was passed in

This ensures that the user does not have to install `@payloadcms/ui`
anymore, which previously exported an empty component to be used in
component paths
This commit is contained in:
Alessio Gravili
2024-08-14 18:31:58 -04:00
committed by GitHub
parent cb7fa00a6f
commit 49a2d70fbb
15 changed files with 41 additions and 18 deletions

View File

@@ -227,8 +227,13 @@ export type MappedClientComponent<TComponentClientProps extends JsonObject = Jso
type: 'client' type: 'client'
} }
export type MappedEmptyComponent = {
type: 'empty'
}
export type MappedComponent<TComponentClientProps extends JsonObject = JsonObject> = export type MappedComponent<TComponentClientProps extends JsonObject = JsonObject> =
| MappedClientComponent<TComponentClientProps> | MappedClientComponent<TComponentClientProps>
| MappedEmptyComponent
| MappedServerComponent<TComponentClientProps> | MappedServerComponent<TComponentClientProps>
| undefined | undefined

View File

@@ -13,7 +13,7 @@ export const apiKeyFields = [
type: 'checkbox', type: 'checkbox',
admin: { admin: {
components: { components: {
Field: '@payloadcms/ui/shared#emptyComponent', Field: false,
}, },
}, },
label: ({ t }) => t('authentication:enableAPIKey'), label: ({ t }) => t('authentication:enableAPIKey'),
@@ -23,7 +23,7 @@ export const apiKeyFields = [
type: 'text', type: 'text',
admin: { admin: {
components: { components: {
Field: '@payloadcms/ui/shared#emptyComponent', Field: false,
}, },
}, },
hooks: { hooks: {

View File

@@ -7,7 +7,7 @@ export const emailFieldConfig: EmailField = {
type: 'email', type: 'email',
admin: { admin: {
components: { components: {
Field: '@payloadcms/ui/shared#emptyComponent', Field: false,
}, },
}, },
hooks: { hooks: {

View File

@@ -7,7 +7,7 @@ export const usernameFieldConfig: TextField = {
type: 'text', type: 'text',
admin: { admin: {
components: { components: {
Field: '@payloadcms/ui/shared#emptyComponent', Field: false,
}, },
}, },
hooks: { hooks: {

View File

@@ -26,7 +26,7 @@ export const verificationFields: Field[] = [
}, },
admin: { admin: {
components: { components: {
Field: '@payloadcms/ui/shared#emptyComponent', Field: false,
}, },
}, },
label: ({ t }) => t('authentication:verified'), label: ({ t }) => t('authentication:verified'),

View File

@@ -4,6 +4,9 @@ export function parsePayloadComponent(payloadComponent: PayloadComponent): {
exportName: string exportName: string
path: string path: string
} { } {
if (!payloadComponent) {
return null
}
const pathAndMaybeExport = const pathAndMaybeExport =
typeof payloadComponent === 'string' ? payloadComponent : payloadComponent.path typeof payloadComponent === 'string' ? payloadComponent : payloadComponent.path

View File

@@ -13,7 +13,7 @@ import type { default as sharp } from 'sharp'
import type { DeepRequired } from 'ts-essentials' import type { DeepRequired } from 'ts-essentials'
import type { RichTextAdapterProvider } from '../admin/RichText.js' import type { RichTextAdapterProvider } from '../admin/RichText.js'
import type { DocumentTabConfig, MappedComponent, RichTextAdapter } from '../admin/types.js' import type { DocumentTabConfig, RichTextAdapter } from '../admin/types.js'
import type { AdminViewConfig, ServerSideEditViewProps } from '../admin/views/types.js' import type { AdminViewConfig, ServerSideEditViewProps } from '../admin/views/types.js'
import type { Permissions } from '../auth/index.js' import type { Permissions } from '../auth/index.js'
import type { import type {
@@ -38,12 +38,12 @@ import type { PayloadLogger } from '../utilities/logger.js'
/** /**
* The string path pointing to the React component. If one of the generics is `never`, you effectively mark it as a server-only or client-only component. * The string path pointing to the React component. If one of the generics is `never`, you effectively mark it as a server-only or client-only component.
* *
* If the path is an empty string, it will be treated as () => null * If it is `false` an empty component will be rendered.
*/ */
export type PayloadComponent< export type PayloadComponent<
TComponentServerProps extends never | object = Record<string, any>, TComponentServerProps extends never | object = Record<string, any>,
TComponentClientProps extends never | object = Record<string, any>, TComponentClientProps extends never | object = Record<string, any>,
> = RawPayloadComponent<TComponentServerProps, TComponentClientProps> | string > = RawPayloadComponent<TComponentServerProps, TComponentClientProps> | false | string
// We need the actual object as its own type, otherwise the infers for the PayloadClientReactComponent / PayloadServerReactComponent will not work due to the string union. // We need the actual object as its own type, otherwise the infers for the PayloadClientReactComponent / PayloadServerReactComponent will not work due to the string union.
// We also NEED to actually use those generics for this to work, thus they are part of the props. // We also NEED to actually use those generics for this to work, thus they are part of the props.

View File

@@ -17,7 +17,7 @@ const baseVersionFields: Field[] = [
type: 'select', type: 'select',
admin: { admin: {
components: { components: {
Field: '@payloadcms/ui/shared#emptyComponent', Field: false,
}, },
disableBulkEdit: true, disableBulkEdit: true,
}, },

View File

@@ -3,5 +3,5 @@ import type { RichTextCustomElement } from '../../../types.js'
export const textAlign: RichTextCustomElement = { export const textAlign: RichTextCustomElement = {
name: 'alignment', name: 'alignment',
Button: '@payloadcms/richtext-slate/client#TextAlignElementButton', Button: '@payloadcms/richtext-slate/client#TextAlignElementButton',
Element: '@payloadcms/ui/shared#emptyComponent', Element: false,
} }

View File

@@ -21,5 +21,3 @@ export {
} from '../../utilities/groupNavItems.js' } from '../../utilities/groupNavItems.js'
export { hasSavePermission } from '../../utilities/hasSavePermission.js' export { hasSavePermission } from '../../utilities/hasSavePermission.js'
export { isEditing } from '../../utilities/isEditing.js' export { isEditing } from '../../utilities/isEditing.js'
export const emptyComponent = () => null

View File

@@ -58,6 +58,10 @@ export const RenderComponent: React.FC<{
)) ))
} }
if (mappedComponent.type === 'empty') {
return null
}
if (mappedComponent.RenderedComponent) { if (mappedComponent.RenderedComponent) {
return mappedComponent.RenderedComponent return mappedComponent.RenderedComponent
} }

View File

@@ -257,7 +257,9 @@ export const createClientCollectionConfig = ({
Component: createMappedComponent( Component: createMappedComponent(
hasEditView && hasEditView &&
'Component' in collection.admin.components.views.edit.default && 'Component' in collection.admin.components.views.edit.default &&
collection.admin.components.views.edit.default.Component, collection.admin.components.views.edit.default.Component
? collection.admin.components.views.edit.default.Component
: null,
{ {
collectionSlug: collection.slug, collectionSlug: collection.slug,
}, },
@@ -312,7 +314,9 @@ export const createClientCollectionConfig = ({
clientCollection.admin.components.views.list.Component = createMappedComponent( clientCollection.admin.components.views.list.Component = createMappedComponent(
hasListView && hasListView &&
'Component' in collection.admin.components.views.list && 'Component' in collection.admin.components.views.list &&
collection.admin.components.views.list.Component, collection.admin.components.views.list.Component
? collection.admin.components.views.list.Component
: null,
{ {
collectionSlug: collection.slug, collectionSlug: collection.slug,
}, },

View File

@@ -3,7 +3,7 @@ import type { ImportMap, PayloadComponent, ResolvedComponent } from 'payload'
import { parsePayloadComponent } from 'payload/shared' import { parsePayloadComponent } from 'payload/shared'
/** /**
* Gets th resolved React component from `PayloadComponent` from the importMap * Gets the resolved React component from `PayloadComponent` from the importMap
*/ */
export const getComponent = < export const getComponent = <
TComponentServerProps extends object, TComponentServerProps extends object,
@@ -23,6 +23,7 @@ export const getComponent = <
silent?: boolean silent?: boolean
}): ResolvedComponent<TComponentServerProps, TComponentClientProps> => { }): ResolvedComponent<TComponentServerProps, TComponentClientProps> => {
if (!payloadComponent) { if (!payloadComponent) {
// undefined, null or false
return { return {
Component: undefined, Component: undefined,
clientProps: undefined, clientProps: undefined,

View File

@@ -31,7 +31,7 @@ export function getCreateMappedComponent({
Fallback: React.FC<any>, Fallback: React.FC<any>,
identifier: string, identifier: string,
): MappedComponent => { ): MappedComponent => {
if (!payloadComponent) { if (payloadComponent === undefined || payloadComponent === null) {
if (!Fallback) { if (!Fallback) {
return undefined return undefined
} }
@@ -55,6 +55,12 @@ export function getCreateMappedComponent({
} }
} }
if (payloadComponent === false) {
return {
type: 'empty',
}
}
const resolvedComponent = const resolvedComponent =
payloadComponent && payloadComponent &&
typeof payloadComponent === 'object' && typeof payloadComponent === 'object' &&
@@ -107,7 +113,7 @@ export function getCreateMappedComponent({
fallback, fallback,
identifier, identifier,
) => { ) => {
if (!payloadComponent && !fallback) { if ((payloadComponent === undefined || payloadComponent === null) && !fallback) {
return undefined as any return undefined as any
} }

View File

@@ -138,7 +138,9 @@ export const createClientGlobalConfig = ({
Component: createMappedComponent( Component: createMappedComponent(
hasEditView && hasEditView &&
'Component' in global.admin.components.views.edit.default && 'Component' in global.admin.components.views.edit.default &&
global.admin.components.views.edit.default.Component, global.admin.components.views.edit.default.Component
? global.admin.components.views.edit.default.Component
: null,
{ {
globalSlug: global.slug, globalSlug: global.slug,
}, },