feat(richtext-lexical)!: initialize lexical during sanitization (#6119)
BREAKING: - sanitizeFields is now an async function - the richText adapters now return a function instead of returning the adapter directly
This commit is contained in:
@@ -2,16 +2,19 @@ import { SanitizedConfig, sanitizeConfig } from 'payload/config'
|
||||
import { Config } from 'payload/config'
|
||||
import { getLocalizedSortProperty } from './getLocalizedSortProperty.js'
|
||||
|
||||
const config = sanitizeConfig({
|
||||
let config: SanitizedConfig
|
||||
|
||||
describe('get localized sort property', () => {
|
||||
beforeAll(async () => {
|
||||
config = (await sanitizeConfig({
|
||||
localization: {
|
||||
locales: ['en', 'es'],
|
||||
defaultLocale: 'en',
|
||||
fallback: true,
|
||||
},
|
||||
} as Config) as SanitizedConfig
|
||||
|
||||
describe('get localized sort property', () => {
|
||||
it('passes through a non-localized sort property', () => {
|
||||
} as Config)) as SanitizedConfig
|
||||
})
|
||||
it('passes through a non-localized sort property', async () => {
|
||||
const result = getLocalizedSortProperty({
|
||||
segments: ['title'],
|
||||
config,
|
||||
|
||||
@@ -462,6 +462,10 @@ function buildObjectType({
|
||||
async resolve(parent, args, context: Context) {
|
||||
let depth = config.defaultDepth
|
||||
if (typeof args.depth !== 'undefined') depth = args.depth
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
const editor: RichTextAdapter = field?.editor
|
||||
|
||||
// RichText fields have their own depth argument in GraphQL.
|
||||
|
||||
@@ -68,6 +68,10 @@ export const traverseFields = ({
|
||||
break
|
||||
|
||||
case 'richText':
|
||||
if (typeof field.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
if (typeof field.editor.generateSchemaMap === 'function') {
|
||||
field.editor.generateSchemaMap({
|
||||
config,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { JSONSchema4 } from 'json-schema'
|
||||
|
||||
import type { SanitizedConfig } from '../config/types.js'
|
||||
import type { Config, SanitizedConfig } from '../config/types.js'
|
||||
import type { Field, FieldBase, RichTextField, Validate } from '../fields/config/types.js'
|
||||
import type { PayloadRequestWithData, RequestContext } from '../types/index.js'
|
||||
import type { WithServerSideProps } from './elements/WithServerSideProps.js'
|
||||
@@ -84,3 +84,15 @@ export type RichTextAdapter<
|
||||
CellComponent: React.FC<any>
|
||||
FieldComponent: React.FC<RichTextFieldProps<Value, AdapterProps, ExtraFieldProperties>>
|
||||
}
|
||||
|
||||
export type RichTextAdapterProvider<
|
||||
Value extends object = object,
|
||||
AdapterProps = any,
|
||||
ExtraFieldProperties = {},
|
||||
> = ({
|
||||
config,
|
||||
}: {
|
||||
config: SanitizedConfig
|
||||
}) =>
|
||||
| Promise<RichTextAdapter<Value, AdapterProps, ExtraFieldProperties>>
|
||||
| RichTextAdapter<Value, AdapterProps, ExtraFieldProperties>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type { RichTextAdapter, RichTextFieldProps } from './RichText.js'
|
||||
export type { RichTextAdapter, RichTextAdapterProvider, RichTextFieldProps } from './RichText.js'
|
||||
export type { CellComponentProps, DefaultCellComponentProps } from './elements/Cell.js'
|
||||
export type { ConditionalDateProps } from './elements/DatePicker.js'
|
||||
export type { DayPickerProps, SharedProps, TimePickerProps } from './elements/DatePicker.js'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import merge from 'deepmerge'
|
||||
|
||||
import type { Config } from '../../config/types.js'
|
||||
import type { Config, SanitizedConfig } from '../../config/types.js'
|
||||
import type { CollectionConfig, SanitizedCollectionConfig } from './types.js'
|
||||
|
||||
import baseAccountLockFields from '../../auth/baseFields/accountLock.js'
|
||||
@@ -17,10 +17,15 @@ import { isPlainObject } from '../../utilities/isPlainObject.js'
|
||||
import baseVersionFields from '../../versions/baseFields.js'
|
||||
import { authDefaults, defaults } from './defaults.js'
|
||||
|
||||
export const sanitizeCollection = (
|
||||
export const sanitizeCollection = async (
|
||||
config: Config,
|
||||
collection: CollectionConfig,
|
||||
): SanitizedCollectionConfig => {
|
||||
/**
|
||||
* If this property is set, RichText fields won't be sanitized immediately. Instead, they will be added to this array as promises
|
||||
* so that you can sanitize them together, after the config has been sanitized.
|
||||
*/
|
||||
richTextSanitizationPromises?: Array<(config: SanitizedConfig) => Promise<void>>,
|
||||
): Promise<SanitizedCollectionConfig> => {
|
||||
// /////////////////////////////////
|
||||
// Make copy of collection config
|
||||
// /////////////////////////////////
|
||||
@@ -151,9 +156,10 @@ export const sanitizeCollection = (
|
||||
// /////////////////////////////////
|
||||
|
||||
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||
sanitized.fields = sanitizeFields({
|
||||
sanitized.fields = await sanitizeFields({
|
||||
config,
|
||||
fields: sanitized.fields,
|
||||
richTextSanitizationPromises,
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable no-use-before-define */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import type { Config, SanitizedConfig } from './types.js'
|
||||
|
||||
import { sanitizeConfig } from './sanitize.js'
|
||||
@@ -16,10 +14,8 @@ export async function buildConfig(config: Config): Promise<SanitizedConfig> {
|
||||
return plugin(configAfterPlugin)
|
||||
}, Promise.resolve(config))
|
||||
|
||||
const sanitizedConfig = sanitizeConfig(configAfterPlugins)
|
||||
|
||||
return sanitizedConfig
|
||||
return await sanitizeConfig(configAfterPlugins)
|
||||
}
|
||||
|
||||
return sanitizeConfig(config)
|
||||
return await sanitizeConfig(config)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { defaultUserCollection } from '../auth/defaultUser.js'
|
||||
import { sanitizeCollection } from '../collections/config/sanitize.js'
|
||||
import { migrationsCollection } from '../database/migrations/migrationsCollection.js'
|
||||
import { InvalidConfiguration } from '../errors/index.js'
|
||||
import sanitizeGlobals from '../globals/config/sanitize.js'
|
||||
import { sanitizeGlobals } from '../globals/config/sanitize.js'
|
||||
import getPreferencesCollection from '../preferences/preferencesCollection.js'
|
||||
import checkDuplicateCollections from '../utilities/checkDuplicateCollections.js'
|
||||
import { isPlainObject } from '../utilities/isPlainObject.js'
|
||||
@@ -41,10 +41,10 @@ const sanitizeAdminConfig = (configToSanitize: Config): Partial<SanitizedConfig>
|
||||
)
|
||||
}
|
||||
|
||||
return sanitizedConfig as Partial<SanitizedConfig>
|
||||
return sanitizedConfig as unknown as Partial<SanitizedConfig>
|
||||
}
|
||||
|
||||
export const sanitizeConfig = (incomingConfig: Config): SanitizedConfig => {
|
||||
export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedConfig> => {
|
||||
const configWithDefaults: Config = merge(defaults, incomingConfig, {
|
||||
isMergeableObject: isPlainObject,
|
||||
}) as Config
|
||||
@@ -97,16 +97,25 @@ export const sanitizeConfig = (incomingConfig: Config): SanitizedConfig => {
|
||||
...(incomingConfig?.i18n ?? {}),
|
||||
}
|
||||
|
||||
configWithDefaults.collections.push(getPreferencesCollection(configWithDefaults))
|
||||
configWithDefaults.collections.push(getPreferencesCollection(config as unknown as Config))
|
||||
configWithDefaults.collections.push(migrationsCollection)
|
||||
|
||||
config.collections = config.collections.map((collection) =>
|
||||
sanitizeCollection(configWithDefaults, collection),
|
||||
const richTextSanitizationPromises: Array<(config: SanitizedConfig) => Promise<void>> = []
|
||||
for (let i = 0; i < config.collections.length; i++) {
|
||||
config.collections[i] = await sanitizeCollection(
|
||||
config as unknown as Config,
|
||||
config.collections[i],
|
||||
richTextSanitizationPromises,
|
||||
)
|
||||
}
|
||||
|
||||
checkDuplicateCollections(config.collections)
|
||||
|
||||
if (config.globals.length > 0) {
|
||||
config.globals = sanitizeGlobals(config as SanitizedConfig)
|
||||
config.globals = await sanitizeGlobals(
|
||||
config as unknown as Config,
|
||||
richTextSanitizationPromises,
|
||||
)
|
||||
}
|
||||
|
||||
if (config.serverURL !== '') {
|
||||
@@ -119,5 +128,20 @@ export const sanitizeConfig = (incomingConfig: Config): SanitizedConfig => {
|
||||
new Set(config.collections.map((c) => c.upload?.adapter).filter(Boolean)),
|
||||
)
|
||||
|
||||
/*
|
||||
Execute richText sanitization
|
||||
*/
|
||||
if (typeof incomingConfig.editor === 'function') {
|
||||
config.editor = await incomingConfig.editor({
|
||||
config: config as SanitizedConfig,
|
||||
})
|
||||
}
|
||||
|
||||
const promises: Promise<void>[] = []
|
||||
for (const sanitizeFunction of richTextSanitizationPromises) {
|
||||
promises.push(sanitizeFunction(config as SanitizedConfig))
|
||||
}
|
||||
await Promise.all(promises)
|
||||
|
||||
return config as SanitizedConfig
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type React from 'react'
|
||||
import type { default as sharp } from 'sharp'
|
||||
import type { DeepRequired } from 'ts-essentials'
|
||||
|
||||
import type { RichTextAdapterProvider } from '../admin/RichText.js'
|
||||
import type { DocumentTab, RichTextAdapter } from '../admin/types.js'
|
||||
import type { AdminView, ServerSideEditViewProps } from '../admin/views/types.js'
|
||||
import type { User } from '../auth/types.js'
|
||||
@@ -527,7 +528,7 @@ export type Config = {
|
||||
*/
|
||||
defaultMaxTextLength?: number
|
||||
/** Default richtext editor to use for richText fields */
|
||||
editor: RichTextAdapter<any, any, any>
|
||||
editor: RichTextAdapterProvider<any, any, any>
|
||||
/**
|
||||
* Email Adapter
|
||||
*
|
||||
@@ -640,9 +641,11 @@ export type Config = {
|
||||
|
||||
export type SanitizedConfig = Omit<
|
||||
DeepRequired<Config>,
|
||||
'collections' | 'endpoint' | 'globals' | 'i18n' | 'localization' | 'upload'
|
||||
'collections' | 'editor' | 'endpoint' | 'globals' | 'i18n' | 'localization' | 'upload'
|
||||
> & {
|
||||
collections: SanitizedCollectionConfig[]
|
||||
/** Default richtext editor to use for richText fields */
|
||||
editor: RichTextAdapter<any, any, any>
|
||||
endpoints: Endpoint[]
|
||||
globals: SanitizedGlobalConfig[]
|
||||
i18n: Required<I18nOptions>
|
||||
|
||||
1
packages/payload/src/exports/i18n.ts
Normal file
1
packages/payload/src/exports/i18n.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { getLocalI18n } from '../translations/getLocalI18n.js'
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Config } from '../../config/types'
|
||||
import { InvalidFieldName, InvalidFieldRelationship, MissingFieldType } from '../../errors'
|
||||
import { sanitizeFields } from './sanitize'
|
||||
import { Config } from '../../config/types.js'
|
||||
import { InvalidFieldName, InvalidFieldRelationship, MissingFieldType } from '../../errors/index.js'
|
||||
import { sanitizeFields } from './sanitize.js'
|
||||
import type {
|
||||
ArrayField,
|
||||
Block,
|
||||
@@ -9,11 +9,11 @@ import type {
|
||||
Field,
|
||||
NumberField,
|
||||
TextField,
|
||||
} from './types'
|
||||
} from './types.js'
|
||||
|
||||
describe('sanitizeFields', () => {
|
||||
const config = {} as Config
|
||||
it('should throw on missing type field', () => {
|
||||
it('should throw on missing type field', async () => {
|
||||
const fields: Field[] = [
|
||||
// @ts-expect-error
|
||||
{
|
||||
@@ -21,15 +21,15 @@ describe('sanitizeFields', () => {
|
||||
name: 'Some Collection',
|
||||
},
|
||||
]
|
||||
expect(() => {
|
||||
sanitizeFields({
|
||||
await expect(async () => {
|
||||
await sanitizeFields({
|
||||
config,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
}).toThrow(MissingFieldType)
|
||||
}).rejects.toThrow(MissingFieldType)
|
||||
})
|
||||
it('should throw on invalid field name', () => {
|
||||
it('should throw on invalid field name', async () => {
|
||||
const fields: Field[] = [
|
||||
{
|
||||
label: 'some.collection',
|
||||
@@ -37,33 +37,35 @@ describe('sanitizeFields', () => {
|
||||
type: 'text',
|
||||
},
|
||||
]
|
||||
expect(() => {
|
||||
sanitizeFields({
|
||||
await expect(async () => {
|
||||
await sanitizeFields({
|
||||
config,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
}).toThrow(InvalidFieldName)
|
||||
}).rejects.toThrow(InvalidFieldName)
|
||||
})
|
||||
|
||||
describe('auto-labeling', () => {
|
||||
it('should populate label if missing', () => {
|
||||
it('should populate label if missing', async () => {
|
||||
const fields: Field[] = [
|
||||
{
|
||||
name: 'someField',
|
||||
type: 'text',
|
||||
},
|
||||
]
|
||||
const sanitizedField = sanitizeFields({
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})[0] as TextField
|
||||
})
|
||||
)[0] as TextField
|
||||
expect(sanitizedField.name).toStrictEqual('someField')
|
||||
expect(sanitizedField.label).toStrictEqual('Some Field')
|
||||
expect(sanitizedField.type).toStrictEqual('text')
|
||||
})
|
||||
it('should allow auto-label override', () => {
|
||||
it('should allow auto-label override', async () => {
|
||||
const fields: Field[] = [
|
||||
{
|
||||
label: 'Do not label',
|
||||
@@ -71,18 +73,20 @@ describe('sanitizeFields', () => {
|
||||
type: 'text',
|
||||
},
|
||||
]
|
||||
const sanitizedField = sanitizeFields({
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})[0] as TextField
|
||||
})
|
||||
)[0] as TextField
|
||||
expect(sanitizedField.name).toStrictEqual('someField')
|
||||
expect(sanitizedField.label).toStrictEqual('Do not label')
|
||||
expect(sanitizedField.type).toStrictEqual('text')
|
||||
})
|
||||
|
||||
describe('opt-out', () => {
|
||||
it('should allow label opt-out', () => {
|
||||
it('should allow label opt-out', async () => {
|
||||
const fields: Field[] = [
|
||||
{
|
||||
label: false,
|
||||
@@ -90,17 +94,19 @@ describe('sanitizeFields', () => {
|
||||
type: 'text',
|
||||
},
|
||||
]
|
||||
const sanitizedField = sanitizeFields({
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})[0] as TextField
|
||||
})
|
||||
)[0] as TextField
|
||||
expect(sanitizedField.name).toStrictEqual('someField')
|
||||
expect(sanitizedField.label).toStrictEqual(false)
|
||||
expect(sanitizedField.type).toStrictEqual('text')
|
||||
})
|
||||
|
||||
it('should allow label opt-out for arrays', () => {
|
||||
it('should allow label opt-out for arrays', async () => {
|
||||
const arrayField: ArrayField = {
|
||||
fields: [
|
||||
{
|
||||
@@ -112,17 +118,19 @@ describe('sanitizeFields', () => {
|
||||
name: 'items',
|
||||
type: 'array',
|
||||
}
|
||||
const sanitizedField = sanitizeFields({
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
fields: [arrayField],
|
||||
validRelationships: [],
|
||||
})[0] as ArrayField
|
||||
})
|
||||
)[0] as ArrayField
|
||||
expect(sanitizedField.name).toStrictEqual('items')
|
||||
expect(sanitizedField.label).toStrictEqual(false)
|
||||
expect(sanitizedField.type).toStrictEqual('array')
|
||||
expect(sanitizedField.labels).toBeUndefined()
|
||||
})
|
||||
it('should allow label opt-out for blocks', () => {
|
||||
it('should allow label opt-out for blocks', async () => {
|
||||
const fields: Field[] = [
|
||||
{
|
||||
blocks: [
|
||||
@@ -141,11 +149,13 @@ describe('sanitizeFields', () => {
|
||||
type: 'blocks',
|
||||
},
|
||||
]
|
||||
const sanitizedField = sanitizeFields({
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})[0] as BlockField
|
||||
})
|
||||
)[0] as BlockField
|
||||
expect(sanitizedField.name).toStrictEqual('noLabelBlock')
|
||||
expect(sanitizedField.label).toStrictEqual(false)
|
||||
expect(sanitizedField.type).toStrictEqual('blocks')
|
||||
@@ -153,7 +163,7 @@ describe('sanitizeFields', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should label arrays with plural and singular', () => {
|
||||
it('should label arrays with plural and singular', async () => {
|
||||
const fields: Field[] = [
|
||||
{
|
||||
fields: [
|
||||
@@ -166,18 +176,20 @@ describe('sanitizeFields', () => {
|
||||
type: 'array',
|
||||
},
|
||||
]
|
||||
const sanitizedField = sanitizeFields({
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})[0] as ArrayField
|
||||
})
|
||||
)[0] as ArrayField
|
||||
expect(sanitizedField.name).toStrictEqual('items')
|
||||
expect(sanitizedField.label).toStrictEqual('Items')
|
||||
expect(sanitizedField.type).toStrictEqual('array')
|
||||
expect(sanitizedField.labels).toMatchObject({ plural: 'Items', singular: 'Item' })
|
||||
})
|
||||
|
||||
it('should label blocks with plural and singular', () => {
|
||||
it('should label blocks with plural and singular', async () => {
|
||||
const fields: Field[] = [
|
||||
{
|
||||
blocks: [
|
||||
@@ -190,11 +202,13 @@ describe('sanitizeFields', () => {
|
||||
type: 'blocks',
|
||||
},
|
||||
]
|
||||
const sanitizedField = sanitizeFields({
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})[0] as BlockField
|
||||
})
|
||||
)[0] as BlockField
|
||||
expect(sanitizedField.name).toStrictEqual('specialBlock')
|
||||
expect(sanitizedField.label).toStrictEqual('Special Block')
|
||||
expect(sanitizedField.type).toStrictEqual('blocks')
|
||||
@@ -207,7 +221,7 @@ describe('sanitizeFields', () => {
|
||||
})
|
||||
|
||||
describe('relationships', () => {
|
||||
it('should not throw on valid relationship', () => {
|
||||
it('should not throw on valid relationship', async () => {
|
||||
const validRelationships = ['some-collection']
|
||||
const fields: Field[] = [
|
||||
{
|
||||
@@ -217,12 +231,12 @@ describe('sanitizeFields', () => {
|
||||
type: 'relationship',
|
||||
},
|
||||
]
|
||||
expect(() => {
|
||||
sanitizeFields({ config, fields, validRelationships })
|
||||
await expect(async () => {
|
||||
await sanitizeFields({ config, fields, validRelationships })
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should not throw on valid relationship - multiple', () => {
|
||||
it('should not throw on valid relationship - multiple', async () => {
|
||||
const validRelationships = ['some-collection', 'another-collection']
|
||||
const fields: Field[] = [
|
||||
{
|
||||
@@ -232,12 +246,12 @@ describe('sanitizeFields', () => {
|
||||
type: 'relationship',
|
||||
},
|
||||
]
|
||||
expect(() => {
|
||||
sanitizeFields({ config, fields, validRelationships })
|
||||
await expect(async () => {
|
||||
await sanitizeFields({ config, fields, validRelationships })
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should not throw on valid relationship inside blocks', () => {
|
||||
it('should not throw on valid relationship inside blocks', async () => {
|
||||
const validRelationships = ['some-collection']
|
||||
const relationshipBlock: Block = {
|
||||
fields: [
|
||||
@@ -258,12 +272,12 @@ describe('sanitizeFields', () => {
|
||||
type: 'blocks',
|
||||
},
|
||||
]
|
||||
expect(() => {
|
||||
sanitizeFields({ config, fields, validRelationships })
|
||||
await expect(async () => {
|
||||
await sanitizeFields({ config, fields, validRelationships })
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should throw on invalid relationship', () => {
|
||||
it('should throw on invalid relationship', async () => {
|
||||
const validRelationships = ['some-collection']
|
||||
const fields: Field[] = [
|
||||
{
|
||||
@@ -273,12 +287,12 @@ describe('sanitizeFields', () => {
|
||||
type: 'relationship',
|
||||
},
|
||||
]
|
||||
expect(() => {
|
||||
sanitizeFields({ config, fields, validRelationships })
|
||||
}).toThrow(InvalidFieldRelationship)
|
||||
await expect(async () => {
|
||||
await sanitizeFields({ config, fields, validRelationships })
|
||||
}).rejects.toThrow(InvalidFieldRelationship)
|
||||
})
|
||||
|
||||
it('should throw on invalid relationship - multiple', () => {
|
||||
it('should throw on invalid relationship - multiple', async () => {
|
||||
const validRelationships = ['some-collection', 'another-collection']
|
||||
const fields: Field[] = [
|
||||
{
|
||||
@@ -288,12 +302,12 @@ describe('sanitizeFields', () => {
|
||||
type: 'relationship',
|
||||
},
|
||||
]
|
||||
expect(() => {
|
||||
sanitizeFields({ config, fields, validRelationships })
|
||||
}).toThrow(InvalidFieldRelationship)
|
||||
await expect(async () => {
|
||||
await sanitizeFields({ config, fields, validRelationships })
|
||||
}).rejects.toThrow(InvalidFieldRelationship)
|
||||
})
|
||||
|
||||
it('should throw on invalid relationship inside blocks', () => {
|
||||
it('should throw on invalid relationship inside blocks', async () => {
|
||||
const validRelationships = ['some-collection']
|
||||
const relationshipBlock: Block = {
|
||||
fields: [
|
||||
@@ -314,12 +328,12 @@ describe('sanitizeFields', () => {
|
||||
type: 'blocks',
|
||||
},
|
||||
]
|
||||
expect(() => {
|
||||
sanitizeFields({ config, fields, validRelationships })
|
||||
}).toThrow(InvalidFieldRelationship)
|
||||
await expect(async () => {
|
||||
await sanitizeFields({ config, fields, validRelationships })
|
||||
}).rejects.toThrow(InvalidFieldRelationship)
|
||||
})
|
||||
|
||||
it('should defaultValue of checkbox to false if required and undefined', () => {
|
||||
it('should defaultValue of checkbox to false if required and undefined', async () => {
|
||||
const fields: Field[] = [
|
||||
{
|
||||
name: 'My Checkbox',
|
||||
@@ -328,16 +342,18 @@ describe('sanitizeFields', () => {
|
||||
},
|
||||
]
|
||||
|
||||
const sanitizedField = sanitizeFields({
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})[0] as CheckboxField
|
||||
})
|
||||
)[0] as CheckboxField
|
||||
expect(sanitizedField.defaultValue).toStrictEqual(false)
|
||||
})
|
||||
|
||||
it('should return empty field array if no fields', () => {
|
||||
const sanitizedFields = sanitizeFields({
|
||||
it('should return empty field array if no fields', async () => {
|
||||
const sanitizedFields = await sanitizeFields({
|
||||
config,
|
||||
fields: [],
|
||||
validRelationships: [],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Config } from '../../config/types.js'
|
||||
import type { Config, SanitizedConfig } from '../../config/types.js'
|
||||
import type { Field } from './types.js'
|
||||
|
||||
import { MissingEditorProp } from '../../errors/MissingEditorProp.js'
|
||||
@@ -25,6 +25,12 @@ type Args = {
|
||||
* @default false
|
||||
*/
|
||||
requireFieldLevelRichTextEditor?: boolean
|
||||
/**
|
||||
* If this property is set, RichText fields won't be sanitized immediately. Instead, they will be added to this array as promises
|
||||
* so that you can sanitize them together, after the config has been sanitized.
|
||||
*/
|
||||
richTextSanitizationPromises?: Array<(config: SanitizedConfig) => Promise<void>>
|
||||
|
||||
/**
|
||||
* If not null, will validate that upload and relationship fields do not relate to a collection that is not in this array.
|
||||
* This validation will be skipped if validRelationships is null.
|
||||
@@ -32,17 +38,18 @@ type Args = {
|
||||
validRelationships: null | string[]
|
||||
}
|
||||
|
||||
export const sanitizeFields = ({
|
||||
export const sanitizeFields = async ({
|
||||
config,
|
||||
existingFieldNames = new Set(),
|
||||
fields,
|
||||
requireFieldLevelRichTextEditor = false,
|
||||
richTextSanitizationPromises,
|
||||
validRelationships,
|
||||
}: Args): Field[] => {
|
||||
}: Args): Promise<Field[]> => {
|
||||
if (!fields) return []
|
||||
|
||||
return fields.map((unsanitizedField) => {
|
||||
const field: Field = { ...unsanitizedField }
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const field = fields[i]
|
||||
|
||||
if (!field.type) throw new MissingFieldType(field)
|
||||
|
||||
@@ -143,88 +150,95 @@ export const sanitizeFields = ({
|
||||
|
||||
// Make sure that the richText field has an editor
|
||||
if (field.type === 'richText') {
|
||||
const sanitizeRichText = async (_config: SanitizedConfig) => {
|
||||
if (!field.editor) {
|
||||
if (config.editor && !requireFieldLevelRichTextEditor) {
|
||||
field.editor = config.editor
|
||||
if (_config.editor && !requireFieldLevelRichTextEditor) {
|
||||
// config.editor should be sanitized at this point
|
||||
field.editor = _config.editor
|
||||
} else {
|
||||
throw new MissingEditorProp(field)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof field.editor === 'function') {
|
||||
field.editor = await field.editor({ config: _config })
|
||||
}
|
||||
|
||||
// Add editor adapter hooks to field hooks
|
||||
if (!field.hooks) field.hooks = {}
|
||||
|
||||
if (field?.editor?.hooks?.afterRead?.length) {
|
||||
field.hooks.afterRead = field.hooks.afterRead
|
||||
? field.hooks.afterRead.concat(field.editor.hooks.afterRead)
|
||||
: field.editor.hooks.afterRead
|
||||
}
|
||||
if (field?.editor?.hooks?.beforeChange?.length) {
|
||||
field.hooks.beforeChange = field.hooks.beforeChange
|
||||
? field.hooks.beforeChange.concat(field.editor.hooks.beforeChange)
|
||||
: field.editor.hooks.beforeChange
|
||||
}
|
||||
if (field?.editor?.hooks?.beforeValidate?.length) {
|
||||
field.hooks.beforeValidate = field.hooks.beforeValidate
|
||||
? field.hooks.beforeValidate.concat(field.editor.hooks.beforeValidate)
|
||||
: field.editor.hooks.beforeValidate
|
||||
}
|
||||
if (field?.editor?.hooks?.beforeChange?.length) {
|
||||
field.hooks.beforeChange = field.hooks.beforeChange
|
||||
? field.hooks.beforeChange.concat(field.editor.hooks.beforeChange)
|
||||
: field.editor.hooks.beforeChange
|
||||
const mergeHooks = (hookName: keyof typeof field.editor.hooks) => {
|
||||
if (typeof field.editor === 'function') return
|
||||
|
||||
if (field.editor?.hooks?.[hookName]?.length) {
|
||||
field.hooks[hookName] = field.hooks[hookName]
|
||||
? field.hooks[hookName].concat(field.editor.hooks[hookName])
|
||||
: [...field.editor.hooks[hookName]]
|
||||
}
|
||||
}
|
||||
|
||||
mergeHooks('afterRead')
|
||||
mergeHooks('afterChange')
|
||||
mergeHooks('beforeChange')
|
||||
mergeHooks('beforeValidate')
|
||||
mergeHooks('beforeDuplicate')
|
||||
}
|
||||
if (richTextSanitizationPromises) {
|
||||
richTextSanitizationPromises.push(sanitizeRichText)
|
||||
} else {
|
||||
await sanitizeRichText(config as unknown as SanitizedConfig)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Handle sanitization for any lexical sub-fields here as well
|
||||
if ('fields' in field && field.fields) {
|
||||
field.fields = sanitizeFields({
|
||||
field.fields = await sanitizeFields({
|
||||
config,
|
||||
existingFieldNames: fieldAffectsData(field) ? new Set() : existingFieldNames,
|
||||
fields: field.fields,
|
||||
requireFieldLevelRichTextEditor,
|
||||
richTextSanitizationPromises,
|
||||
validRelationships,
|
||||
})
|
||||
}
|
||||
|
||||
if (field.type === 'tabs') {
|
||||
field.tabs = field.tabs.map((tab) => {
|
||||
const unsanitizedTab = { ...tab }
|
||||
for (let j = 0; j < field.tabs.length; j++) {
|
||||
const tab = field.tabs[j]
|
||||
if (tabHasName(tab) && typeof tab.label === 'undefined') {
|
||||
unsanitizedTab.label = toWords(tab.name)
|
||||
tab.label = toWords(tab.name)
|
||||
}
|
||||
|
||||
unsanitizedTab.fields = sanitizeFields({
|
||||
tab.fields = await sanitizeFields({
|
||||
config,
|
||||
existingFieldNames: tabHasName(tab) ? new Set() : existingFieldNames,
|
||||
fields: tab.fields,
|
||||
requireFieldLevelRichTextEditor,
|
||||
richTextSanitizationPromises,
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
return unsanitizedTab
|
||||
})
|
||||
field.tabs[j] = tab
|
||||
}
|
||||
}
|
||||
|
||||
if ('blocks' in field && field.blocks) {
|
||||
field.blocks = field.blocks.map((block) => {
|
||||
const unsanitizedBlock = { ...block }
|
||||
unsanitizedBlock.labels = !unsanitizedBlock.labels
|
||||
? formatLabels(unsanitizedBlock.slug)
|
||||
: unsanitizedBlock.labels
|
||||
for (let j = 0; j < field.blocks.length; j++) {
|
||||
const block = field.blocks[j]
|
||||
block.labels = !block.labels ? formatLabels(block.slug) : block.labels
|
||||
|
||||
unsanitizedBlock.fields = sanitizeFields({
|
||||
block.fields = await sanitizeFields({
|
||||
config,
|
||||
existingFieldNames: new Set(),
|
||||
fields: block.fields,
|
||||
requireFieldLevelRichTextEditor,
|
||||
richTextSanitizationPromises,
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
return unsanitizedBlock
|
||||
})
|
||||
field.blocks[j] = block
|
||||
}
|
||||
}
|
||||
|
||||
return field
|
||||
})
|
||||
fields[i] = field
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ import type { CSSProperties } from 'react'
|
||||
import monacoeditor from 'monaco-editor' // IMPORTANT - DO NOT REMOVE: This is required for pnpm's default isolated mode to work - even though the import is not used. This is due to a typescript bug: https://github.com/microsoft/TypeScript/issues/47663#issuecomment-1519138189. (tsbugisolatedmode)
|
||||
import type React from 'react'
|
||||
|
||||
import type { RichTextAdapter, RichTextAdapterProvider } from '../../admin/RichText.js'
|
||||
import type {
|
||||
ConditionalDateProps,
|
||||
Description,
|
||||
ErrorProps,
|
||||
LabelProps,
|
||||
RichTextAdapter,
|
||||
RowLabel,
|
||||
} from '../../admin/types.js'
|
||||
import type { SanitizedCollectionConfig, TypeWithID } from '../../collections/config/types.js'
|
||||
@@ -585,7 +585,9 @@ export type RichTextField<
|
||||
Label?: CustomComponent<LabelProps>
|
||||
}
|
||||
}
|
||||
editor?: RichTextAdapter<Value, AdapterProps, AdapterProps>
|
||||
editor?:
|
||||
| RichTextAdapter<Value, AdapterProps, AdapterProps>
|
||||
| RichTextAdapterProvider<Value, AdapterProps, AdapterProps>
|
||||
type: 'richText'
|
||||
} & ExtraProperties
|
||||
|
||||
@@ -594,7 +596,9 @@ export type RichTextFieldRequiredEditor<
|
||||
AdapterProps = any,
|
||||
ExtraProperties = object,
|
||||
> = Omit<RichTextField<Value, AdapterProps, ExtraProperties>, 'editor'> & {
|
||||
editor: RichTextAdapter<Value, AdapterProps, ExtraProperties>
|
||||
editor:
|
||||
| RichTextAdapter<Value, AdapterProps, AdapterProps>
|
||||
| RichTextAdapterProvider<Value, AdapterProps, AdapterProps>
|
||||
}
|
||||
|
||||
export type ArrayField = FieldBase & {
|
||||
|
||||
@@ -141,6 +141,10 @@ export const promise = async ({
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
const editor: RichTextAdapter = field?.editor
|
||||
// This is run here AND in the GraphQL Resolver
|
||||
if (editor?.populationPromises) {
|
||||
|
||||
@@ -264,6 +264,10 @@ export const richText: Validate<object, unknown, unknown, RichTextField> = async
|
||||
value,
|
||||
options,
|
||||
) => {
|
||||
if (typeof options?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
const editor: RichTextAdapter = options?.editor
|
||||
|
||||
return editor.validate(value, options)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Config } from '../../config/types.js'
|
||||
import type { Config, SanitizedConfig } from '../../config/types.js'
|
||||
import type { SanitizedGlobalConfig } from './types.js'
|
||||
|
||||
import defaultAccess from '../../auth/defaultAccess.js'
|
||||
@@ -8,60 +8,66 @@ import mergeBaseFields from '../../fields/mergeBaseFields.js'
|
||||
import { toWords } from '../../utilities/formatLabels.js'
|
||||
import baseVersionFields from '../../versions/baseFields.js'
|
||||
|
||||
const sanitizeGlobals = (config: Config): SanitizedGlobalConfig[] => {
|
||||
export const sanitizeGlobals = async (
|
||||
config: Config,
|
||||
/**
|
||||
* If this property is set, RichText fields won't be sanitized immediately. Instead, they will be added to this array as promises
|
||||
* so that you can sanitize them together, after the config has been sanitized.
|
||||
*/
|
||||
richTextSanitizationPromises?: Array<(config: SanitizedConfig) => Promise<void>>,
|
||||
): Promise<SanitizedGlobalConfig[]> => {
|
||||
const { collections, globals } = config
|
||||
|
||||
const sanitizedGlobals = globals.map((global) => {
|
||||
const sanitizedGlobal = { ...global }
|
||||
|
||||
sanitizedGlobal.label = sanitizedGlobal.label || toWords(sanitizedGlobal.slug)
|
||||
for (let i = 0; i < globals.length; i++) {
|
||||
const global = globals[i]
|
||||
global.label = global.label || toWords(global.slug)
|
||||
|
||||
// /////////////////////////////////
|
||||
// Ensure that collection has required object structure
|
||||
// /////////////////////////////////
|
||||
|
||||
sanitizedGlobal.endpoints = sanitizedGlobal.endpoints ?? []
|
||||
if (!sanitizedGlobal.hooks) sanitizedGlobal.hooks = {}
|
||||
if (!sanitizedGlobal.access) sanitizedGlobal.access = {}
|
||||
if (!sanitizedGlobal.admin) sanitizedGlobal.admin = {}
|
||||
global.endpoints = global.endpoints ?? []
|
||||
if (!global.hooks) global.hooks = {}
|
||||
if (!global.access) global.access = {}
|
||||
if (!global.admin) global.admin = {}
|
||||
|
||||
if (!sanitizedGlobal.access.read) sanitizedGlobal.access.read = defaultAccess
|
||||
if (!sanitizedGlobal.access.update) sanitizedGlobal.access.update = defaultAccess
|
||||
if (!global.access.read) global.access.read = defaultAccess
|
||||
if (!global.access.update) global.access.update = defaultAccess
|
||||
|
||||
if (!sanitizedGlobal.hooks.beforeValidate) sanitizedGlobal.hooks.beforeValidate = []
|
||||
if (!sanitizedGlobal.hooks.beforeChange) sanitizedGlobal.hooks.beforeChange = []
|
||||
if (!sanitizedGlobal.hooks.afterChange) sanitizedGlobal.hooks.afterChange = []
|
||||
if (!sanitizedGlobal.hooks.beforeRead) sanitizedGlobal.hooks.beforeRead = []
|
||||
if (!sanitizedGlobal.hooks.afterRead) sanitizedGlobal.hooks.afterRead = []
|
||||
if (!global.hooks.beforeValidate) global.hooks.beforeValidate = []
|
||||
if (!global.hooks.beforeChange) global.hooks.beforeChange = []
|
||||
if (!global.hooks.afterChange) global.hooks.afterChange = []
|
||||
if (!global.hooks.beforeRead) global.hooks.beforeRead = []
|
||||
if (!global.hooks.afterRead) global.hooks.afterRead = []
|
||||
|
||||
if (sanitizedGlobal.versions) {
|
||||
if (sanitizedGlobal.versions === true) sanitizedGlobal.versions = { drafts: false }
|
||||
if (global.versions) {
|
||||
if (global.versions === true) global.versions = { drafts: false }
|
||||
|
||||
if (sanitizedGlobal.versions.drafts) {
|
||||
if (sanitizedGlobal.versions.drafts === true) {
|
||||
sanitizedGlobal.versions.drafts = {
|
||||
if (global.versions.drafts) {
|
||||
if (global.versions.drafts === true) {
|
||||
global.versions.drafts = {
|
||||
autosave: false,
|
||||
}
|
||||
}
|
||||
|
||||
if (sanitizedGlobal.versions.drafts.autosave === true) {
|
||||
sanitizedGlobal.versions.drafts.autosave = {
|
||||
if (global.versions.drafts.autosave === true) {
|
||||
global.versions.drafts.autosave = {
|
||||
interval: 2000,
|
||||
}
|
||||
}
|
||||
|
||||
sanitizedGlobal.fields = mergeBaseFields(sanitizedGlobal.fields, baseVersionFields)
|
||||
global.fields = mergeBaseFields(global.fields, baseVersionFields)
|
||||
}
|
||||
}
|
||||
|
||||
if (!sanitizedGlobal.custom) sanitizedGlobal.custom = {}
|
||||
if (!global.custom) global.custom = {}
|
||||
|
||||
// /////////////////////////////////
|
||||
// Sanitize fields
|
||||
// /////////////////////////////////
|
||||
let hasUpdatedAt = null
|
||||
let hasCreatedAt = null
|
||||
sanitizedGlobal.fields.some((field) => {
|
||||
global.fields.some((field) => {
|
||||
if (fieldAffectsData(field)) {
|
||||
if (field.name === 'updatedAt') hasUpdatedAt = true
|
||||
if (field.name === 'createdAt') hasCreatedAt = true
|
||||
@@ -69,7 +75,7 @@ const sanitizeGlobals = (config: Config): SanitizedGlobalConfig[] => {
|
||||
return hasCreatedAt && hasUpdatedAt
|
||||
})
|
||||
if (!hasUpdatedAt) {
|
||||
sanitizedGlobal.fields.push({
|
||||
global.fields.push({
|
||||
name: 'updatedAt',
|
||||
type: 'date',
|
||||
admin: {
|
||||
@@ -80,7 +86,7 @@ const sanitizeGlobals = (config: Config): SanitizedGlobalConfig[] => {
|
||||
})
|
||||
}
|
||||
if (!hasCreatedAt) {
|
||||
sanitizedGlobal.fields.push({
|
||||
global.fields.push({
|
||||
name: 'createdAt',
|
||||
type: 'date',
|
||||
admin: {
|
||||
@@ -92,16 +98,15 @@ const sanitizeGlobals = (config: Config): SanitizedGlobalConfig[] => {
|
||||
}
|
||||
|
||||
const validRelationships = collections.map((c) => c.slug) || []
|
||||
sanitizedGlobal.fields = sanitizeFields({
|
||||
global.fields = await sanitizeFields({
|
||||
config,
|
||||
fields: sanitizedGlobal.fields,
|
||||
fields: global.fields,
|
||||
richTextSanitizationPromises,
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
return sanitizedGlobal as SanitizedGlobalConfig
|
||||
})
|
||||
|
||||
return sanitizedGlobals
|
||||
globals[i] = global
|
||||
}
|
||||
|
||||
export default sanitizeGlobals
|
||||
return globals as SanitizedGlobalConfig[]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { sanitizeConfig } from '../config/sanitize.js'
|
||||
import { configToJSONSchema } from './configToJSONSchema.js'
|
||||
|
||||
describe('configToJSONSchema', () => {
|
||||
it('should handle optional arrays with required fields', () => {
|
||||
it('should handle optional arrays with required fields', async () => {
|
||||
const config: Config = {
|
||||
collections: [
|
||||
{
|
||||
@@ -27,7 +27,7 @@ describe('configToJSONSchema', () => {
|
||||
],
|
||||
}
|
||||
|
||||
const sanitizedConfig = sanitizeConfig(config)
|
||||
const sanitizedConfig = await sanitizeConfig(config)
|
||||
const schema = configToJSONSchema(sanitizedConfig, 'text')
|
||||
|
||||
expect(schema?.definitions?.test).toStrictEqual({
|
||||
|
||||
@@ -195,6 +195,9 @@ export function fieldsToJSONSchema(
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
if (typeof field.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
if (field.editor.outputSchema) {
|
||||
fieldSchema = field.editor.outputSchema({
|
||||
collectionIDFieldTypes,
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import type { Config } from 'payload/config'
|
||||
import type { Block, BlockField, Field, FieldWithRichTextRequiredEditor } from 'payload/types'
|
||||
|
||||
import { traverseFields } from '@payloadcms/next/utilities'
|
||||
import { baseBlockFields, sanitizeFields } from 'payload/config'
|
||||
import {
|
||||
beforeChangeTraverseFields,
|
||||
beforeValidateTraverseFields,
|
||||
deepCopyObject,
|
||||
fieldsToJSONSchema,
|
||||
formatLabels,
|
||||
} from 'payload/utilities'
|
||||
import { fieldsToJSONSchema, formatLabels } from 'payload/utilities'
|
||||
|
||||
import type { FeatureProviderProviderServer } from '../types.js'
|
||||
import type { BlocksFeatureClientProps } from './feature.client.js'
|
||||
|
||||
import { cloneDeep } from '../../lexical/utils/cloneDeep.js'
|
||||
import { createNode } from '../typeUtilities.js'
|
||||
import { BlocksFeatureClientComponent } from './feature.client.js'
|
||||
import { BlockNode } from './nodes/BlocksNode.js'
|
||||
@@ -32,21 +26,22 @@ export const BlocksFeature: FeatureProviderProviderServer<
|
||||
BlocksFeatureProps,
|
||||
BlocksFeatureClientProps
|
||||
> = (props) => {
|
||||
// Sanitization taken from payload/src/fields/config/sanitize.ts
|
||||
|
||||
if (props?.blocks?.length) {
|
||||
props.blocks = props.blocks.map((block) => {
|
||||
const blockCopy = cloneDeep(block)
|
||||
|
||||
return {
|
||||
...blockCopy,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
fields: blockCopy.fields.concat(baseBlockFields as FieldWithRichTextRequiredEditor[]),
|
||||
labels: !blockCopy.labels ? formatLabels(blockCopy.slug) : blockCopy.labels,
|
||||
feature: async ({ config: _config }) => {
|
||||
if (props?.blocks?.length) {
|
||||
const validRelationships = _config.collections.map((c) => c.slug) || []
|
||||
|
||||
for (const block of props.blocks) {
|
||||
block.fields = block.fields.concat(baseBlockFields as FieldWithRichTextRequiredEditor[])
|
||||
block.labels = !block.labels ? formatLabels(block.slug) : block.labels
|
||||
|
||||
block.fields = (await sanitizeFields({
|
||||
config: _config as unknown as Config,
|
||||
fields: block.fields,
|
||||
requireFieldLevelRichTextEditor: true,
|
||||
validRelationships,
|
||||
})) as FieldWithRichTextRequiredEditor[]
|
||||
}
|
||||
})
|
||||
// unSanitizedBlock.fields are sanitized in the React component and not here.
|
||||
// That's because we do not have access to the payload config here.
|
||||
}
|
||||
|
||||
// Build clientProps
|
||||
@@ -63,8 +58,6 @@ export const BlocksFeature: FeatureProviderProviderServer<
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
feature: () => {
|
||||
return {
|
||||
ClientComponent: BlocksFeatureClientComponent,
|
||||
clientFeatureProps: clientProps,
|
||||
@@ -104,26 +97,10 @@ export const BlocksFeature: FeatureProviderProviderServer<
|
||||
return currentSchema
|
||||
}
|
||||
|
||||
// sanitize blocks
|
||||
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||
|
||||
const sanitizedBlocks = props.blocks.map((block) => {
|
||||
const blockCopy = cloneDeep(block)
|
||||
return {
|
||||
...blockCopy,
|
||||
fields: sanitizeFields({
|
||||
config,
|
||||
fields: blockCopy.fields,
|
||||
requireFieldLevelRichTextEditor: true,
|
||||
validRelationships,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
const blocksField: BlockField = {
|
||||
name: field?.name + '_lexical_blocks',
|
||||
type: 'blocks',
|
||||
blocks: sanitizedBlocks,
|
||||
blocks: props.blocks,
|
||||
}
|
||||
// This is only done so that interfaceNameDefinitions sets those block's interfaceNames.
|
||||
// we don't actually use the JSON Schema itself in the generated types yet.
|
||||
@@ -149,19 +126,6 @@ export const BlocksFeature: FeatureProviderProviderServer<
|
||||
props.blocks.find((block) => block.slug === blockType),
|
||||
)
|
||||
|
||||
// sanitize blocks
|
||||
const validRelationships = req.payload.config.collections.map((c) => c.slug) || []
|
||||
|
||||
const sanitizedBlock = {
|
||||
...block,
|
||||
fields: sanitizeFields({
|
||||
config: req.payload.config,
|
||||
fields: block.fields,
|
||||
requireFieldLevelRichTextEditor: true,
|
||||
validRelationships,
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
await beforeChangeTraverseFields({
|
||||
id: null,
|
||||
@@ -193,18 +157,7 @@ export const BlocksFeature: FeatureProviderProviderServer<
|
||||
props.blocks.find((block) => block.slug === blockType),
|
||||
)
|
||||
|
||||
// sanitize blocks
|
||||
const validRelationships = req.payload.config.collections.map((c) => c.slug) || []
|
||||
|
||||
const sanitizedBlock = {
|
||||
...block,
|
||||
fields: sanitizeFields({
|
||||
config: req.payload.config,
|
||||
fields: block.fields,
|
||||
requireFieldLevelRichTextEditor: true,
|
||||
validRelationships,
|
||||
}),
|
||||
}
|
||||
|
||||
await beforeValidateTraverseFields({
|
||||
id: null,
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import type { Block } from 'payload/types'
|
||||
|
||||
import { sanitizeFields } from 'payload/config'
|
||||
|
||||
import type { PopulationPromise } from '../types.js'
|
||||
import type { BlocksFeatureProps } from './feature.server.js'
|
||||
import type { SerializedBlockNode } from './nodes/BlocksNode.js'
|
||||
@@ -26,21 +22,8 @@ export const blockPopulationPromiseHOC = (
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
}) => {
|
||||
const blocks: Block[] = props.blocks
|
||||
const blockFieldData = node.fields
|
||||
|
||||
// Sanitize block's fields here. This is done here and not in the feature, because the payload config is available here
|
||||
const payloadConfig = req.payload.config
|
||||
const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
|
||||
blocks.forEach((block) => {
|
||||
block.fields = sanitizeFields({
|
||||
config: payloadConfig,
|
||||
fields: block.fields,
|
||||
requireFieldLevelRichTextEditor: true,
|
||||
validRelationships,
|
||||
})
|
||||
})
|
||||
|
||||
// find block used in this node
|
||||
const block = props.blocks.find((block) => block.slug === blockFieldData.blockType)
|
||||
if (!block || !block?.fields?.length || !blockFieldData) {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import type { Block } from 'payload/types'
|
||||
|
||||
import { buildStateFromSchema } from '@payloadcms/ui/forms/buildStateFromSchema'
|
||||
import { sanitizeFields } from 'payload/config'
|
||||
|
||||
import type { NodeValidation } from '../types.js'
|
||||
import type { BlocksFeatureProps } from './feature.server.js'
|
||||
@@ -12,31 +9,11 @@ export const blockValidationHOC = (
|
||||
): NodeValidation<SerializedBlockNode> => {
|
||||
return async ({ node, validation }) => {
|
||||
const blockFieldData = node.fields
|
||||
const blocks: Block[] = props.blocks
|
||||
|
||||
const {
|
||||
options: {
|
||||
id,
|
||||
operation,
|
||||
preferences,
|
||||
req,
|
||||
req: {
|
||||
payload: { config },
|
||||
},
|
||||
},
|
||||
options: { id, operation, preferences, req },
|
||||
} = validation
|
||||
|
||||
// Sanitize block's fields here. This is done here and not in the feature, because the payload config is available here
|
||||
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||
blocks.forEach((block) => {
|
||||
block.fields = sanitizeFields({
|
||||
config,
|
||||
fields: block.fields,
|
||||
requireFieldLevelRichTextEditor: true,
|
||||
validRelationships,
|
||||
})
|
||||
})
|
||||
|
||||
// find block
|
||||
const block = props.blocks.find((block) => block.slug === blockFieldData.blockType)
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { User } from 'payload/auth'
|
||||
import type { Config } from 'payload/config'
|
||||
import type { Field, RadioField, TextField } from 'payload/types'
|
||||
import type { SanitizedConfig } from 'payload/config'
|
||||
import type { Field, FieldWithRichTextRequiredEditor, RadioField, TextField } from 'payload/types'
|
||||
|
||||
import { validateUrl } from '../../../lexical/utils/url.js'
|
||||
|
||||
export const getBaseFields = (
|
||||
config: Config,
|
||||
config: SanitizedConfig,
|
||||
enabledCollections: false | string[],
|
||||
disabledCollections: false | string[],
|
||||
): Field[] => {
|
||||
): FieldWithRichTextRequiredEditor[] => {
|
||||
let enabledRelations: string[]
|
||||
|
||||
/**
|
||||
@@ -122,5 +122,5 @@ export const getBaseFields = (
|
||||
label: ({ t }) => t('fields:openInNewTab'),
|
||||
})
|
||||
|
||||
return baseFields as Field[]
|
||||
return baseFields as FieldWithRichTextRequiredEditor[]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { SanitizedConfig } from 'payload/config'
|
||||
import type { Config, SanitizedConfig } from 'payload/config'
|
||||
import type { Field, FieldWithRichTextRequiredEditor } from 'payload/types'
|
||||
|
||||
import { traverseFields } from '@payloadcms/next/utilities'
|
||||
import { sanitizeFields } from 'payload/config'
|
||||
import { deepCopyObject } from 'payload/utilities'
|
||||
|
||||
import type { FeatureProviderProviderServer } from '../types.js'
|
||||
@@ -47,7 +47,6 @@ export type LinkFeatureServerProps = ExclusiveLinkCollectionsProps & {
|
||||
| ((args: {
|
||||
config: SanitizedConfig
|
||||
defaultFields: FieldWithRichTextRequiredEditor[]
|
||||
i18n: I18n
|
||||
}) => FieldWithRichTextRequiredEditor[])
|
||||
| FieldWithRichTextRequiredEditor[]
|
||||
}
|
||||
@@ -59,27 +58,32 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
|
||||
props = {}
|
||||
}
|
||||
return {
|
||||
feature: () => {
|
||||
feature: async ({ config: _config }) => {
|
||||
const validRelationships = _config.collections.map((c) => c.slug) || []
|
||||
|
||||
const _transformedFields = transformExtraFields(
|
||||
deepCopyObject(props.fields),
|
||||
_config,
|
||||
props.enabledCollections,
|
||||
props.disabledCollections,
|
||||
)
|
||||
|
||||
const sanitizedFields = (await sanitizeFields({
|
||||
config: _config as unknown as Config,
|
||||
fields: _transformedFields,
|
||||
requireFieldLevelRichTextEditor: true,
|
||||
validRelationships,
|
||||
})) as FieldWithRichTextRequiredEditor[]
|
||||
props.fields = sanitizedFields
|
||||
|
||||
return {
|
||||
ClientComponent: LinkFeatureClientComponent,
|
||||
clientFeatureProps: {
|
||||
disabledCollections: props.disabledCollections,
|
||||
enabledCollections: props.enabledCollections,
|
||||
} as ExclusiveLinkCollectionsProps,
|
||||
generateSchemaMap: ({ config, i18n, props }) => {
|
||||
const transformedFields = transformExtraFields(
|
||||
deepCopyObject(props.fields),
|
||||
config,
|
||||
i18n,
|
||||
props.enabledCollections,
|
||||
props.disabledCollections,
|
||||
)
|
||||
|
||||
if (
|
||||
!transformedFields ||
|
||||
!Array.isArray(transformedFields) ||
|
||||
transformedFields.length === 0
|
||||
) {
|
||||
generateSchemaMap: ({ config, i18n }) => {
|
||||
if (!sanitizedFields || !Array.isArray(sanitizedFields) || sanitizedFields.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -87,11 +91,11 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
|
||||
|
||||
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||
|
||||
schemaMap.set('fields', transformedFields)
|
||||
schemaMap.set('fields', sanitizedFields)
|
||||
|
||||
traverseFields({
|
||||
config,
|
||||
fields: transformedFields,
|
||||
fields: sanitizedFields,
|
||||
i18n,
|
||||
schemaMap,
|
||||
schemaPath: 'fields',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { SanitizedConfig } from 'payload/config'
|
||||
import type { Field, GroupField } from 'payload/types'
|
||||
import type { Field, FieldWithRichTextRequiredEditor, GroupField } from 'payload/types'
|
||||
|
||||
import { getBaseFields } from '../../drawer/baseFields.js'
|
||||
|
||||
@@ -10,18 +9,24 @@ import { getBaseFields } from '../../drawer/baseFields.js'
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
export function transformExtraFields(
|
||||
customFieldSchema:
|
||||
| ((args: { config: SanitizedConfig; defaultFields: Field[]; i18n: I18n }) => Field[])
|
||||
| Field[],
|
||||
| ((args: {
|
||||
config: SanitizedConfig
|
||||
defaultFields: FieldWithRichTextRequiredEditor[]
|
||||
}) => FieldWithRichTextRequiredEditor[])
|
||||
| FieldWithRichTextRequiredEditor[],
|
||||
config: SanitizedConfig,
|
||||
i18n: I18n,
|
||||
enabledCollections?: false | string[],
|
||||
disabledCollections?: false | string[],
|
||||
): Field[] {
|
||||
const baseFields: Field[] = getBaseFields(config, enabledCollections, disabledCollections)
|
||||
const baseFields: FieldWithRichTextRequiredEditor[] = getBaseFields(
|
||||
config,
|
||||
enabledCollections,
|
||||
disabledCollections,
|
||||
)
|
||||
|
||||
const fields =
|
||||
typeof customFieldSchema === 'function'
|
||||
? customFieldSchema({ config, defaultFields: baseFields, i18n })
|
||||
? customFieldSchema({ config, defaultFields: baseFields })
|
||||
: baseFields
|
||||
|
||||
// Wrap fields which are not part of the base schema in a group named 'fields' - otherwise they will be rendered but not saved
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { sanitizeFields } from 'payload/config'
|
||||
import { deepCopyObject } from 'payload/utilities'
|
||||
|
||||
import type { PopulationPromise } from '../types.js'
|
||||
import type { LinkFeatureServerProps } from './feature.server.js'
|
||||
import type { SerializedLinkNode } from './nodes/types.js'
|
||||
|
||||
import { recurseNestedFields } from '../../../populate/recurseNestedFields.js'
|
||||
import { transformExtraFields } from './plugins/floatingLinkEditor/utilities.js'
|
||||
|
||||
export const linkPopulationPromiseHOC = (
|
||||
props: LinkFeatureServerProps,
|
||||
@@ -25,34 +21,14 @@ export const linkPopulationPromiseHOC = (
|
||||
req,
|
||||
showHiddenFields,
|
||||
}) => {
|
||||
// Sanitize link's fields here. This is done here and not in the feature, because the payload config is available here
|
||||
const payloadConfig = req.payload.config
|
||||
const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
|
||||
|
||||
const transformedFields = transformExtraFields(
|
||||
deepCopyObject(props.fields),
|
||||
payloadConfig,
|
||||
req.i18n,
|
||||
props.enabledCollections,
|
||||
props.disabledCollections,
|
||||
)
|
||||
|
||||
// TODO: Sanitize & transform ahead of time! On startup!
|
||||
const sanitizedFields = sanitizeFields({
|
||||
config: payloadConfig,
|
||||
fields: transformedFields,
|
||||
requireFieldLevelRichTextEditor: true,
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
if (!sanitizedFields?.length) {
|
||||
if (!props.fields?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* Should populate all fields, including the doc field (for internal links), as it's treated like a normal field
|
||||
*/
|
||||
if (Array.isArray(sanitizedFields)) {
|
||||
if (Array.isArray(props.fields)) {
|
||||
recurseNestedFields({
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -62,7 +38,7 @@ export const linkPopulationPromiseHOC = (
|
||||
depth,
|
||||
editorPopulationPromises,
|
||||
fieldPromises,
|
||||
fields: sanitizedFields,
|
||||
fields: props.fields,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
overrideAccess,
|
||||
|
||||
@@ -84,14 +84,20 @@ export type FeatureProviderServer<ServerFeatureProps, ClientFeatureProps> = {
|
||||
/** Keys of soft-dependencies needed for this feature. The FeatureProviders dependencies are optional, but are considered as last-priority in the loading process */
|
||||
dependenciesSoft?: string[]
|
||||
|
||||
/**
|
||||
* This is being called during the payload sanitization process
|
||||
*/
|
||||
feature: (props: {
|
||||
config: SanitizedConfig
|
||||
/** unSanitizedEditorConfig.features, but mapped */
|
||||
featureProviderMap: ServerFeatureProviderMap
|
||||
// other resolved features, which have been loaded before this one. All features declared in 'dependencies' should be available here
|
||||
resolvedFeatures: ResolvedServerFeatureMap
|
||||
// unSanitized EditorConfig,
|
||||
unSanitizedEditorConfig: ServerEditorConfig
|
||||
}) => ServerFeature<ServerFeatureProps, ClientFeatureProps>
|
||||
}) =>
|
||||
| Promise<ServerFeature<ServerFeatureProps, ClientFeatureProps>>
|
||||
| ServerFeature<ServerFeatureProps, ClientFeatureProps>
|
||||
key: string
|
||||
/** Props which were passed into your feature will have to be passed here. This will allow them to be used / read in other places of the code, e.g. wherever you can use useEditorConfigContext */
|
||||
serverFeatureProps: ServerFeatureProps
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Config } from 'payload/config'
|
||||
import type {
|
||||
Field,
|
||||
FieldWithRichTextRequiredEditor,
|
||||
@@ -8,6 +9,7 @@ import type {
|
||||
} from 'payload/types'
|
||||
|
||||
import { traverseFields } from '@payloadcms/next/utilities'
|
||||
import { sanitizeFields } from 'payload/config'
|
||||
|
||||
import type { FeatureProviderProviderServer } from '../types.js'
|
||||
import type { UploadFeaturePropsClient } from './feature.client.js'
|
||||
@@ -51,7 +53,20 @@ export const UploadFeature: FeatureProviderProviderServer<
|
||||
}
|
||||
|
||||
return {
|
||||
feature: () => {
|
||||
feature: async ({ config: _config }) => {
|
||||
const validRelationships = _config.collections.map((c) => c.slug) || []
|
||||
|
||||
for (const collection in props.collections) {
|
||||
if (props.collections[collection].fields?.length) {
|
||||
props.collections[collection].fields = (await sanitizeFields({
|
||||
config: _config as unknown as Config,
|
||||
fields: props.collections[collection].fields,
|
||||
requireFieldLevelRichTextEditor: true,
|
||||
validRelationships,
|
||||
})) as FieldWithRichTextRequiredEditor[]
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ClientComponent: UploadFeatureClientComponent,
|
||||
clientFeatureProps: clientProps,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { sanitizeFields } from 'payload/config'
|
||||
|
||||
import type { PopulationPromise } from '../types.js'
|
||||
import type { UploadFeatureProps } from './feature.server.js'
|
||||
import type { SerializedUploadNode } from './nodes/UploadNode.js'
|
||||
@@ -25,8 +23,6 @@ export const uploadPopulationPromiseHOC = (
|
||||
req,
|
||||
showHiddenFields,
|
||||
}) => {
|
||||
const payloadConfig = req.payload.config
|
||||
|
||||
if (node?.value) {
|
||||
const collection = req.payload.collections[node?.relationTo]
|
||||
|
||||
@@ -50,17 +46,7 @@ export const uploadPopulationPromiseHOC = (
|
||||
)
|
||||
}
|
||||
if (Array.isArray(props?.collections?.[node?.relationTo]?.fields)) {
|
||||
const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
|
||||
|
||||
// TODO: Sanitize & transform ahead of time! On startup!
|
||||
const sanitizedFields = sanitizeFields({
|
||||
config: payloadConfig,
|
||||
fields: props?.collections?.[node?.relationTo]?.fields,
|
||||
requireFieldLevelRichTextEditor: true,
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
if (!sanitizedFields?.length) {
|
||||
if (!props?.collections?.[node?.relationTo]?.fields?.length) {
|
||||
return
|
||||
}
|
||||
recurseNestedFields({
|
||||
@@ -70,7 +56,7 @@ export const uploadPopulationPromiseHOC = (
|
||||
depth,
|
||||
editorPopulationPromises,
|
||||
fieldPromises,
|
||||
fields: sanitizedFields,
|
||||
fields: props?.collections?.[node?.relationTo]?.fields,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
overrideAccess,
|
||||
|
||||
@@ -23,7 +23,6 @@ import { ParagraphFeature } from '../../../features/paragraph/feature.server.js'
|
||||
import { RelationshipFeature } from '../../../features/relationship/feature.server.js'
|
||||
import { UploadFeature } from '../../../features/upload/feature.server.js'
|
||||
import { LexicalEditorTheme } from '../../theme/EditorTheme.js'
|
||||
import { sanitizeServerEditorConfig } from './sanitize.js'
|
||||
|
||||
export const defaultEditorLexicalConfig: LexicalEditorConfig = {
|
||||
namespace: 'lexical',
|
||||
@@ -56,6 +55,3 @@ export const defaultEditorConfig: ServerEditorConfig = {
|
||||
features: defaultEditorFeatures,
|
||||
lexical: defaultEditorLexicalConfig,
|
||||
}
|
||||
|
||||
export const defaultSanitizedServerEditorConfig: SanitizedServerEditorConfig =
|
||||
sanitizeServerEditorConfig(defaultEditorConfig)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Config, SanitizedConfig } from 'payload/config'
|
||||
|
||||
import type {
|
||||
FeatureProviderServer,
|
||||
ResolvedServerFeatureMap,
|
||||
@@ -103,11 +105,13 @@ export function sortFeaturesForOptimalLoading(
|
||||
return topologicallySortFeatures(featureProviders)
|
||||
}
|
||||
|
||||
export function loadFeatures({
|
||||
export async function loadFeatures({
|
||||
config,
|
||||
unSanitizedEditorConfig,
|
||||
}: {
|
||||
config: SanitizedConfig
|
||||
unSanitizedEditorConfig: ServerEditorConfig
|
||||
}): ResolvedServerFeatureMap {
|
||||
}): Promise<ResolvedServerFeatureMap> {
|
||||
// First remove all duplicate features. The LAST feature with a given key wins.
|
||||
unSanitizedEditorConfig.features = unSanitizedEditorConfig.features
|
||||
.reverse()
|
||||
@@ -167,7 +171,8 @@ export function loadFeatures({
|
||||
}
|
||||
}
|
||||
|
||||
const feature = featureProvider.feature({
|
||||
const feature = await featureProvider.feature({
|
||||
config,
|
||||
featureProviderMap,
|
||||
resolvedFeatures,
|
||||
unSanitizedEditorConfig,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Config, SanitizedConfig } from 'payload/config'
|
||||
|
||||
import type { ResolvedServerFeatureMap, SanitizedServerFeatures } from '../../../features/types.js'
|
||||
import type { SanitizedServerEditorConfig, ServerEditorConfig } from '../types.js'
|
||||
|
||||
@@ -81,10 +83,12 @@ export const sanitizeServerFeatures = (
|
||||
return sanitized
|
||||
}
|
||||
|
||||
export function sanitizeServerEditorConfig(
|
||||
export async function sanitizeServerEditorConfig(
|
||||
editorConfig: ServerEditorConfig,
|
||||
): SanitizedServerEditorConfig {
|
||||
const resolvedFeatureMap = loadFeatures({
|
||||
config: SanitizedConfig,
|
||||
): Promise<SanitizedServerEditorConfig> {
|
||||
const resolvedFeatureMap = await loadFeatures({
|
||||
config,
|
||||
unSanitizedEditorConfig: editorConfig,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
import type { RichTextAdapter } from 'payload/types'
|
||||
|
||||
import { mapFields } from '@payloadcms/ui/utilities/buildComponentMap'
|
||||
import { sanitizeFields } from 'payload/config'
|
||||
import React from 'react'
|
||||
|
||||
import type { ResolvedServerFeatureMap } from './field/features/types.js'
|
||||
import type { GeneratedFeatureProviderComponent } from './types.js'
|
||||
|
||||
import { cloneDeep } from './field/lexical/utils/cloneDeep.js'
|
||||
|
||||
export const getGenerateComponentMap =
|
||||
(args: {
|
||||
resolvedFeatureMap: ResolvedServerFeatureMap
|
||||
}): RichTextAdapter['generateComponentMap'] =>
|
||||
({ WithServerSideProps, config, i18n, schemaPath }) => {
|
||||
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||
|
||||
const componentMap = new Map()
|
||||
|
||||
// turn args.resolvedFeatureMap into an array of [key, value] pairs, ordered by value.order, lowest order first:
|
||||
@@ -78,18 +73,11 @@ export const getGenerateComponentMap =
|
||||
|
||||
if (schemas) {
|
||||
for (const [schemaKey, fields] of schemas.entries()) {
|
||||
const sanitizedFields = sanitizeFields({
|
||||
config,
|
||||
fields: cloneDeep(fields),
|
||||
requireFieldLevelRichTextEditor: true,
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
const mappedFields = mapFields({
|
||||
WithServerSideProps,
|
||||
config,
|
||||
disableAddingID: true,
|
||||
fieldSchema: sanitizedFields,
|
||||
fieldSchema: fields,
|
||||
i18n,
|
||||
parentPath: `${schemaPath}.feature.${featureKey}.fields.${schemaKey}`,
|
||||
readOnly: false,
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import type { RichTextAdapter } from 'payload/types'
|
||||
|
||||
import { sanitizeFields } from 'payload/config'
|
||||
|
||||
import type { ResolvedServerFeatureMap } from './field/features/types.js'
|
||||
|
||||
import { cloneDeep } from './field/lexical/utils/cloneDeep.js'
|
||||
|
||||
export const getGenerateSchemaMap =
|
||||
(args: { resolvedFeatureMap: ResolvedServerFeatureMap }): RichTextAdapter['generateSchemaMap'] =>
|
||||
({ config, i18n, schemaMap, schemaPath }) => {
|
||||
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||
|
||||
for (const [featureKey, resolvedFeature] of args.resolvedFeatureMap.entries()) {
|
||||
if (
|
||||
!('generateSchemaMap' in resolvedFeature) ||
|
||||
@@ -28,14 +22,7 @@ export const getGenerateSchemaMap =
|
||||
|
||||
if (schemas) {
|
||||
for (const [schemaKey, fields] of schemas.entries()) {
|
||||
const sanitizedFields = sanitizeFields({
|
||||
config,
|
||||
fields: cloneDeep(fields),
|
||||
requireFieldLevelRichTextEditor: true,
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
schemaMap.set(`${schemaPath}.feature.${featureKey}.${schemaKey}`, sanitizedFields)
|
||||
schemaMap.set(`${schemaPath}.feature.${featureKey}.${schemaKey}`, fields)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,29 +6,42 @@ import { withNullableJSONSchemaType } from 'payload/utilities'
|
||||
|
||||
import type { FeatureProviderServer, ResolvedServerFeatureMap } from './field/features/types.js'
|
||||
import type { SanitizedServerEditorConfig } from './field/lexical/config/types.js'
|
||||
import type { AdapterProps, LexicalEditorProps, LexicalRichTextAdapter } from './types.js'
|
||||
import type { AdapterProps, LexicalEditorProps, LexicalRichTextAdapterProvider } from './types.js'
|
||||
|
||||
import { RichTextCell } from './cell/index.js'
|
||||
import { RichTextField } from './field/index.js'
|
||||
import {
|
||||
defaultEditorConfig,
|
||||
defaultEditorFeatures,
|
||||
defaultSanitizedServerEditorConfig,
|
||||
} from './field/lexical/config/server/default.js'
|
||||
import { loadFeatures } from './field/lexical/config/server/loader.js'
|
||||
import { sanitizeServerFeatures } from './field/lexical/config/server/sanitize.js'
|
||||
import {
|
||||
sanitizeServerEditorConfig,
|
||||
sanitizeServerFeatures,
|
||||
} from './field/lexical/config/server/sanitize.js'
|
||||
import { cloneDeep } from './field/lexical/utils/cloneDeep.js'
|
||||
import { getGenerateComponentMap } from './generateComponentMap.js'
|
||||
import { getGenerateSchemaMap } from './generateSchemaMap.js'
|
||||
import { populateLexicalPopulationPromises } from './populate/populateLexicalPopulationPromises.js'
|
||||
import { richTextValidateHOC } from './validate/index.js'
|
||||
|
||||
export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapter {
|
||||
let defaultSanitizedServerEditorConfig: SanitizedServerEditorConfig = null
|
||||
|
||||
export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapterProvider {
|
||||
return async ({ config }) => {
|
||||
let resolvedFeatureMap: ResolvedServerFeatureMap = null
|
||||
|
||||
let finalSanitizedEditorConfig: SanitizedServerEditorConfig // For server only
|
||||
if (!props || (!props.features && !props.lexical)) {
|
||||
if (!defaultSanitizedServerEditorConfig) {
|
||||
defaultSanitizedServerEditorConfig = await sanitizeServerEditorConfig(
|
||||
defaultEditorConfig,
|
||||
config,
|
||||
)
|
||||
}
|
||||
|
||||
finalSanitizedEditorConfig = cloneDeep(defaultSanitizedServerEditorConfig)
|
||||
|
||||
resolvedFeatureMap = finalSanitizedEditorConfig.resolvedFeatureMap
|
||||
} else {
|
||||
let features: FeatureProviderServer<unknown, unknown>[] =
|
||||
@@ -41,7 +54,8 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
|
||||
const lexical: LexicalEditorConfig = props.lexical
|
||||
|
||||
resolvedFeatureMap = loadFeatures({
|
||||
resolvedFeatureMap = await loadFeatures({
|
||||
config,
|
||||
unSanitizedEditorConfig: {
|
||||
features,
|
||||
lexical: lexical ? lexical : defaultEditorConfig.lexical,
|
||||
@@ -284,6 +298,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { AlignFeature } from './field/features/align/feature.server.js'
|
||||
export { BlockQuoteFeature } from './field/features/blockquote/feature.server.js'
|
||||
@@ -435,7 +450,6 @@ export {
|
||||
defaultEditorConfig,
|
||||
defaultEditorFeatures,
|
||||
defaultEditorLexicalConfig,
|
||||
defaultSanitizedServerEditorConfig,
|
||||
} from './field/lexical/config/server/default.js'
|
||||
export {
|
||||
loadFeatures,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { EditorConfig as LexicalEditorConfig, SerializedEditorState } from 'lexical'
|
||||
import type { FieldPermissions } from 'payload/auth'
|
||||
import type { FieldTypes } from 'payload/config'
|
||||
import type { FieldTypes, SanitizedConfig } from 'payload/config'
|
||||
import type { RichTextAdapter, RichTextFieldProps } from 'payload/types'
|
||||
import type React from 'react'
|
||||
|
||||
@@ -18,11 +18,16 @@ export type LexicalEditorProps = {
|
||||
lexical?: LexicalEditorConfig
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
||||
export type LexicalRichTextAdapter = RichTextAdapter<SerializedEditorState, AdapterProps, any> & {
|
||||
editorConfig: SanitizedServerEditorConfig
|
||||
}
|
||||
|
||||
export type LexicalRichTextAdapterProvider =
|
||||
/**
|
||||
* This is being called during the payload sanitization process
|
||||
*/
|
||||
({ config }: { config: SanitizedConfig }) => Promise<LexicalRichTextAdapter>
|
||||
|
||||
export type FieldProps = RichTextFieldProps<SerializedEditorState, AdapterProps, AdapterProps> & {
|
||||
fieldTypes: FieldTypes
|
||||
indexPath: string
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { User } from 'payload/auth'
|
||||
import type { Config } from 'payload/config'
|
||||
import type { SanitizedConfig } from 'payload/config'
|
||||
import type { Field } from 'payload/types'
|
||||
|
||||
export const getBaseFields = (config: Config): Field[] => [
|
||||
export const getBaseFields = (config: SanitizedConfig): Field[] => [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { SanitizedConfig } from 'payload/config'
|
||||
import type { Config, SanitizedConfig } from 'payload/config'
|
||||
import type { Field } from 'payload/types'
|
||||
import type { Editor } from 'slate'
|
||||
|
||||
@@ -35,16 +35,15 @@ export const wrapLink = (editor: Editor): void => {
|
||||
*/
|
||||
export function transformExtraFields(
|
||||
customFieldSchema:
|
||||
| ((args: { config: SanitizedConfig; defaultFields: Field[]; i18n: I18n }) => Field[])
|
||||
| ((args: { config: SanitizedConfig; defaultFields: Field[] }) => Field[])
|
||||
| Field[],
|
||||
config: SanitizedConfig,
|
||||
i18n: I18n,
|
||||
): Field[] {
|
||||
const baseFields: Field[] = getBaseFields(config)
|
||||
|
||||
const fields =
|
||||
typeof customFieldSchema === 'function'
|
||||
? customFieldSchema({ config, defaultFields: baseFields, i18n })
|
||||
? customFieldSchema({ config, defaultFields: baseFields })
|
||||
: baseFields
|
||||
|
||||
// Wrap fields which are not part of the base schema in a group named 'fields' - otherwise they will be rendered but not saved
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import type { RichTextAdapter } from 'payload/types'
|
||||
import type { Field, RichTextAdapter } from 'payload/types'
|
||||
|
||||
import { mapFields } from '@payloadcms/ui/utilities/buildComponentMap'
|
||||
import { sanitizeFields } from 'payload/config'
|
||||
import React from 'react'
|
||||
|
||||
import type { AdapterArguments, RichTextCustomElement, RichTextCustomLeaf } from './types.js'
|
||||
|
||||
import { elements as elementTypes } from './field/elements/index.js'
|
||||
import { linkFieldsSchemaPath } from './field/elements/link/shared.js'
|
||||
import { transformExtraFields } from './field/elements/link/utilities.js'
|
||||
import { uploadFieldsSchemaPath } from './field/elements/upload/shared.js'
|
||||
import { defaultLeaves as leafTypes } from './field/leaves/index.js'
|
||||
|
||||
@@ -17,8 +15,6 @@ export const getGenerateComponentMap =
|
||||
({ WithServerSideProps, config, i18n }) => {
|
||||
const componentMap = new Map()
|
||||
|
||||
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||
|
||||
;(args?.admin?.leaves || Object.values(leafTypes)).forEach((leaf) => {
|
||||
let leafObject: RichTextCustomLeaf
|
||||
|
||||
@@ -66,16 +62,10 @@ export const getGenerateComponentMap =
|
||||
|
||||
switch (element.name) {
|
||||
case 'link': {
|
||||
const linkFields = sanitizeFields({
|
||||
config,
|
||||
fields: transformExtraFields(args.admin?.link?.fields, config, i18n),
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
const mappedFields = mapFields({
|
||||
WithServerSideProps,
|
||||
config,
|
||||
fieldSchema: linkFields,
|
||||
fieldSchema: args.admin?.link?.fields as Field[],
|
||||
i18n,
|
||||
readOnly: false,
|
||||
})
|
||||
@@ -98,16 +88,10 @@ export const getGenerateComponentMap =
|
||||
|
||||
uploadEnabledCollections.forEach((collection) => {
|
||||
if (args?.admin?.upload?.collections[collection.slug]?.fields) {
|
||||
const uploadFields = sanitizeFields({
|
||||
config,
|
||||
fields: args?.admin?.upload?.collections[collection.slug]?.fields,
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
const mappedFields = mapFields({
|
||||
WithServerSideProps,
|
||||
config,
|
||||
fieldSchema: uploadFields,
|
||||
fieldSchema: args?.admin?.upload?.collections[collection.slug]?.fields,
|
||||
i18n,
|
||||
readOnly: false,
|
||||
})
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import type { RichTextAdapter } from 'payload/types'
|
||||
|
||||
import { sanitizeFields } from 'payload/config'
|
||||
import type { Field, RichTextAdapter } from 'payload/types'
|
||||
|
||||
import type { AdapterArguments, RichTextCustomElement } from './types.js'
|
||||
|
||||
import { elements as elementTypes } from './field/elements/index.js'
|
||||
import { linkFieldsSchemaPath } from './field/elements/link/shared.js'
|
||||
import { transformExtraFields } from './field/elements/link/utilities.js'
|
||||
import { uploadFieldsSchemaPath } from './field/elements/upload/shared.js'
|
||||
|
||||
export const getGenerateSchemaMap =
|
||||
(args: AdapterArguments): RichTextAdapter['generateSchemaMap'] =>
|
||||
({ config, i18n, schemaMap, schemaPath }) => {
|
||||
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||
|
||||
({ config, schemaMap, schemaPath }) => {
|
||||
;(args?.admin?.elements || Object.values(elementTypes)).forEach((el) => {
|
||||
let element: RichTextCustomElement
|
||||
|
||||
@@ -26,13 +21,10 @@ export const getGenerateSchemaMap =
|
||||
if (element) {
|
||||
switch (element.name) {
|
||||
case 'link': {
|
||||
const linkFields = sanitizeFields({
|
||||
config,
|
||||
fields: transformExtraFields(args.admin?.link?.fields, config, i18n),
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
schemaMap.set(`${schemaPath}.${linkFieldsSchemaPath}`, linkFields)
|
||||
schemaMap.set(
|
||||
`${schemaPath}.${linkFieldsSchemaPath}`,
|
||||
args.admin?.link?.fields as Field[],
|
||||
)
|
||||
|
||||
break
|
||||
}
|
||||
@@ -50,15 +42,9 @@ export const getGenerateSchemaMap =
|
||||
|
||||
uploadEnabledCollections.forEach((collection) => {
|
||||
if (args?.admin?.upload?.collections[collection.slug]?.fields) {
|
||||
const uploadFields = sanitizeFields({
|
||||
config,
|
||||
fields: args?.admin?.upload?.collections[collection.slug]?.fields,
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
schemaMap.set(
|
||||
`${schemaPath}.${uploadFieldsSchemaPath}.${collection.slug}`,
|
||||
uploadFields,
|
||||
args?.admin?.upload?.collections[collection.slug]?.fields,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { RichTextAdapter } from 'payload/types'
|
||||
import type { Config } from 'payload/config'
|
||||
import type { RichTextAdapterProvider } from 'payload/types'
|
||||
|
||||
import { sanitizeFields } from 'payload/config'
|
||||
import { withNullableJSONSchemaType } from 'payload/utilities'
|
||||
|
||||
import type { AdapterArguments } from './types.js'
|
||||
@@ -7,11 +9,44 @@ import type { AdapterArguments } from './types.js'
|
||||
import { RichTextCell } from './cell/index.js'
|
||||
import { richTextRelationshipPromise } from './data/richTextRelationshipPromise.js'
|
||||
import { richTextValidate } from './data/validation.js'
|
||||
import { transformExtraFields } from './field/elements/link/utilities.js'
|
||||
import { RichTextField } from './field/index.js'
|
||||
import { getGenerateComponentMap } from './generateComponentMap.js'
|
||||
import { getGenerateSchemaMap } from './generateSchemaMap.js'
|
||||
|
||||
export function slateEditor(args: AdapterArguments): RichTextAdapter<any[], AdapterArguments, any> {
|
||||
export function slateEditor(
|
||||
args: AdapterArguments,
|
||||
): RichTextAdapterProvider<any[], AdapterArguments, any> {
|
||||
return async ({ config }) => {
|
||||
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||
|
||||
if (!args.admin) {
|
||||
args.admin = {}
|
||||
}
|
||||
if (!args.admin.link) {
|
||||
args.admin.link = {}
|
||||
}
|
||||
if (!args.admin.link.fields) {
|
||||
args.admin.link.fields = []
|
||||
}
|
||||
args.admin.link.fields = await sanitizeFields({
|
||||
config: config as unknown as Config,
|
||||
fields: transformExtraFields(args.admin?.link?.fields, config),
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
if (args?.admin?.upload?.collections) {
|
||||
for (const collection of Object.keys(args.admin.upload.collections)) {
|
||||
if (args?.admin?.upload?.collections[collection]?.fields) {
|
||||
args.admin.upload.collections[collection].fields = await sanitizeFields({
|
||||
config: config as unknown as Config,
|
||||
fields: args.admin?.upload?.collections[collection]?.fields,
|
||||
validRelationships,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
CellComponent: RichTextCell,
|
||||
FieldComponent: RichTextField,
|
||||
@@ -64,6 +99,7 @@ export function slateEditor(args: AdapterArguments): RichTextAdapter<any[], Adap
|
||||
validate: richTextValidate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { ElementButton } from './field/elements/Button.js'
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { SanitizedConfig } from 'payload/config'
|
||||
import type { Config, SanitizedConfig } from 'payload/config'
|
||||
import type { Field, RichTextFieldProps } from 'payload/types'
|
||||
import type { Editor } from 'slate'
|
||||
|
||||
@@ -58,9 +57,7 @@ export type AdapterArguments = {
|
||||
hideGutter?: boolean
|
||||
leaves?: RichTextLeaf[]
|
||||
link?: {
|
||||
fields?:
|
||||
| ((args: { config: SanitizedConfig; defaultFields: Field[]; i18n: I18n }) => Field[])
|
||||
| Field[]
|
||||
fields?: ((args: { config: SanitizedConfig; defaultFields: Field[] }) => Field[]) | Field[]
|
||||
}
|
||||
placeholder?: Record<string, string> | string
|
||||
rtl?: boolean
|
||||
|
||||
@@ -568,6 +568,9 @@ export const mapFields = (args: {
|
||||
style: field.admin?.style,
|
||||
width: field.admin?.width,
|
||||
}
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
const RichTextFieldComponent = field.editor.FieldComponent
|
||||
const RichTextCellComponent = field.editor.CellComponent
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
|
||||
import { mediaSlug } from '../Media/index.js'
|
||||
import { BlocksFeature, lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const postsSlug = 'posts'
|
||||
|
||||
@@ -18,6 +18,28 @@ export const PostsCollection: CollectionConfig = {
|
||||
name: 'richText',
|
||||
type: 'richText',
|
||||
},
|
||||
{
|
||||
name: 'richText2',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
BlocksFeature({
|
||||
blocks: [
|
||||
{
|
||||
slug: 'testblock',
|
||||
fields: [
|
||||
{
|
||||
name: 'testfield',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
// {
|
||||
// type: 'row',
|
||||
// fields: [],
|
||||
|
||||
@@ -189,5 +189,5 @@ export async function buildConfigWithDefaults(
|
||||
config.admin.disable = true
|
||||
}
|
||||
|
||||
return buildConfig(config)
|
||||
return await buildConfig(config)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user