chore(payload,ui)!:update custom config to separate client and server bundles (#5914)

This commit is contained in:
Paul
2024-04-19 11:52:55 -03:00
committed by GitHub
parent 2ee6a8ec3a
commit 23c5b71f95
27 changed files with 290 additions and 53 deletions

View File

@@ -77,6 +77,12 @@ export const createClientCollectionConfig = ({
})
}
if ('custom' in sanitized && sanitized.custom) {
if ('server' in sanitized.custom && sanitized.custom.server) {
delete sanitized.custom.server
}
}
if ('admin' in sanitized) {
sanitized.admin = { ...sanitized.admin }

View File

@@ -107,7 +107,10 @@ const collectionSchema = joi.object().keys({
}),
joi.boolean(),
),
custom: joi.object().pattern(joi.string(), joi.any()),
custom: joi.object().keys({
client: joi.object().pattern(joi.string(), joi.any()),
server: joi.object().pattern(joi.string(), joi.any()),
}),
dbName: joi.alternatives().try(joi.string(), joi.func()),
defaultSort: joi.string(),
disableDuplicate: joi.bool(),

View File

@@ -314,7 +314,16 @@ export type CollectionConfig = {
*/
auth?: IncomingAuthType | boolean
/** Extension point to add your custom data. */
custom?: Record<string, any>
custom?: {
/**
* Available in client bundle.
*/
client?: Record<string, any>
/**
* Server only.
*/
server?: Record<string, any>
}
/**
* Default field to sort by in collection list view
*/

View File

@@ -49,6 +49,7 @@ export const createClientConfig = async ({
}: {
config: SanitizedConfig
t: TFunction
// eslint-disable-next-line @typescript-eslint/require-await
}): Promise<ClientConfig> => {
const clientConfig: ClientConfig = { ...config }
@@ -83,6 +84,12 @@ export const createClientConfig = async ({
})
}
if ('custom' in clientConfig && clientConfig.custom) {
if ('server' in clientConfig.custom && clientConfig.custom.server) {
delete clientConfig.custom.server
}
}
if ('admin' in clientConfig) {
clientConfig.admin = { ...clientConfig.admin }

View File

@@ -83,7 +83,10 @@ export default joi.object({
cookiePrefix: joi.string(),
cors: [joi.string().valid('*'), joi.array().items(joi.string())],
csrf: joi.array().items(joi.string().allow('')).sparse(),
custom: joi.object().pattern(joi.string(), joi.any()),
custom: joi.object().keys({
client: joi.object().pattern(joi.string(), joi.any()),
server: joi.object().pattern(joi.string(), joi.any()),
}),
db: joi.any(),
debug: joi.boolean(),
defaultDepth: joi.number().min(0).max(30),

View File

@@ -548,7 +548,16 @@ export type Config = {
csrf?: string[]
/** Extension point to add your custom data. */
custom?: Record<string, any>
custom?: {
/**
* Available in client bundle.
*/
client?: Record<string, any>
/**
* Server only.
*/
server?: Record<string, any>
}
/** Pass in a database adapter for use on this project. */
db: DatabaseAdapterResult

View File

@@ -45,6 +45,12 @@ export const createClientFieldConfig = ({
}
})
if ('custom' in field && field.custom) {
if ('server' in field.custom && field.custom.server) {
delete field.custom.server
}
}
if ('options' in field && Array.isArray(field.options)) {
field.options = field.options.map((option) => {
if (typeof option === 'object' && typeof option.label === 'function') {

View File

@@ -37,7 +37,10 @@ export const baseField = joi
update: joi.func(),
}),
admin: baseAdminFields.default(),
custom: joi.object().pattern(joi.string(), joi.any()),
custom: joi.object().keys({
client: joi.object().pattern(joi.string(), joi.any()),
server: joi.object().pattern(joi.string(), joi.any()),
}),
hidden: joi.boolean().default(false),
hooks: joi
.object()
@@ -418,7 +421,10 @@ export const blocks = baseField.keys({
.items(
joi.object({
slug: joi.string().required(),
custom: joi.object().pattern(joi.string(), joi.any()),
custom: joi.object().keys({
client: joi.object().pattern(joi.string(), joi.any()),
server: joi.object().pattern(joi.string(), joi.any()),
}),
dbName: joi.alternatives().try(joi.string(), joi.func()),
fields: joi.array().items(joi.link('#field')),
graphQL: joi.object().keys({
@@ -519,7 +525,10 @@ export const ui = joi.object().keys({
width: joi.string(),
})
.default(),
custom: joi.object().pattern(joi.string(), joi.any()),
custom: joi.object().keys({
client: joi.object().pattern(joi.string(), joi.any()),
server: joi.object().pattern(joi.string(), joi.any()),
}),
label: joi.alternatives().try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
})

View File

@@ -180,7 +180,16 @@ export interface FieldBase {
}
admin?: Admin
/** Extension point to add your custom data. */
custom?: Record<string, any>
custom?: {
/**
* Available in client bundle.
*/
client?: Record<string, any>
/**
* Server only.
*/
server?: Record<string, any>
}
defaultValue?: any
hidden?: boolean
hooks?: {
@@ -426,7 +435,16 @@ export type UIField = {
width?: string
}
/** Extension point to add your custom data. */
custom?: Record<string, any>
custom?: {
/**
* Available in client bundle.
*/
client?: Record<string, any>
/**
* Server only.
*/
server?: Record<string, any>
}
label?: Record<string, string> | string
name: string
type: 'ui'
@@ -637,7 +655,16 @@ export type RadioField = FieldBase & {
export type Block = {
/** Extension point to add your custom data. */
custom?: Record<string, any>
custom?: {
/**
* Available in client bundle.
*/
client?: Record<string, any>
/**
* Server only.
*/
server?: Record<string, any>
}
/**
* Customize the SQL table name
*/

View File

@@ -55,6 +55,12 @@ export const createClientGlobalConfig = ({
}
})
if ('custom' in sanitized && sanitized.custom) {
if ('server' in sanitized.custom && sanitized.custom.server) {
delete sanitized.custom.server
}
}
if ('admin' in sanitized) {
sanitized.admin = { ...sanitized.admin }

View File

@@ -48,7 +48,10 @@ const globalSchema = joi
livePreview: joi.object(livePreviewSchema),
preview: joi.func(),
}),
custom: joi.object().pattern(joi.string(), joi.any()),
custom: joi.object().keys({
client: joi.object().pattern(joi.string(), joi.any()),
server: joi.object().pattern(joi.string(), joi.any()),
}),
dbName: joi.alternatives().try(joi.string(), joi.func()),
endpoints: endpointsSchema,
fields: joi.array(),

View File

@@ -141,7 +141,16 @@ export type GlobalConfig = {
}
admin?: GlobalAdminOptions
/** Extension point to add your custom data. */
custom?: Record<string, any>
custom?: {
/**
* Available in client bundle.
*/
client?: Record<string, any>
/**
* Server only.
*/
server?: Record<string, any>
}
/**
* Customize the SQL table name
*/

View File

@@ -17,6 +17,9 @@ export type FormFieldBase = {
CustomError?: React.ReactNode
CustomLabel?: React.ReactNode
className?: string
custom?: {
client?: Record<string, any>
}
descriptionProps?: FieldDescriptionProps
disabled?: boolean
docPreferences?: DocumentPreferences

View File

@@ -5,6 +5,9 @@ import type { FieldPermissions } from 'payload/types'
import React from 'react'
export type FieldPropsContextType = {
custom?: {
client?: Record<string, any>
}
indexPath?: string
path: string
permissions?: FieldPermissions
@@ -28,6 +31,9 @@ const FieldPropsContext = React.createContext<FieldPropsContextType>({
export type Props = {
children: React.ReactNode
custom?: {
client?: Record<string, any>
}
indexPath?: string
path: string
permissions?: FieldPermissions
@@ -42,6 +48,7 @@ export type Props = {
export const FieldPropsProvider: React.FC<Props> = ({
type,
children,
custom,
indexPath,
path,
permissions,
@@ -53,6 +60,7 @@ export const FieldPropsProvider: React.FC<Props> = ({
<FieldPropsContext.Provider
value={{
type,
custom,
indexPath,
path,
permissions,
@@ -67,6 +75,6 @@ export const FieldPropsProvider: React.FC<Props> = ({
}
export const useFieldProps = () => {
const path = React.useContext(FieldPropsContext)
return path
const props = React.useContext(FieldPropsContext)
return props
}

View File

@@ -17,6 +17,9 @@ import { FieldPropsProvider, useFieldProps } from '../FieldPropsProvider/index.j
type Props = {
CustomField: MappedField['CustomField']
custom?: {
client?: Record<string, any>
}
disabled: boolean
fieldComponentProps?: FieldComponentProps
indexPath?: string
@@ -36,6 +39,7 @@ export const RenderField: React.FC<Props> = ({
name,
type,
CustomField,
custom,
disabled,
fieldComponentProps,
indexPath,
@@ -78,6 +82,7 @@ export const RenderField: React.FC<Props> = ({
return (
<FieldPropsProvider
custom={custom}
indexPath={indexPath}
path={path}
permissions={permissions}

View File

@@ -71,6 +71,7 @@ export const RenderFields: React.FC<Props> = (props) => {
return (
<RenderField
CustomField={CustomField}
custom={f.custom}
disabled={disabled}
fieldComponentProps={fieldComponentProps}
indexPath={indexPath !== undefined ? `${indexPath}.${fieldIndex}` : `${fieldIndex}`}

View File

@@ -200,6 +200,7 @@ export const mapFields = (args: {
CustomDescription,
CustomError,
CustomLabel,
custom: field.custom,
descriptionProps,
disabled: 'admin' in field && 'disabled' in field.admin ? field.admin?.disabled : false,
errorProps,
@@ -754,6 +755,7 @@ export const mapFields = (args: {
<WithServerSideProps Component={CustomFieldComponent} {...fieldComponentProps} />
) : undefined,
cellComponentProps,
custom: field.custom,
disableBulkEdit:
'admin' in field && 'disableBulkEdit' in field.admin && field.admin.disableBulkEdit,
fieldComponentProps,

View File

@@ -68,6 +68,9 @@ export type MappedField = {
CustomCell?: React.ReactNode
CustomField?: React.ReactNode
cellComponentProps: CellComponentProps
custom?: {
client?: Record<string, any>
}
disableBulkEdit?: boolean
disabled?: boolean
fieldComponentProps: FieldComponentProps

View File

@@ -25,7 +25,10 @@ export default buildConfigWithDefaults({
{
name: 'title',
type: 'text',
custom: { description: 'The title of this page' },
custom: {
client: { description: 'The title of this page' },
server: { description: 'The title of this page' },
},
},
{
name: 'myBlocks',
@@ -43,13 +46,22 @@ export default buildConfigWithDefaults({
type: 'text',
},
],
custom: { description: 'The blockOne of this page' },
custom: {
client: { description: 'The blockOne of this page' },
server: { description: 'The blockOne of this page' },
},
},
],
custom: { description: 'The blocks of this page' },
custom: {
client: { description: 'The blocks of this page' },
server: { description: 'The blocks of this page' },
},
},
],
custom: { externalLink: 'https://foo.bar' },
custom: {
client: { externalLink: 'https://foo.bar' },
server: { externalLink: 'https://foo.bar' },
},
},
],
globals: [
@@ -70,10 +82,13 @@ export default buildConfigWithDefaults({
{
name: 'title',
type: 'text',
custom: { description: 'The title of my global' },
custom: {
client: { description: 'The title of my global' },
server: { description: 'The title of my global' },
},
},
],
custom: { foo: 'bar' },
custom: { client: { foo: 'bar' }, server: { foo: 'bar' } },
},
],
endpoints: [
@@ -86,7 +101,7 @@ export default buildConfigWithDefaults({
custom: { description: 'Get the sanitized payload config' },
},
],
custom: { name: 'Customer portal' },
custom: { client: { name: 'Customer portal' }, server: { name: 'Customer portal' } },
onInit: async (payload) => {
await payload.create({
collection: 'users',

View File

@@ -20,20 +20,28 @@ describe('Config', () => {
describe('payload config', () => {
it('allows a custom field at the config root', () => {
const { config } = payload
expect(config.custom).toEqual({ name: 'Customer portal' })
expect(config.custom).toEqual({
client: { name: 'Customer portal' },
server: { name: 'Customer portal' },
})
})
it('allows a custom field in the root endpoints', () => {
const [endpoint] = payload.config.endpoints
expect(endpoint.custom).toEqual({ description: 'Get the sanitized payload config' })
expect(endpoint.custom).toEqual({
description: 'Get the sanitized payload config',
})
})
})
describe('collection config', () => {
it('allows a custom field in collections', () => {
const [collection] = payload.config.collections
expect(collection.custom).toEqual({ externalLink: 'https://foo.bar' })
expect(collection.custom).toEqual({
client: { externalLink: 'https://foo.bar' },
server: { externalLink: 'https://foo.bar' },
})
})
it('allows a custom field in collection endpoints', () => {
@@ -49,7 +57,12 @@ describe('Config', () => {
const [collection] = payload.config.collections
const [field] = collection.fields
expect(field.custom).toEqual({ description: 'The title of this page' })
console.log({ custom: field.custom })
expect(field.custom).toEqual({
client: { description: 'The title of this page' },
server: { description: 'The title of this page' },
})
})
it('allows a custom field in blocks in collection fields', () => {
@@ -57,7 +70,8 @@ describe('Config', () => {
const [, blocksField] = collection.fields
expect((blocksField as BlockField).blocks[0].custom).toEqual({
description: 'The blockOne of this page',
server: { description: 'The blockOne of this page' },
client: { description: 'The blockOne of this page' },
})
})
})
@@ -65,7 +79,7 @@ describe('Config', () => {
describe('global config', () => {
it('allows a custom field in globals', () => {
const [global] = payload.config.globals
expect(global.custom).toEqual({ foo: 'bar' })
expect(global.custom).toEqual({ client: { foo: 'bar' }, server: { foo: 'bar' } })
})
it('allows a custom field in global endpoints', () => {
@@ -79,7 +93,10 @@ describe('Config', () => {
const [global] = payload.config.globals
const [field] = global.fields
expect(field.custom).toEqual({ description: 'The title of my global' })
expect(field.custom).toEqual({
client: { description: 'The title of my global' },
server: { description: 'The title of my global' },
})
})
})
})

View File

@@ -0,0 +1,10 @@
'use client'
import { useFieldProps } from '@payloadcms/ui/forms/FieldPropsProvider'
import React from 'react'
export const UICustomClient: React.FC = () => {
const { custom, path } = useFieldProps()
const client = custom?.client
return <div id={path}>{client?.customValue}</div>
}

View File

@@ -0,0 +1,38 @@
import type { CollectionConfig } from 'payload/types'
import { UICustomClient } from './UICustomClient.js'
import { uiFieldsSlug } from './shared.js'
const UIFields: CollectionConfig = {
slug: uiFieldsSlug,
admin: {
useAsTitle: 'text',
},
defaultSort: 'id',
fields: [
{
name: 'text',
type: 'text',
required: true,
},
{
type: 'ui',
name: 'uiCustomClient',
admin: {
components: {
Field: UICustomClient,
},
},
custom: {
client: {
customValue: `client-side-configuration`,
},
server: {
'new-server-value': 'only available on server',
},
},
},
],
}
export default UIFields

View File

@@ -0,0 +1,2 @@
export const defaultText = 'default-text'
export const uiFieldsSlug = 'ui-fields'

View File

@@ -22,6 +22,7 @@ import RowFields from './collections/Row/index.js'
import SelectFields from './collections/Select/index.js'
import TabsFields from './collections/Tabs/index.js'
import TextFields from './collections/Text/index.js'
import UIFields from './collections/UI/index.js'
import Uploads from './collections/Upload/index.js'
import Uploads2 from './collections/Upload2/index.js'
import Uploads3 from './collections/Uploads3/index.js'
@@ -67,11 +68,20 @@ export const collectionSlugs: CollectionConfig[] = [
Uploads,
Uploads2,
Uploads3,
UIFields,
]
export default buildConfigWithDefaults({
collections: collectionSlugs,
globals: [TabsWithRichText],
custom: {
client: {
'new-value': 'client available',
},
server: {
'new-server-value': 'only available on server',
},
},
localization: {
defaultLocale: 'en',
fallback: true,

View File

@@ -930,6 +930,22 @@ describe('fields', () => {
})
})
describe('ui', () => {
let url: AdminUrlUtil
beforeAll(() => {
url = new AdminUrlUtil(serverURL, 'ui-fields')
})
test('should show custom: client configuration', async () => {
await page.goto(url.create)
const uiField = page.locator('#uiCustomClient')
await expect(uiField).toBeVisible()
await expect(uiField).toContainText('client-side-configuration')
})
})
describe('conditional logic', () => {
let url: AdminUrlUtil
beforeAll(() => {

View File

@@ -43,6 +43,7 @@ import {
selectFieldsSlug,
tabsFieldsSlug,
textFieldsSlug,
uiSlug,
uploadsSlug,
usersSlug,
} from './slugs.js'
@@ -301,6 +302,13 @@ export const seed = async (_payload) => {
depth: 0,
overrideAccess: true,
})
await _payload.create({
collection: uiSlug,
data: {
text: 'text',
},
})
}
export async function clearAndSeedEverything(_payload: Payload) {

View File

@@ -1,28 +1,29 @@
export const usersSlug = 'users' as const
export const arrayFieldsSlug = 'array-fields' as const
export const blockFieldsSlug = 'block-fields' as const
export const checkboxFieldsSlug = 'checkbox-fields' as const
export const codeFieldsSlug = 'code-fields' as const
export const collapsibleFieldsSlug = 'collapsible-fields' as const
export const conditionalLogicSlug = 'conditional-logic' as const
export const dateFieldsSlug = 'date-fields' as const
export const groupFieldsSlug = 'group-fields' as const
export const indexedFieldsSlug = 'indexed-fields' as const
export const jsonFieldsSlug = 'json-fields' as const
export const lexicalFieldsSlug = 'lexical-fields' as const
export const lexicalMigrateFieldsSlug = 'lexical-migrate-fields' as const
export const numberFieldsSlug = 'number-fields' as const
export const pointFieldsSlug = 'point-fields' as const
export const radioFieldsSlug = 'radio-fields' as const
export const relationshipFieldsSlug = 'relationship-fields' as const
export const richTextFieldsSlug = 'rich-text-fields' as const
export const rowFieldsSlug = 'row-fields' as const
export const selectFieldsSlug = 'select-fields' as const
export const tabsFieldsSlug = 'tabs-fields' as const
export const textFieldsSlug = 'text-fields' as const
export const uploadsSlug = 'uploads' as const
export const uploads2Slug = 'uploads2' as const
export const uploads3Slug = 'uploads3' as const
export const usersSlug = 'users'
export const arrayFieldsSlug = 'array-fields'
export const blockFieldsSlug = 'block-fields'
export const checkboxFieldsSlug = 'checkbox-fields'
export const codeFieldsSlug = 'code-fields'
export const collapsibleFieldsSlug = 'collapsible-fields'
export const conditionalLogicSlug = 'conditional-logic'
export const dateFieldsSlug = 'date-fields'
export const groupFieldsSlug = 'group-fields'
export const indexedFieldsSlug = 'indexed-fields'
export const jsonFieldsSlug = 'json-fields'
export const lexicalFieldsSlug = 'lexical-fields'
export const lexicalMigrateFieldsSlug = 'lexical-migrate-fields'
export const numberFieldsSlug = 'number-fields'
export const pointFieldsSlug = 'point-fields'
export const radioFieldsSlug = 'radio-fields'
export const relationshipFieldsSlug = 'relationship-fields'
export const richTextFieldsSlug = 'rich-text-fields'
export const rowFieldsSlug = 'row-fields'
export const selectFieldsSlug = 'select-fields'
export const tabsFieldsSlug = 'tabs-fields'
export const textFieldsSlug = 'text-fields'
export const uploadsSlug = 'uploads'
export const uploads2Slug = 'uploads2'
export const uploads3Slug = 'uploads3'
export const uiSlug = 'ui-fields'
export const collectionSlugs = [
usersSlug,
arrayFieldsSlug,
@@ -49,4 +50,5 @@ export const collectionSlugs = [
uploadsSlug,
uploads2Slug,
uploads3Slug,
uiSlug,
]