perf: deduplicate blocks used in multiple places using new config.blocks property (#10905)
If you have multiple blocks that are used in multiple places, this can quickly blow up the size of your Payload Config. This will incur a performance hit, as more data is
1. sent to the client (=> bloated `ClientConfig` and large initial html) and
2. processed on the server (permissions are calculated every single time you navigate to a page - this iterates through all blocks you have defined, even if they're duplicative)
This can be optimized by defining your block **once** in your Payload Config, and just referencing the block slug whenever it's used, instead of passing the entire block config. To do this, the block can be defined in the `blocks` array of the Payload Config. The slug can then be passed to the `blockReferences` array in the Blocks Field - the `blocks` array has to be empty for compatibility reasons.
```ts
import { buildConfig } from 'payload'
import { lexicalEditor, BlocksFeature } from '@payloadcms/richtext-lexical'
// Payload Config
const config = buildConfig({
// Define the block once
blocks: [
{
slug: 'TextBlock',
fields: [
{
name: 'text',
type: 'text',
},
],
},
],
collections: [
{
slug: 'collection1',
fields: [
{
name: 'content',
type: 'blocks',
// Reference the block by slug
blockReferences: ['TextBlock'],
blocks: [], // Required to be empty, for compatibility reasons
},
],
},
{
slug: 'collection2',
fields: [
{
name: 'editor',
type: 'richText',
editor: lexicalEditor({
BlocksFeature({
// Same reference can be reused anywhere, even in the lexical editor, without incurred performance hit
blocks: ['TextBlock'],
})
})
},
],
},
],
})
```
## v4.0 Plans
In 4.0, we will remove the `blockReferences` property, and allow string block references to be passed directly to the blocks `property`. Essentially, we'd remove the `blocks` property and rename `blockReferences` to `blocks`.
The reason we opted to a new property in this PR is to avoid breaking changes. Allowing strings to be passed to the `blocks` property will prevent plugins that iterate through fields / blocks from compiling.
## PR Changes
- Testing: This PR introduces a plugin that automatically converts blocks to block references. This is done in the fields__blocks test suite, to run our existing test suite using block references.
- Block References support: Most changes are similar. Everywhere we iterate through blocks, we have to now do the following:
1. Check if `field.blockReferences` is provided. If so, only iterate through that.
2. Check if the block is an object (= actual block), or string
3. If it's a string, pull the actual block from the Payload Config or from `payload.blocks`.
The exception is config sanitization and block type generations. This PR optimizes them so that each block is only handled once, instead of every time the block is referenced.
## Benchmarks
60 Block fields, each block field having the same 600 Blocks.
### Before:
**Initial HTML:** 195 kB
**Generated types:** takes 11 minutes, 461,209 lines
https://github.com/user-attachments/assets/11d49a4e-5414-4579-8050-e6346e552f56
### After:
**Initial HTML:** 73.6 kB
**Generated types:** takes 2 seconds, 35,810 lines
https://github.com/user-attachments/assets/3eab1a99-6c29-489d-add5-698df67780a3
### After Permissions Optimization (follow-up PR)
Initial HTML: 73.6 kB
https://github.com/user-attachments/assets/a909202e-45a8-4bf6-9a38-8c85813f1312
## Future Plans
1. This PR does not yet deduplicate block references during permissions calculation. We'll optimize that in a separate PR, as this one is already large enough
2. The same optimization can be done to deduplicate fields. One common use-case would be link field groups that may be referenced in multiple entities, outside of blocks. We might explore adding a new `fieldReferences` property, that allows you to reference those same `config.blocks`.
This commit is contained in:
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -283,6 +283,7 @@ jobs:
|
||||
- fields-relationship
|
||||
- fields__collections__Array
|
||||
- fields__collections__Blocks
|
||||
- fields__collections__Blocks#config.blockreferences.ts
|
||||
- fields__collections__Checkbox
|
||||
- fields__collections__Collapsible
|
||||
- fields__collections__ConditionalLogic
|
||||
@@ -293,6 +294,7 @@ jobs:
|
||||
- fields__collections__JSON
|
||||
- fields__collections__Lexical__e2e__main
|
||||
- fields__collections__Lexical__e2e__blocks
|
||||
- fields__collections__Lexical__e2e__blocks#config.blockreferences.ts
|
||||
- fields__collections__Number
|
||||
- fields__collections__Point
|
||||
- fields__collections__Radio
|
||||
|
||||
@@ -295,6 +295,70 @@ export const CustomBlocksFieldLabelClient: BlocksFieldLabelClientComponent = ({
|
||||
}
|
||||
```
|
||||
|
||||
## Block References
|
||||
|
||||
If you have multiple blocks used in multiple places, your Payload Config can grow in size, potentially sending more data to the client and requiring more processing on the server. However, you can optimize performance by defining each block **once** in your Payload Config and then referencing its slug wherever it's used instead of passing the entire block config.
|
||||
|
||||
To do this, define the block in the `blocks` array of the Payload Config. Then, in the Blocks Field, pass the block slug to the `blockReferences` array - leaving the `blocks` array empty for compatibility reasons.
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
import { lexicalEditor, BlocksFeature } from '@payloadcms/richtext-lexical'
|
||||
|
||||
// Payload Config
|
||||
const config = buildConfig({
|
||||
// Define the block once
|
||||
blocks: [
|
||||
{
|
||||
slug: 'TextBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
collections: [
|
||||
{
|
||||
slug: 'collection1',
|
||||
fields: [
|
||||
{
|
||||
name: 'content',
|
||||
type: 'blocks',
|
||||
// Reference the block by slug
|
||||
blockReferences: ['TextBlock'],
|
||||
blocks: [], // Required to be empty, for compatibility reasons
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'collection2',
|
||||
fields: [
|
||||
{
|
||||
name: 'editor',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
BlocksFeature({
|
||||
// Same reference can be reused anywhere, even in the lexical editor, without incurred performance hit
|
||||
blocks: ['TextBlock'],
|
||||
})
|
||||
})
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
**Reminder:**
|
||||
Blocks referenced in the `blockReferences` array are treated as isolated from the collection / global config. This has the following implications:
|
||||
|
||||
1. The block config cannot be modified or extended in the collection config. It will be identical everywhere it's referenced.
|
||||
2. Access control for blocks referenced in the `blockReferences` are run only once - data from the collection will not be available in the block's access control.
|
||||
</Banner>
|
||||
|
||||
## TypeScript
|
||||
|
||||
As you build your own Block configs, you might want to store them in separate files but retain typing accordingly. To do so, you can import and use Payload's `Block` type:
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { IndexOptions, Schema, SchemaOptions, SchemaTypeOptions } from 'mon
|
||||
import mongoose from 'mongoose'
|
||||
import {
|
||||
type ArrayField,
|
||||
type Block,
|
||||
type BlocksField,
|
||||
type CheckboxField,
|
||||
type CodeField,
|
||||
@@ -193,11 +192,12 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, fieldSchema, payload.config.localization),
|
||||
})
|
||||
|
||||
field.blocks.forEach((blockItem: Block) => {
|
||||
;(field.blockReferences ?? field.blocks).forEach((blockItem) => {
|
||||
const blockSchema = new mongoose.Schema({}, { _id: false, id: false })
|
||||
|
||||
blockItem.fields.forEach((blockField) => {
|
||||
const block = typeof blockItem === 'string' ? payload.blocks[blockItem] : blockItem
|
||||
|
||||
block.fields.forEach((blockField) => {
|
||||
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[blockField.type]
|
||||
if (addFieldSchema) {
|
||||
addFieldSchema(blockField, blockSchema, payload, buildSchemaOptions)
|
||||
@@ -207,11 +207,11 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
if (field.localized && payload.config.localization) {
|
||||
payload.config.localization.localeCodes.forEach((localeCode) => {
|
||||
// @ts-expect-error Possible incorrect typing in mongoose types, this works
|
||||
schema.path(`${field.name}.${localeCode}`).discriminator(blockItem.slug, blockSchema)
|
||||
schema.path(`${field.name}.${localeCode}`).discriminator(block.slug, blockSchema)
|
||||
})
|
||||
} else {
|
||||
// @ts-expect-error Possible incorrect typing in mongoose types, this works
|
||||
schema.path(field.name).discriminator(blockItem.slug, blockSchema)
|
||||
schema.path(field.name).discriminator(block.slug, blockSchema)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -80,6 +80,10 @@ const hasRelationshipOrUploadField = ({ fields }: { fields: Field[] }): boolean
|
||||
|
||||
if ('blocks' in field) {
|
||||
for (const block of field.blocks) {
|
||||
if (typeof block === 'string') {
|
||||
// Skip - string blocks have been added in v3 and thus don't need to be migrated
|
||||
continue
|
||||
}
|
||||
if (hasRelationshipOrUploadField({ fields: block.fields })) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -65,18 +65,24 @@ export const getLocalizedSortProperty = ({
|
||||
}
|
||||
|
||||
if (matchedField.type === 'blocks') {
|
||||
nextFields = matchedField.blocks.reduce((flattenedBlockFields, block) => {
|
||||
return [
|
||||
...flattenedBlockFields,
|
||||
...block.flattenedFields.filter(
|
||||
(blockField) =>
|
||||
(fieldAffectsData(blockField) &&
|
||||
blockField.name !== 'blockType' &&
|
||||
blockField.name !== 'blockName') ||
|
||||
!fieldAffectsData(blockField),
|
||||
),
|
||||
]
|
||||
}, [])
|
||||
nextFields = (matchedField.blockReferences ?? matchedField.blocks).reduce(
|
||||
(flattenedBlockFields, _block) => {
|
||||
// TODO: iterate over blocks mapped to block slug in v4, or pass through payload.blocks
|
||||
const block =
|
||||
typeof _block === 'string' ? config.blocks.find((b) => b.slug === _block) : _block
|
||||
return [
|
||||
...flattenedBlockFields,
|
||||
...block.flattenedFields.filter(
|
||||
(blockField) =>
|
||||
(fieldAffectsData(blockField) &&
|
||||
blockField.name !== 'blockType' &&
|
||||
blockField.name !== 'blockName') ||
|
||||
!fieldAffectsData(blockField),
|
||||
),
|
||||
]
|
||||
},
|
||||
[],
|
||||
)
|
||||
}
|
||||
|
||||
const result = incomingResult ? `${incomingResult}.${localizedSegment}` : localizedSegment
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import type { FlattenedBlock, FlattenedField, Payload, RelationshipField } from 'payload'
|
||||
import type {
|
||||
FlattenedBlock,
|
||||
FlattenedBlocksField,
|
||||
FlattenedField,
|
||||
Payload,
|
||||
RelationshipField,
|
||||
} from 'payload'
|
||||
|
||||
import { Types } from 'mongoose'
|
||||
import { createArrayFromCommaDelineated } from 'payload'
|
||||
@@ -40,14 +46,18 @@ const buildExistsQuery = (formattedValue, path, treatEmptyString = true) => {
|
||||
// returns nestedField Field object from blocks.nestedField path because getLocalizedPaths splits them only for relationships
|
||||
const getFieldFromSegments = ({
|
||||
field,
|
||||
payload,
|
||||
segments,
|
||||
}: {
|
||||
field: FlattenedBlock | FlattenedField
|
||||
payload: Payload
|
||||
segments: string[]
|
||||
}) => {
|
||||
if ('blocks' in field) {
|
||||
for (const block of field.blocks) {
|
||||
const field = getFieldFromSegments({ field: block, segments })
|
||||
if ('blocks' in field || 'blockReferences' in field) {
|
||||
const _field: FlattenedBlocksField = field as FlattenedBlocksField
|
||||
for (const _block of _field.blockReferences ?? _field.blocks) {
|
||||
const block: FlattenedBlock = typeof _block === 'string' ? payload.blocks[_block] : _block
|
||||
const field = getFieldFromSegments({ field: block, payload, segments })
|
||||
if (field) {
|
||||
return field
|
||||
}
|
||||
@@ -67,7 +77,7 @@ const getFieldFromSegments = ({
|
||||
}
|
||||
|
||||
segments.shift()
|
||||
return getFieldFromSegments({ field: foundField, segments })
|
||||
return getFieldFromSegments({ field: foundField, payload, segments })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,7 +101,7 @@ export const sanitizeQueryValue = ({
|
||||
if (['array', 'blocks', 'group', 'tab'].includes(field.type) && path.includes('.')) {
|
||||
const segments = path.split('.')
|
||||
segments.shift()
|
||||
const foundField = getFieldFromSegments({ field, segments })
|
||||
const foundField = getFieldFromSegments({ field, payload, segments })
|
||||
|
||||
if (foundField) {
|
||||
field = foundField
|
||||
|
||||
@@ -123,7 +123,8 @@ const traverseFields = ({
|
||||
case 'blocks': {
|
||||
const blocksSelect = select[field.name] as SelectType
|
||||
|
||||
for (const block of field.blocks) {
|
||||
for (const _block of field.blockReferences ?? field.blocks) {
|
||||
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block
|
||||
if (
|
||||
(selectMode === 'include' && blocksSelect[block.slug] === true) ||
|
||||
(selectMode === 'exclude' && typeof blocksSelect[block.slug] === 'undefined')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { CollectionConfig, Field, SanitizedConfig, TraverseFieldsCallback } from 'payload'
|
||||
|
||||
import { Types } from 'mongoose'
|
||||
import { APIError, traverseFields } from 'payload'
|
||||
import { traverseFields } from 'payload'
|
||||
import { fieldAffectsData } from 'payload/shared'
|
||||
|
||||
type Args = {
|
||||
@@ -150,7 +150,7 @@ export const sanitizeRelationshipIDs = ({
|
||||
}
|
||||
}
|
||||
|
||||
traverseFields({ callback: sanitize, fields, fillEmpty: false, ref: data })
|
||||
traverseFields({ callback: sanitize, config, fields, fillEmpty: false, ref: data })
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -185,7 +185,8 @@ export const traverseFields = ({
|
||||
}
|
||||
}
|
||||
|
||||
field.blocks.forEach((block) => {
|
||||
;(field.blockReferences ?? field.blocks).forEach((_block) => {
|
||||
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block
|
||||
const blockKey = `_blocks_${block.slug}`
|
||||
|
||||
let blockSelect: boolean | SelectType | undefined
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FlattenedField } from 'payload'
|
||||
import type { FlattenedBlock, FlattenedField } from 'payload'
|
||||
|
||||
type Args = {
|
||||
doc: Record<string, unknown>
|
||||
@@ -51,7 +51,10 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => {
|
||||
Object.entries(rowData).forEach(([locale, localeRows]) => {
|
||||
if (Array.isArray(localeRows)) {
|
||||
localeRows.forEach((row, i) => {
|
||||
const matchedBlock = field.blocks.find((block) => block.slug === row.blockType)
|
||||
// Can ignore string blocks, as those were added in v3 and don't need to be migrated
|
||||
const matchedBlock = field.blocks.find(
|
||||
(block) => typeof block !== 'string' && block.slug === row.blockType,
|
||||
) as FlattenedBlock | undefined
|
||||
|
||||
if (matchedBlock) {
|
||||
return traverseFields({
|
||||
@@ -69,7 +72,10 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => {
|
||||
|
||||
if (Array.isArray(rowData)) {
|
||||
rowData.forEach((row, i) => {
|
||||
const matchedBlock = field.blocks.find((block) => block.slug === row.blockType)
|
||||
// Can ignore string blocks, as those were added in v3 and don't need to be migrated
|
||||
const matchedBlock = field.blocks.find(
|
||||
(block) => typeof block !== 'string' && block.slug === row.blockType,
|
||||
) as FlattenedBlock | undefined
|
||||
|
||||
if (matchedBlock) {
|
||||
return traverseFields({
|
||||
|
||||
@@ -42,6 +42,11 @@ export const traverseFields = (args: Args) => {
|
||||
|
||||
case 'blocks': {
|
||||
return field.blocks.forEach((block) => {
|
||||
// Can ignore string blocks, as those were added in v3 and don't need to be migrated
|
||||
if (typeof block === 'string') {
|
||||
return
|
||||
}
|
||||
|
||||
const newTableName = args.adapter.tableNameMap.get(
|
||||
`${args.rootTableName}_blocks_${toSnakeCase(block.slug)}`,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SQL } from 'drizzle-orm'
|
||||
import type { SQLiteTableWithColumns } from 'drizzle-orm/sqlite-core'
|
||||
import type { FlattenedField, NumberField, TextField } from 'payload'
|
||||
import type { FlattenedBlock, FlattenedField, NumberField, TextField } from 'payload'
|
||||
|
||||
import { and, eq, like, sql } from 'drizzle-orm'
|
||||
import { type PgTableWithColumns } from 'drizzle-orm/pg-core'
|
||||
@@ -176,7 +176,12 @@ export const getTableColumnFromPath = ({
|
||||
// find the block config using the value
|
||||
const blockTypes = Array.isArray(value) ? value : [value]
|
||||
blockTypes.forEach((blockType) => {
|
||||
const block = field.blocks.find((block) => block.slug === blockType)
|
||||
const block =
|
||||
adapter.payload.blocks[blockType] ??
|
||||
((field.blockReferences ?? field.blocks).find(
|
||||
(block) => typeof block !== 'string' && block.slug === blockType,
|
||||
) as FlattenedBlock | undefined)
|
||||
|
||||
newTableName = adapter.tableNameMap.get(
|
||||
`${tableName}_blocks_${toSnakeCase(block.slug)}`,
|
||||
)
|
||||
@@ -201,11 +206,13 @@ export const getTableColumnFromPath = ({
|
||||
}
|
||||
}
|
||||
|
||||
const hasBlockField = field.blocks.some((block) => {
|
||||
const hasBlockField = (field.blockReferences ?? field.blocks).some((_block) => {
|
||||
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block
|
||||
|
||||
newTableName = adapter.tableNameMap.get(`${tableName}_blocks_${toSnakeCase(block.slug)}`)
|
||||
constraintPath = `${constraintPath}${field.name}.%.`
|
||||
|
||||
let result
|
||||
let result: TableColumn
|
||||
const blockConstraints = []
|
||||
const blockSelectFields = {}
|
||||
try {
|
||||
|
||||
@@ -343,7 +343,9 @@ export const traverseFields = ({
|
||||
case 'blocks': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
|
||||
field.blocks.forEach((block) => {
|
||||
;(field.blockReferences ?? field.blocks).forEach((_block) => {
|
||||
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block
|
||||
|
||||
const blockTableName = createTableName({
|
||||
adapter,
|
||||
config: block,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FlattenedField, JoinQuery, SanitizedConfig } from 'payload'
|
||||
import type { FlattenedBlock, FlattenedField, JoinQuery, SanitizedConfig } from 'payload'
|
||||
|
||||
import { fieldAffectsData, fieldIsVirtual } from 'payload/shared'
|
||||
import { fieldIsVirtual } from 'payload/shared'
|
||||
|
||||
import type { DrizzleAdapter } from '../../types.js'
|
||||
import type { BlocksMap } from '../../utilities/createBlocksMap.js'
|
||||
@@ -216,7 +216,11 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
|
||||
Object.entries(result[field.name]).forEach(([locale, localizedBlocks]) => {
|
||||
result[field.name][locale] = localizedBlocks.map((row) => {
|
||||
const block = field.blocks.find(({ slug }) => slug === row.blockType)
|
||||
const block =
|
||||
adapter.payload.blocks[row.blockType] ??
|
||||
((field.blockReferences ?? field.blocks).find(
|
||||
(block) => typeof block !== 'string' && block.slug === row.blockType,
|
||||
) as FlattenedBlock | undefined)
|
||||
|
||||
if (block) {
|
||||
const blockResult = traverseFields<T>({
|
||||
@@ -265,7 +269,16 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
row.id = row._uuid
|
||||
delete row._uuid
|
||||
}
|
||||
const block = field.blocks.find(({ slug }) => slug === row.blockType)
|
||||
|
||||
if (typeof row.blockType !== 'string') {
|
||||
return acc
|
||||
}
|
||||
|
||||
const block =
|
||||
adapter.payload.blocks[row.blockType] ??
|
||||
((field.blockReferences ?? field.blocks).find(
|
||||
(block) => typeof block !== 'string' && block.slug === row.blockType,
|
||||
) as FlattenedBlock | undefined)
|
||||
|
||||
if (block) {
|
||||
if (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FlattenedBlocksField } from 'payload'
|
||||
import type { FlattenedBlock, FlattenedBlocksField } from 'payload'
|
||||
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
@@ -51,7 +51,13 @@ export const transformBlocks = ({
|
||||
if (typeof blockRow.blockType !== 'string') {
|
||||
return
|
||||
}
|
||||
const matchedBlock = field.blocks.find(({ slug }) => slug === blockRow.blockType)
|
||||
|
||||
const matchedBlock =
|
||||
adapter.payload.blocks[blockRow.blockType] ??
|
||||
((field.blockReferences ?? field.blocks).find(
|
||||
(block) => typeof block !== 'string' && block.slug === blockRow.blockType,
|
||||
) as FlattenedBlock | undefined)
|
||||
|
||||
if (!matchedBlock) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -168,8 +168,8 @@ export const traverseFields = ({
|
||||
}
|
||||
|
||||
if (field.type === 'blocks') {
|
||||
field.blocks.forEach(({ slug }) => {
|
||||
blocksToDelete.add(toSnakeCase(slug))
|
||||
;(field.blockReferences ?? field.blocks).forEach((block) => {
|
||||
blocksToDelete.add(toSnakeCase(typeof block === 'string' ? block : block.slug))
|
||||
})
|
||||
|
||||
if (field.localized) {
|
||||
|
||||
@@ -108,8 +108,15 @@ export function buildObjectType({
|
||||
}
|
||||
},
|
||||
blocks: (objectTypeConfig: ObjectTypeConfig, field: BlocksField) => {
|
||||
const blockTypes: GraphQLObjectType<any, any>[] = field.blocks.reduce((acc, block) => {
|
||||
if (!graphqlResult.types.blockTypes[block.slug]) {
|
||||
const blockTypes: GraphQLObjectType<any, any>[] = (
|
||||
field.blockReferences ?? field.blocks
|
||||
).reduce((acc, _block) => {
|
||||
const blockSlug = typeof _block === 'string' ? _block : _block.slug
|
||||
if (!graphqlResult.types.blockTypes[blockSlug]) {
|
||||
// TODO: iterate over blocks mapped to block slug in v4, or pass through payload.blocks
|
||||
const block =
|
||||
typeof _block === 'string' ? config.blocks.find((b) => b.slug === _block) : _block
|
||||
|
||||
const interfaceName =
|
||||
block?.interfaceName || block?.graphQL?.singularName || toWords(block.slug, true)
|
||||
|
||||
@@ -133,8 +140,8 @@ export function buildObjectType({
|
||||
}
|
||||
}
|
||||
|
||||
if (graphqlResult.types.blockTypes[block.slug]) {
|
||||
acc.push(graphqlResult.types.blockTypes[block.slug])
|
||||
if (graphqlResult.types.blockTypes[blockSlug]) {
|
||||
acc.push(graphqlResult.types.blockTypes[blockSlug])
|
||||
}
|
||||
|
||||
return acc
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { ClientField, LivePreviewConfig } from 'payload'
|
||||
|
||||
import { DndContext } from '@dnd-kit/core'
|
||||
import { useConfig } from '@payloadcms/ui'
|
||||
import { fieldSchemaToJSON } from 'payload/shared'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
@@ -43,6 +44,7 @@ export const LivePreviewProvider: React.FC<LivePreviewProviderProps> = ({
|
||||
const iframeRef = React.useRef<HTMLIFrameElement>(null)
|
||||
|
||||
const [iframeHasLoaded, setIframeHasLoaded] = useState(false)
|
||||
const { config } = useConfig()
|
||||
|
||||
const [zoom, setZoom] = useState(1)
|
||||
|
||||
@@ -59,7 +61,7 @@ export const LivePreviewProvider: React.FC<LivePreviewProviderProps> = ({
|
||||
React.useState<LivePreviewConfig['breakpoints'][0]['name']>('responsive')
|
||||
|
||||
const [fieldSchemaJSON] = useState(() => {
|
||||
return fieldSchemaToJSON(fieldSchema)
|
||||
return fieldSchemaToJSON(fieldSchema, config)
|
||||
})
|
||||
|
||||
// The toolbar needs to freely drag and drop around the page
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
import type { ClientField } from 'payload'
|
||||
|
||||
import { ChevronIcon, Pill, useTranslation } from '@payloadcms/ui'
|
||||
import { ChevronIcon, Pill, useConfig, useTranslation } from '@payloadcms/ui'
|
||||
import { fieldIsArrayType, fieldIsBlockType } from 'payload/shared'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
@@ -49,6 +50,7 @@ export const DiffCollapser: React.FC<Props> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isCollapsed, setIsCollapsed] = useState(initCollapsed)
|
||||
const { config } = useConfig()
|
||||
|
||||
let changeCount = 0
|
||||
|
||||
@@ -69,6 +71,7 @@ export const DiffCollapser: React.FC<Props> = ({
|
||||
|
||||
changeCount = countChangedFieldsInRows({
|
||||
comparisonRows,
|
||||
config,
|
||||
field,
|
||||
locales,
|
||||
versionRows,
|
||||
@@ -76,6 +79,7 @@ export const DiffCollapser: React.FC<Props> = ({
|
||||
} else {
|
||||
changeCount = countChangedFields({
|
||||
comparison,
|
||||
config,
|
||||
fields,
|
||||
locales,
|
||||
version,
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
FieldDiffClientProps,
|
||||
FieldDiffServerProps,
|
||||
FieldTypes,
|
||||
FlattenedBlock,
|
||||
PayloadComponent,
|
||||
PayloadRequest,
|
||||
SanitizedFieldPermissions,
|
||||
@@ -332,14 +333,27 @@ const buildVersionField = ({
|
||||
for (let i = 0; i < blocksValue.length; i++) {
|
||||
const comparisonRow = comparisonValue?.[i] || {}
|
||||
const versionRow = blocksValue[i] || {}
|
||||
const versionBlock = field.blocks.find((block) => block.slug === versionRow.blockType)
|
||||
|
||||
const blockSlugToMatch: string = versionRow.blockType
|
||||
const versionBlock =
|
||||
req.payload.blocks[blockSlugToMatch] ??
|
||||
((field.blockReferences ?? field.blocks).find(
|
||||
(block) => typeof block !== 'string' && block.slug === blockSlugToMatch,
|
||||
) as FlattenedBlock | undefined)
|
||||
|
||||
let fields = []
|
||||
|
||||
if (versionRow.blockType === comparisonRow.blockType) {
|
||||
fields = versionBlock.fields
|
||||
} else {
|
||||
const comparisonBlock = field.blocks.find((block) => block.slug === comparisonRow.blockType)
|
||||
const comparisonBlockSlugToMatch: string = versionRow.blockType
|
||||
|
||||
const comparisonBlock =
|
||||
req.payload.blocks[comparisonBlockSlugToMatch] ??
|
||||
((field.blockReferences ?? field.blocks).find(
|
||||
(block) => typeof block !== 'string' && block.slug === comparisonBlockSlugToMatch,
|
||||
) as FlattenedBlock | undefined)
|
||||
|
||||
if (comparisonBlock) {
|
||||
fields = getUniqueListBy<Field>(
|
||||
[...versionBlock.fields, ...comparisonBlock.fields],
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import type { FieldDiffClientProps } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { useTranslation } from '@payloadcms/ui'
|
||||
import { useConfig, useTranslation } from '@payloadcms/ui'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
@@ -26,6 +26,7 @@ export const Iterable: React.FC<FieldDiffClientProps> = ({
|
||||
}) => {
|
||||
const { i18n } = useTranslation()
|
||||
const { selectedLocales } = useSelectedLocales()
|
||||
const { config } = useConfig()
|
||||
|
||||
const versionRowCount = Array.isArray(versionValue) ? versionValue.length : 0
|
||||
const comparisonRowCount = Array.isArray(comparisonValue) ? comparisonValue.length : 0
|
||||
@@ -63,6 +64,7 @@ export const Iterable: React.FC<FieldDiffClientProps> = ({
|
||||
const { fields, versionFields } = getFieldsForRowComparison({
|
||||
baseVersionField,
|
||||
comparisonRow,
|
||||
config,
|
||||
field,
|
||||
row: i,
|
||||
versionRow,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { ArrayFieldClient, BlocksFieldClient, ClientField } from 'payload'
|
||||
import type { ArrayFieldClient, BlocksFieldClient, ClientConfig, ClientField } from 'payload'
|
||||
|
||||
import { fieldHasChanges } from './fieldHasChanges.js'
|
||||
import { getFieldsForRowComparison } from './getFieldsForRowComparison.js'
|
||||
|
||||
type Args = {
|
||||
comparison: unknown
|
||||
config: ClientConfig
|
||||
fields: ClientField[]
|
||||
locales: string[] | undefined
|
||||
version: unknown
|
||||
@@ -14,7 +15,7 @@ type Args = {
|
||||
* Recursively counts the number of changed fields between comparison and
|
||||
* version data for a given set of fields.
|
||||
*/
|
||||
export function countChangedFields({ comparison, fields, locales, version }: Args) {
|
||||
export function countChangedFields({ comparison, config, fields, locales, version }: Args) {
|
||||
let count = 0
|
||||
|
||||
fields.forEach((field) => {
|
||||
@@ -32,12 +33,18 @@ export function countChangedFields({ comparison, fields, locales, version }: Arg
|
||||
locales.forEach((locale) => {
|
||||
const comparisonRows = comparison?.[field.name]?.[locale] ?? []
|
||||
const versionRows = version?.[field.name]?.[locale] ?? []
|
||||
count += countChangedFieldsInRows({ comparisonRows, field, locales, versionRows })
|
||||
count += countChangedFieldsInRows({
|
||||
comparisonRows,
|
||||
config,
|
||||
field,
|
||||
locales,
|
||||
versionRows,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
const comparisonRows = comparison?.[field.name] ?? []
|
||||
const versionRows = version?.[field.name] ?? []
|
||||
count += countChangedFieldsInRows({ comparisonRows, field, locales, versionRows })
|
||||
count += countChangedFieldsInRows({ comparisonRows, config, field, locales, versionRows })
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -77,6 +84,7 @@ export function countChangedFields({ comparison, fields, locales, version }: Arg
|
||||
case 'row': {
|
||||
count += countChangedFields({
|
||||
comparison,
|
||||
config,
|
||||
fields: field.fields,
|
||||
locales,
|
||||
version,
|
||||
@@ -91,6 +99,7 @@ export function countChangedFields({ comparison, fields, locales, version }: Arg
|
||||
locales.forEach((locale) => {
|
||||
count += countChangedFields({
|
||||
comparison: comparison?.[field.name]?.[locale],
|
||||
config,
|
||||
fields: field.fields,
|
||||
locales,
|
||||
version: version?.[field.name]?.[locale],
|
||||
@@ -99,6 +108,7 @@ export function countChangedFields({ comparison, fields, locales, version }: Arg
|
||||
} else {
|
||||
count += countChangedFields({
|
||||
comparison: comparison?.[field.name],
|
||||
config,
|
||||
fields: field.fields,
|
||||
locales,
|
||||
version: version?.[field.name],
|
||||
@@ -116,6 +126,7 @@ export function countChangedFields({ comparison, fields, locales, version }: Arg
|
||||
locales.forEach((locale) => {
|
||||
count += countChangedFields({
|
||||
comparison: comparison?.[tab.name]?.[locale],
|
||||
config,
|
||||
fields: tab.fields,
|
||||
locales,
|
||||
version: version?.[tab.name]?.[locale],
|
||||
@@ -125,6 +136,7 @@ export function countChangedFields({ comparison, fields, locales, version }: Arg
|
||||
// Named tab
|
||||
count += countChangedFields({
|
||||
comparison: comparison?.[tab.name],
|
||||
config,
|
||||
fields: tab.fields,
|
||||
locales,
|
||||
version: version?.[tab.name],
|
||||
@@ -133,6 +145,7 @@ export function countChangedFields({ comparison, fields, locales, version }: Arg
|
||||
// Unnamed tab
|
||||
count += countChangedFields({
|
||||
comparison,
|
||||
config,
|
||||
fields: tab.fields,
|
||||
locales,
|
||||
version,
|
||||
@@ -160,6 +173,7 @@ export function countChangedFields({ comparison, fields, locales, version }: Arg
|
||||
|
||||
type countChangedFieldsInRowsArgs = {
|
||||
comparisonRows: unknown[]
|
||||
config: ClientConfig
|
||||
field: ArrayFieldClient | BlocksFieldClient
|
||||
locales: string[] | undefined
|
||||
versionRows: unknown[]
|
||||
@@ -167,6 +181,7 @@ type countChangedFieldsInRowsArgs = {
|
||||
|
||||
export function countChangedFieldsInRows({
|
||||
comparisonRows = [],
|
||||
config,
|
||||
field,
|
||||
locales,
|
||||
versionRows = [],
|
||||
@@ -181,6 +196,7 @@ export function countChangedFieldsInRows({
|
||||
const { fields: rowFields } = getFieldsForRowComparison({
|
||||
baseVersionField: { type: 'text', fields: [], path: '', schemaPath: '' }, // Doesn't matter, as we don't need the versionFields output here
|
||||
comparisonRow,
|
||||
config,
|
||||
field,
|
||||
row: i,
|
||||
versionRow,
|
||||
@@ -188,6 +204,7 @@ export function countChangedFieldsInRows({
|
||||
|
||||
count += countChangedFields({
|
||||
comparison: comparisonRow,
|
||||
config,
|
||||
fields: rowFields,
|
||||
locales,
|
||||
version: versionRow,
|
||||
|
||||
@@ -2,6 +2,8 @@ import type {
|
||||
ArrayFieldClient,
|
||||
BaseVersionField,
|
||||
BlocksFieldClient,
|
||||
ClientBlock,
|
||||
ClientConfig,
|
||||
ClientField,
|
||||
VersionField,
|
||||
} from 'payload'
|
||||
@@ -17,12 +19,14 @@ import { getUniqueListBy } from 'payload/shared'
|
||||
export function getFieldsForRowComparison({
|
||||
baseVersionField,
|
||||
comparisonRow,
|
||||
config,
|
||||
field,
|
||||
row,
|
||||
versionRow,
|
||||
}: {
|
||||
baseVersionField: BaseVersionField
|
||||
comparisonRow: any
|
||||
config: ClientConfig
|
||||
field: ArrayFieldClient | BlocksFieldClient
|
||||
row: number
|
||||
versionRow: any
|
||||
@@ -37,24 +41,37 @@ export function getFieldsForRowComparison({
|
||||
: baseVersionField.fields
|
||||
} else if (field.type === 'blocks') {
|
||||
if (versionRow?.blockType === comparisonRow?.blockType) {
|
||||
const matchedBlock = ('blocks' in field &&
|
||||
field.blocks?.find((block) => block.slug === versionRow?.blockType)) || {
|
||||
fields: [],
|
||||
}
|
||||
const matchedBlock: ClientBlock =
|
||||
config?.blocksMap?.[versionRow?.blockType] ??
|
||||
(((('blocks' in field || 'blockReferences' in field) &&
|
||||
(field.blockReferences ?? field.blocks)?.find(
|
||||
(block) => typeof block !== 'string' && block.slug === versionRow?.blockType,
|
||||
)) || {
|
||||
fields: [],
|
||||
}) as ClientBlock)
|
||||
|
||||
fields = matchedBlock.fields
|
||||
versionFields = baseVersionField.rows?.length
|
||||
? baseVersionField.rows[row]
|
||||
: baseVersionField.fields
|
||||
} else {
|
||||
const matchedVersionBlock = ('blocks' in field &&
|
||||
field.blocks?.find((block) => block.slug === versionRow?.blockType)) || {
|
||||
fields: [],
|
||||
}
|
||||
const matchedComparisonBlock = ('blocks' in field &&
|
||||
field.blocks?.find((block) => block.slug === comparisonRow?.blockType)) || {
|
||||
fields: [],
|
||||
}
|
||||
const matchedVersionBlock =
|
||||
config?.blocksMap?.[versionRow?.blockType] ??
|
||||
(((('blocks' in field || 'blockReferences' in field) &&
|
||||
(field.blockReferences ?? field.blocks)?.find(
|
||||
(block) => typeof block !== 'string' && block.slug === versionRow?.blockType,
|
||||
)) || {
|
||||
fields: [],
|
||||
}) as ClientBlock)
|
||||
|
||||
const matchedComparisonBlock =
|
||||
config?.blocksMap?.[comparisonRow?.blockType] ??
|
||||
(((('blocks' in field || 'blockReferences' in field) &&
|
||||
(field.blockReferences ?? field.blocks)?.find(
|
||||
(block) => typeof block !== 'string' && block.slug === comparisonRow?.blockType,
|
||||
)) || {
|
||||
fields: [],
|
||||
}) as ClientBlock)
|
||||
|
||||
fields = getUniqueListBy<ClientField>(
|
||||
[...matchedVersionBlock.fields, ...matchedComparisonBlock.fields],
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// @ts-strict-ignore
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
||||
|
||||
import type { AdminViewConfig } from '../../admin/views/types.js'
|
||||
import type { SanitizedConfig } from '../../config/types.js'
|
||||
import type { AddToImportMap, Imports, InternalImportMap } from './index.js'
|
||||
|
||||
import { iterateCollections } from './iterateCollections.js'
|
||||
import { genImportMapIterateFields } from './iterateFields.js'
|
||||
import { iterateGlobals } from './iterateGlobals.js'
|
||||
|
||||
export function iterateConfig({
|
||||
@@ -38,6 +39,20 @@ export function iterateConfig({
|
||||
imports,
|
||||
})
|
||||
|
||||
if (config?.blocks) {
|
||||
const blocks = Object.values(config.blocks)
|
||||
if (blocks?.length) {
|
||||
genImportMapIterateFields({
|
||||
addToImportMap,
|
||||
baseDir,
|
||||
config,
|
||||
fields: blocks,
|
||||
importMap,
|
||||
imports,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof config.admin?.avatar === 'object') {
|
||||
addToImportMap(config.admin?.avatar?.Component)
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export function genImportMapIterateFields({
|
||||
addToImportMap,
|
||||
baseDir,
|
||||
config,
|
||||
fields: field.blocks,
|
||||
fields: field.blocks.filter((block) => typeof block !== 'string'),
|
||||
importMap,
|
||||
imports,
|
||||
})
|
||||
|
||||
@@ -27,6 +27,7 @@ export const sanitizeCollection = async (
|
||||
* so that you can sanitize them together, after the config has been sanitized.
|
||||
*/
|
||||
richTextSanitizationPromises?: Array<(config: SanitizedConfig) => Promise<void>>,
|
||||
_validRelationships?: string[],
|
||||
): Promise<SanitizedCollectionConfig> => {
|
||||
// /////////////////////////////////
|
||||
// Make copy of collection config
|
||||
@@ -38,13 +39,8 @@ export const sanitizeCollection = async (
|
||||
// Sanitize fields
|
||||
// /////////////////////////////////
|
||||
|
||||
const validRelationships = (config.collections || []).reduce(
|
||||
(acc, c) => {
|
||||
acc.push(c.slug)
|
||||
return acc
|
||||
},
|
||||
[collection.slug],
|
||||
)
|
||||
const validRelationships = _validRelationships ?? config.collections.map((c) => c.slug) ?? []
|
||||
|
||||
const joins: SanitizedJoins = {}
|
||||
sanitized.fields = await sanitizeFields({
|
||||
collectionConfig: sanitized,
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { DeepPartial } from 'ts-essentials'
|
||||
|
||||
import type { ImportMap } from '../bin/generateImportMap/index.js'
|
||||
import type { ClientBlock } from '../fields/config/types.js'
|
||||
import type { BlockSlug } from '../index.js'
|
||||
import type {
|
||||
LivePreviewConfig,
|
||||
SanitizedConfig,
|
||||
@@ -13,8 +15,8 @@ import {
|
||||
type ClientCollectionConfig,
|
||||
createClientCollectionConfigs,
|
||||
} from '../collections/config/client.js'
|
||||
import { createClientBlocks } from '../fields/config/client.js'
|
||||
import { type ClientGlobalConfig, createClientGlobalConfigs } from '../globals/config/client.js'
|
||||
|
||||
export type ServerOnlyRootProperties = keyof Pick<
|
||||
SanitizedConfig,
|
||||
| 'bin'
|
||||
@@ -39,10 +41,22 @@ export type ServerOnlyRootProperties = keyof Pick<
|
||||
|
||||
export type ServerOnlyRootAdminProperties = keyof Pick<SanitizedConfig['admin'], 'components'>
|
||||
|
||||
export type UnsanitizedClientConfig = {
|
||||
admin: {
|
||||
livePreview?: Omit<LivePreviewConfig, ServerOnlyLivePreviewProperties>
|
||||
} & Omit<SanitizedConfig['admin'], 'components' | 'dependencies' | 'livePreview'>
|
||||
blocks: ClientBlock[]
|
||||
collections: ClientCollectionConfig[]
|
||||
custom?: Record<string, any>
|
||||
globals: ClientGlobalConfig[]
|
||||
} & Omit<SanitizedConfig, 'admin' | 'collections' | 'globals' | 'i18n' | ServerOnlyRootProperties>
|
||||
|
||||
export type ClientConfig = {
|
||||
admin: {
|
||||
livePreview?: Omit<LivePreviewConfig, ServerOnlyLivePreviewProperties>
|
||||
} & Omit<SanitizedConfig['admin'], 'components' | 'dependencies' | 'livePreview'>
|
||||
blocks: ClientBlock[]
|
||||
blocksMap: Record<BlockSlug, ClientBlock>
|
||||
collections: ClientCollectionConfig[]
|
||||
custom?: Record<string, any>
|
||||
globals: ClientGlobalConfig[]
|
||||
@@ -109,6 +123,16 @@ export const createClientConfig = ({
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'blocks': {
|
||||
;(clientConfig.blocks as ClientBlock[]) = createClientBlocks({
|
||||
blocks: config.blocks,
|
||||
defaultIDType: config.db.defaultIDType,
|
||||
i18n,
|
||||
importMap,
|
||||
}).filter((block) => typeof block !== 'string') as ClientBlock[]
|
||||
|
||||
break
|
||||
}
|
||||
case 'collections':
|
||||
;(clientConfig.collections as ClientCollectionConfig[]) = createClientCollectionConfigs({
|
||||
collections: config.collections,
|
||||
@@ -125,6 +149,7 @@ export const createClientConfig = ({
|
||||
importMap,
|
||||
})
|
||||
break
|
||||
|
||||
case 'localization':
|
||||
if (typeof config.localization === 'object' && config.localization) {
|
||||
clientConfig.localization = {}
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { AcceptedLanguages } from '@payloadcms/translations'
|
||||
import { en } from '@payloadcms/translations/languages/en'
|
||||
import { deepMergeSimple } from '@payloadcms/translations/utilities'
|
||||
|
||||
import type { CollectionSlug, GlobalSlug } from '../index.js'
|
||||
import type {
|
||||
Config,
|
||||
LocalizationConfigWithLabels,
|
||||
@@ -20,9 +19,17 @@ import { migrationsCollection } from '../database/migrations/migrationsCollectio
|
||||
import { DuplicateCollection, InvalidConfiguration } from '../errors/index.js'
|
||||
import { defaultTimezones } from '../fields/baseFields/timezone/defaultTimezones.js'
|
||||
import { sanitizeGlobal } from '../globals/config/sanitize.js'
|
||||
import {
|
||||
baseBlockFields,
|
||||
type CollectionSlug,
|
||||
formatLabels,
|
||||
type GlobalSlug,
|
||||
sanitizeFields,
|
||||
} from '../index.js'
|
||||
import { getLockedDocumentsCollection } from '../lockedDocuments/lockedDocumentsCollection.js'
|
||||
import getPreferencesCollection from '../preferences/preferencesCollection.js'
|
||||
import { getDefaultJobsCollection } from '../queues/config/jobsCollection.js'
|
||||
import { flattenBlock } from '../utilities/flattenAllFields.js'
|
||||
import { getSchedulePublishTask } from '../versions/schedule/job.js'
|
||||
import { defaults } from './defaults.js'
|
||||
|
||||
@@ -217,6 +224,47 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
|
||||
|
||||
const collectionSlugs = new Set<CollectionSlug>()
|
||||
|
||||
const validRelationships = [
|
||||
...(config.collections.map((c) => c.slug) ?? []),
|
||||
'payload-jobs',
|
||||
'payload-locked-documents',
|
||||
'payload-preferences',
|
||||
]
|
||||
|
||||
/**
|
||||
* Blocks sanitization needs to happen before collections, as collection/global join field sanitization needs config.blocks
|
||||
* to be populated with the sanitized blocks
|
||||
*/
|
||||
config.blocks = []
|
||||
if (incomingConfig.blocks?.length) {
|
||||
for (const block of incomingConfig.blocks) {
|
||||
const sanitizedBlock = block
|
||||
|
||||
if (sanitizedBlock._sanitized === true) {
|
||||
continue
|
||||
}
|
||||
sanitizedBlock._sanitized = true
|
||||
|
||||
sanitizedBlock.fields = sanitizedBlock.fields.concat(baseBlockFields)
|
||||
|
||||
sanitizedBlock.labels = !sanitizedBlock.labels
|
||||
? formatLabels(sanitizedBlock.slug)
|
||||
: sanitizedBlock.labels
|
||||
sanitizedBlock.fields = await sanitizeFields({
|
||||
config: config as unknown as Config,
|
||||
existingFieldNames: new Set(),
|
||||
fields: sanitizedBlock.fields,
|
||||
parentIsLocalized: false,
|
||||
richTextSanitizationPromises,
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
const flattenedSanitizedBlock = flattenBlock({ block })
|
||||
|
||||
config.blocks.push(flattenedSanitizedBlock)
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < config.collections.length; i++) {
|
||||
if (collectionSlugs.has(config.collections[i].slug)) {
|
||||
throw new DuplicateCollection('slug', config.collections[i].slug)
|
||||
@@ -234,6 +282,7 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
|
||||
config as unknown as Config,
|
||||
config.collections[i],
|
||||
richTextSanitizationPromises,
|
||||
validRelationships,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -249,6 +298,7 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
|
||||
config as unknown as Config,
|
||||
config.globals[i],
|
||||
richTextSanitizationPromises,
|
||||
validRelationships,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -285,6 +335,7 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
|
||||
config as unknown as Config,
|
||||
defaultJobsCollection,
|
||||
richTextSanitizationPromises,
|
||||
validRelationships,
|
||||
)
|
||||
|
||||
configWithDefaults.collections.push(sanitizedJobsCollection)
|
||||
@@ -295,6 +346,7 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
|
||||
config as unknown as Config,
|
||||
getLockedDocumentsCollection(config as unknown as Config),
|
||||
richTextSanitizationPromises,
|
||||
validRelationships,
|
||||
),
|
||||
)
|
||||
configWithDefaults.collections.push(
|
||||
@@ -302,6 +354,7 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
|
||||
config as unknown as Config,
|
||||
getPreferencesCollection(config as unknown as Config),
|
||||
richTextSanitizationPromises,
|
||||
validRelationships,
|
||||
),
|
||||
)
|
||||
configWithDefaults.collections.push(
|
||||
@@ -309,6 +362,7 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
|
||||
config as unknown as Config,
|
||||
migrationsCollection,
|
||||
richTextSanitizationPromises,
|
||||
validRelationships,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import type { JSONSchema4 } from 'json-schema'
|
||||
import type { DestinationStream, Level, pino } from 'pino'
|
||||
import type React from 'react'
|
||||
import type { default as sharp } from 'sharp'
|
||||
import type { DeepRequired } from 'ts-essentials'
|
||||
import type { DeepRequired, MarkOptional } from 'ts-essentials'
|
||||
|
||||
import type { RichTextAdapterProvider } from '../admin/RichText.js'
|
||||
import type { DocumentTabConfig, RichTextAdapter } from '../admin/types.js'
|
||||
@@ -42,7 +42,14 @@ import type { DatabaseAdapterResult } from '../database/types.js'
|
||||
import type { EmailAdapter, SendEmailOptions } from '../email/types.js'
|
||||
import type { ErrorName } from '../errors/types.js'
|
||||
import type { GlobalConfig, Globals, SanitizedGlobalConfig } from '../globals/config/types.js'
|
||||
import type { JobsConfig, Payload, RequestContext, TypedUser } from '../index.js'
|
||||
import type {
|
||||
Block,
|
||||
FlattenedBlock,
|
||||
JobsConfig,
|
||||
Payload,
|
||||
RequestContext,
|
||||
TypedUser,
|
||||
} from '../index.js'
|
||||
import type { PayloadRequest, Where } from '../types/index.js'
|
||||
import type { PayloadLogger } from '../utilities/logger.js'
|
||||
|
||||
@@ -916,6 +923,7 @@ export type Config = {
|
||||
}
|
||||
/** Custom Payload bin scripts can be injected via the config. */
|
||||
bin?: BinScriptConfig[]
|
||||
blocks?: Block[]
|
||||
/**
|
||||
* Manage the datamodel of your application
|
||||
*
|
||||
@@ -943,12 +951,12 @@ export type Config = {
|
||||
cookiePrefix?: string
|
||||
/** Either a whitelist array of URLS to allow CORS requests from, or a wildcard string ('*') to accept incoming requests from any domain. */
|
||||
cors?: '*' | CORSConfig | string[]
|
||||
|
||||
/** A whitelist array of URLs to allow Payload cookies to be accepted from as a form of CSRF protection. */
|
||||
csrf?: string[]
|
||||
|
||||
/** Extension point to add your custom data. Server only. */
|
||||
custom?: Record<string, any>
|
||||
|
||||
/** Pass in a database adapter for use on this project. */
|
||||
db: DatabaseAdapterResult
|
||||
/** Enable to expose more detailed error information. */
|
||||
@@ -1183,6 +1191,7 @@ export type SanitizedConfig = {
|
||||
admin: {
|
||||
timezones: SanitizedTimezoneConfig
|
||||
} & DeepRequired<Config['admin']>
|
||||
blocks?: FlattenedBlock[]
|
||||
collections: SanitizedCollectionConfig[]
|
||||
/** Default richtext editor to use for richText fields */
|
||||
editor?: RichTextAdapter<any, any, any>
|
||||
@@ -1207,7 +1216,15 @@ export type SanitizedConfig = {
|
||||
// E.g. in packages/ui/src/graphics/Account/index.tsx in getComponent, if avatar.Component is casted to what it's supposed to be,
|
||||
// the result type is different
|
||||
DeepRequired<Config>,
|
||||
'admin' | 'collections' | 'editor' | 'endpoint' | 'globals' | 'i18n' | 'localization' | 'upload'
|
||||
| 'admin'
|
||||
| 'blocks'
|
||||
| 'collections'
|
||||
| 'editor'
|
||||
| 'endpoint'
|
||||
| 'globals'
|
||||
| 'i18n'
|
||||
| 'localization'
|
||||
| 'upload'
|
||||
>
|
||||
|
||||
export type EditConfig = EditConfigWithoutRoot | EditConfigWithRoot
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @ts-strict-ignore
|
||||
import type { Field, FlattenedField } from '../fields/config/types.js'
|
||||
import type { Field, FlattenedBlock, FlattenedField } from '../fields/config/types.js'
|
||||
import type { Payload } from '../index.js'
|
||||
import type { PathToQuery } from './queryValidation/types.js'
|
||||
|
||||
@@ -55,7 +55,15 @@ export function getLocalizedPaths({
|
||||
type: 'text',
|
||||
}
|
||||
} else {
|
||||
for (const block of lastIncompletePath.field.blocks) {
|
||||
for (const _block of lastIncompletePath.field.blockReferences ??
|
||||
lastIncompletePath.field.blocks) {
|
||||
let block: FlattenedBlock
|
||||
if (typeof _block === 'string') {
|
||||
block = payload?.blocks[_block]
|
||||
} else {
|
||||
block = _block
|
||||
}
|
||||
|
||||
matchedField = block.flattenedFields.find((field) => field.name === segment)
|
||||
if (matchedField) {
|
||||
break
|
||||
|
||||
@@ -146,7 +146,10 @@ export async function validateSearchParam({
|
||||
if (fieldAccess[segment]) {
|
||||
if ('fields' in fieldAccess[segment]) {
|
||||
fieldAccess = fieldAccess[segment].fields
|
||||
} else if ('blocks' in fieldAccess[segment]) {
|
||||
} else if (
|
||||
'blocks' in fieldAccess[segment] ||
|
||||
'blockReferences' in fieldAccess[segment]
|
||||
) {
|
||||
fieldAccess = fieldAccess[segment]
|
||||
} else {
|
||||
fieldAccess = fieldAccess[segment]
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { I18nClient } from '@payloadcms/translations'
|
||||
import type {
|
||||
AdminClient,
|
||||
ArrayFieldClient,
|
||||
Block,
|
||||
BlockJSX,
|
||||
BlocksFieldClient,
|
||||
ClientBlock,
|
||||
@@ -25,7 +26,6 @@ import { getFromImportMap } from '../../bin/generateImportMap/getFromImportMap.j
|
||||
import { MissingEditorProp } from '../../errors/MissingEditorProp.js'
|
||||
import { fieldAffectsData } from '../../fields/config/types.js'
|
||||
import { flattenTopLevelFields, type ImportMap } from '../../index.js'
|
||||
import { removeUndefined } from '../../utilities/removeUndefined.js'
|
||||
|
||||
// Should not be used - ClientField should be used instead. This is why we don't export ClientField, we don't want people
|
||||
// to accidentally use it instead of ClientField and get confused
|
||||
@@ -75,6 +75,83 @@ type FieldWithDescription = {
|
||||
admin: AdminClient
|
||||
} & ClientField
|
||||
|
||||
export const createClientBlocks = ({
|
||||
blocks,
|
||||
defaultIDType,
|
||||
i18n,
|
||||
importMap,
|
||||
}: {
|
||||
blocks: (Block | string)[]
|
||||
defaultIDType: Payload['config']['db']['defaultIDType']
|
||||
i18n: I18nClient
|
||||
importMap: ImportMap
|
||||
}): (ClientBlock | string)[] | ClientBlock[] => {
|
||||
const clientBlocks: (ClientBlock | string)[] = []
|
||||
for (let i = 0; i < blocks.length; i++) {
|
||||
const block = blocks[i]
|
||||
|
||||
if (typeof block === 'string') {
|
||||
// Do not process blocks that are just strings - they are processed once in the client config
|
||||
clientBlocks.push(block)
|
||||
continue
|
||||
}
|
||||
|
||||
const clientBlock: ClientBlock = {
|
||||
slug: block.slug,
|
||||
fields: [],
|
||||
}
|
||||
if (block.imageAltText) {
|
||||
clientBlock.imageAltText = block.imageAltText
|
||||
}
|
||||
if (block.imageURL) {
|
||||
clientBlock.imageURL = block.imageURL
|
||||
}
|
||||
|
||||
if (block.admin?.custom) {
|
||||
clientBlock.admin = {
|
||||
custom: block.admin.custom,
|
||||
}
|
||||
}
|
||||
|
||||
if (block?.admin?.jsx) {
|
||||
const jsxResolved = getFromImportMap<BlockJSX>({
|
||||
importMap,
|
||||
PayloadComponent: block.admin.jsx,
|
||||
schemaPath: '',
|
||||
})
|
||||
clientBlock.jsx = jsxResolved
|
||||
}
|
||||
|
||||
if (block.labels) {
|
||||
clientBlock.labels = {} as unknown as LabelsClient
|
||||
|
||||
if (block.labels.singular) {
|
||||
if (typeof block.labels.singular === 'function') {
|
||||
clientBlock.labels.singular = block.labels.singular({ t: i18n.t })
|
||||
} else {
|
||||
clientBlock.labels.singular = block.labels.singular
|
||||
}
|
||||
if (typeof block.labels.plural === 'function') {
|
||||
clientBlock.labels.plural = block.labels.plural({ t: i18n.t })
|
||||
} else {
|
||||
clientBlock.labels.plural = block.labels.plural
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clientBlock.fields = createClientFields({
|
||||
defaultIDType,
|
||||
fields: block.fields,
|
||||
i18n,
|
||||
importMap,
|
||||
})
|
||||
|
||||
clientBlocks.push(clientBlock)
|
||||
}
|
||||
|
||||
return clientBlocks
|
||||
}
|
||||
|
||||
export const createClientField = ({
|
||||
defaultIDType,
|
||||
field: incomingField,
|
||||
@@ -209,63 +286,22 @@ export const createClientField = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (incomingField.blockReferences?.length) {
|
||||
field.blockReferences = createClientBlocks({
|
||||
blocks: incomingField.blockReferences,
|
||||
defaultIDType,
|
||||
i18n,
|
||||
importMap,
|
||||
})
|
||||
}
|
||||
|
||||
if (incomingField.blocks?.length) {
|
||||
for (let i = 0; i < incomingField.blocks.length; i++) {
|
||||
const block = incomingField.blocks[i]
|
||||
|
||||
// prevent $undefined from being passed through the rsc requests
|
||||
const clientBlock = removeUndefined<ClientBlock>({
|
||||
slug: block.slug,
|
||||
fields: field.blocks?.[i]?.fields || [],
|
||||
imageAltText: block.imageAltText,
|
||||
imageURL: block.imageURL,
|
||||
}) satisfies ClientBlock
|
||||
|
||||
if (block.admin?.custom) {
|
||||
clientBlock.admin = {
|
||||
custom: block.admin.custom,
|
||||
}
|
||||
}
|
||||
|
||||
if (block?.admin?.jsx) {
|
||||
const jsxResolved = getFromImportMap<BlockJSX>({
|
||||
importMap,
|
||||
PayloadComponent: block.admin.jsx,
|
||||
schemaPath: '',
|
||||
})
|
||||
clientBlock.jsx = jsxResolved
|
||||
}
|
||||
|
||||
if (block.labels) {
|
||||
clientBlock.labels = {} as unknown as LabelsClient
|
||||
|
||||
if (block.labels.singular) {
|
||||
if (typeof block.labels.singular === 'function') {
|
||||
clientBlock.labels.singular = block.labels.singular({ t: i18n.t })
|
||||
} else {
|
||||
clientBlock.labels.singular = block.labels.singular
|
||||
}
|
||||
if (typeof block.labels.plural === 'function') {
|
||||
clientBlock.labels.plural = block.labels.plural({ t: i18n.t })
|
||||
} else {
|
||||
clientBlock.labels.plural = block.labels.plural
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clientBlock.fields = createClientFields({
|
||||
defaultIDType,
|
||||
fields: block.fields,
|
||||
i18n,
|
||||
importMap,
|
||||
})
|
||||
|
||||
if (!field.blocks) {
|
||||
field.blocks = []
|
||||
}
|
||||
|
||||
field.blocks[i] = clientBlock
|
||||
}
|
||||
field.blocks = createClientBlocks({
|
||||
blocks: incomingField.blocks,
|
||||
defaultIDType,
|
||||
i18n,
|
||||
importMap,
|
||||
}) as ClientBlock[]
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
@@ -224,7 +224,14 @@ export const sanitizeFields = async ({
|
||||
}
|
||||
|
||||
if (field.type === 'blocks' && field.blocks) {
|
||||
for (const block of field.blocks) {
|
||||
if (field.blockReferences && field.blocks?.length) {
|
||||
throw new Error('You cannot have both blockReferences and blocks in the same blocks field')
|
||||
}
|
||||
|
||||
for (const block of field.blockReferences ?? field.blocks) {
|
||||
if (typeof block === 'string') {
|
||||
continue
|
||||
}
|
||||
if (block._sanitized === true) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-strict-ignore
|
||||
import type { SanitizedJoin, SanitizedJoins } from '../../collections/config/types.js'
|
||||
import type { Config } from '../../config/types.js'
|
||||
import type { Config, SanitizedConfig } from '../../config/types.js'
|
||||
import type { FlattenedJoinField, JoinField, RelationshipField, UploadField } from './types.js'
|
||||
|
||||
import { APIError } from '../../errors/index.js'
|
||||
@@ -91,6 +91,7 @@ export const sanitizeJoinField = ({
|
||||
return
|
||||
}
|
||||
},
|
||||
config: config as unknown as SanitizedConfig,
|
||||
fields: joinCollection.fields,
|
||||
})
|
||||
|
||||
|
||||
@@ -121,6 +121,7 @@ import type { SanitizedGlobalConfig } from '../../globals/config/types.js'
|
||||
import type {
|
||||
ArrayFieldValidation,
|
||||
BlocksFieldValidation,
|
||||
BlockSlug,
|
||||
CheckboxFieldValidation,
|
||||
CodeFieldValidation,
|
||||
CollectionSlug,
|
||||
@@ -1405,6 +1406,12 @@ export type BlocksField = {
|
||||
*/
|
||||
isSortable?: boolean
|
||||
} & Admin
|
||||
/**
|
||||
* Like `blocks`, but allows you to also pass strings that are slugs of blocks defined in `config.blocks`.
|
||||
*
|
||||
* @todo `blockReferences` will be merged with `blocks` in 4.0
|
||||
*/
|
||||
blockReferences?: (Block | BlockSlug)[]
|
||||
blocks: Block[]
|
||||
defaultValue?: DefaultValue
|
||||
labels?: Labels
|
||||
@@ -1416,6 +1423,12 @@ export type BlocksField = {
|
||||
|
||||
export type BlocksFieldClient = {
|
||||
admin?: AdminClient & Pick<BlocksField['admin'], 'initCollapsed' | 'isSortable'>
|
||||
/**
|
||||
* Like `blocks`, but allows you to also pass strings that are slugs of blocks defined in `config.blocks`.
|
||||
*
|
||||
* @todo `blockReferences` will be merged with `blocks` in 4.0
|
||||
*/
|
||||
blockReferences?: (ClientBlock | string)[]
|
||||
blocks: ClientBlock[]
|
||||
labels?: LabelsClient
|
||||
} & FieldBaseClient &
|
||||
@@ -1511,8 +1524,14 @@ export type FlattenedBlock = {
|
||||
} & Block
|
||||
|
||||
export type FlattenedBlocksField = {
|
||||
/**
|
||||
* Like `blocks`, but allows you to also pass strings that are slugs of blocks defined in `config.blocks`.
|
||||
*
|
||||
* @todo `blockReferences` will be merged with `blocks` in 4.0
|
||||
*/
|
||||
blockReferences?: (FlattenedBlock | string)[]
|
||||
blocks: FlattenedBlock[]
|
||||
} & BlocksField
|
||||
} & Omit<BlocksField, 'blockReferences' | 'blocks'>
|
||||
|
||||
export type FlattenedGroupField = {
|
||||
flattenedFields: FlattenedField[]
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { SanitizedCollectionConfig } from '../../../collections/config/type
|
||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||
import type { RequestContext } from '../../../index.js'
|
||||
import type { JsonObject, PayloadRequest } from '../../../types/index.js'
|
||||
import type { Field, TabAsField } from '../../config/types.js'
|
||||
import type { Block, Field, TabAsField } from '../../config/types.js'
|
||||
|
||||
import { MissingEditorProp } from '../../../errors/index.js'
|
||||
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
||||
@@ -146,9 +146,13 @@ export const promise = async ({
|
||||
const promises = []
|
||||
|
||||
rows.forEach((row, rowIndex) => {
|
||||
const block = field.blocks.find(
|
||||
(blockType) => blockType.slug === (row as JsonObject).blockType,
|
||||
)
|
||||
const blockTypeToMatch = (row as JsonObject).blockType
|
||||
|
||||
const block: Block | undefined =
|
||||
req.payload.blocks[blockTypeToMatch] ??
|
||||
((field.blockReferences ?? field.blocks).find(
|
||||
(curBlock) => typeof curBlock !== 'string' && curBlock.slug === blockTypeToMatch,
|
||||
) as Block | undefined)
|
||||
|
||||
if (block) {
|
||||
promises.push(
|
||||
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
SelectMode,
|
||||
SelectType,
|
||||
} from '../../../types/index.js'
|
||||
import type { Field, TabAsField } from '../../config/types.js'
|
||||
import type { Block, Field, TabAsField } from '../../config/types.js'
|
||||
|
||||
import { MissingEditorProp } from '../../../errors/index.js'
|
||||
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
||||
@@ -453,9 +453,13 @@ export const promise = async ({
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
rows.forEach((row, rowIndex) => {
|
||||
const block = field.blocks.find(
|
||||
(blockType) => blockType.slug === (row as JsonObject).blockType,
|
||||
)
|
||||
const blockTypeToMatch = (row as JsonObject).blockType
|
||||
|
||||
const block: Block | undefined =
|
||||
req.payload.blocks[blockTypeToMatch] ??
|
||||
((field.blockReferences ?? field.blocks).find(
|
||||
(curBlock) => typeof curBlock !== 'string' && curBlock.slug === blockTypeToMatch,
|
||||
) as Block | undefined)
|
||||
|
||||
let blockSelectMode = selectMode
|
||||
|
||||
@@ -525,9 +529,13 @@ export const promise = async ({
|
||||
Object.values(rows).forEach((localeRows) => {
|
||||
if (Array.isArray(localeRows)) {
|
||||
localeRows.forEach((row, rowIndex) => {
|
||||
const block = field.blocks.find(
|
||||
(blockType) => blockType.slug === (row as JsonObject).blockType,
|
||||
)
|
||||
const blockTypeToMatch = row.blockType
|
||||
|
||||
const block: Block | undefined =
|
||||
req.payload.blocks[blockTypeToMatch] ??
|
||||
((field.blockReferences ?? field.blocks).find(
|
||||
(curBlock) => typeof curBlock !== 'string' && curBlock.slug === blockTypeToMatch,
|
||||
) as Block | undefined)
|
||||
|
||||
if (block) {
|
||||
traverseFields({
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { ValidationFieldError } from '../../../errors/index.js'
|
||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||
import type { RequestContext } from '../../../index.js'
|
||||
import type { JsonObject, Operation, PayloadRequest } from '../../../types/index.js'
|
||||
import type { Field, TabAsField, Validate } from '../../config/types.js'
|
||||
import type { Block, Field, TabAsField, Validate } from '../../config/types.js'
|
||||
|
||||
import { MissingEditorProp } from '../../../errors/index.js'
|
||||
import { deepMergeWithSourceArrays } from '../../../utilities/deepMerge.js'
|
||||
@@ -278,7 +278,12 @@ export const promise = async ({
|
||||
)
|
||||
|
||||
const blockTypeToMatch = (row as JsonObject).blockType || rowSiblingDoc.blockType
|
||||
const block = field.blocks.find((blockType) => blockType.slug === blockTypeToMatch)
|
||||
|
||||
const block: Block | undefined =
|
||||
req.payload.blocks[blockTypeToMatch] ??
|
||||
((field.blockReferences ?? field.blocks).find(
|
||||
(curBlock) => typeof curBlock !== 'string' && curBlock.slug === blockTypeToMatch,
|
||||
) as Block | undefined)
|
||||
|
||||
if (block) {
|
||||
promises.push(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
||||
import type { RequestContext } from '../../../index.js'
|
||||
import type { JsonObject, PayloadRequest } from '../../../types/index.js'
|
||||
import type { Field, FieldHookArgs, TabAsField } from '../../config/types.js'
|
||||
import type { Block, Field, FieldHookArgs, TabAsField } from '../../config/types.js'
|
||||
|
||||
import { fieldAffectsData } from '../../config/types.js'
|
||||
import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js'
|
||||
@@ -183,9 +183,12 @@ export const promise = async <T>({
|
||||
rows.forEach((row, rowIndex) => {
|
||||
const blockTypeToMatch = row.blockType
|
||||
|
||||
const block = field.blocks.find(
|
||||
(blockType) => blockType.slug === blockTypeToMatch,
|
||||
)
|
||||
const block: Block | undefined =
|
||||
req.payload.blocks[blockTypeToMatch] ??
|
||||
((field.blockReferences ?? field.blocks).find(
|
||||
(curBlock) =>
|
||||
typeof curBlock !== 'string' && curBlock.slug === blockTypeToMatch,
|
||||
) as Block | undefined)
|
||||
|
||||
promises.push(
|
||||
traverseFields({
|
||||
@@ -278,7 +281,12 @@ export const promise = async <T>({
|
||||
|
||||
rows.forEach((row, rowIndex) => {
|
||||
const blockTypeToMatch = row.blockType
|
||||
const block = field.blocks.find((blockType) => blockType.slug === blockTypeToMatch)
|
||||
|
||||
const block: Block | undefined =
|
||||
req.payload.blocks[blockTypeToMatch] ??
|
||||
((field.blockReferences ?? field.blocks).find(
|
||||
(curBlock) => typeof curBlock !== 'string' && curBlock.slug === blockTypeToMatch,
|
||||
) as Block | undefined)
|
||||
|
||||
if (block) {
|
||||
;(row as JsonObject).blockType = blockTypeToMatch
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { SanitizedCollectionConfig, TypeWithID } from '../../../collections
|
||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||
import type { RequestContext } from '../../../index.js'
|
||||
import type { JsonObject, JsonValue, PayloadRequest } from '../../../types/index.js'
|
||||
import type { Field, TabAsField } from '../../config/types.js'
|
||||
import type { Block, Field, TabAsField } from '../../config/types.js'
|
||||
|
||||
import { MissingEditorProp } from '../../../errors/index.js'
|
||||
import { fieldAffectsData, tabHasName, valueIsValueWithRelation } from '../../config/types.js'
|
||||
@@ -378,7 +378,12 @@ export const promise = async <T>({
|
||||
rows.forEach((row, rowIndex) => {
|
||||
const rowSiblingDoc = getExistingRowDoc(row as JsonObject, siblingDoc[field.name])
|
||||
const blockTypeToMatch = (row as JsonObject).blockType || rowSiblingDoc.blockType
|
||||
const block = field.blocks.find((blockType) => blockType.slug === blockTypeToMatch)
|
||||
|
||||
const block: Block | undefined =
|
||||
req.payload.blocks[blockTypeToMatch] ??
|
||||
((field.blockReferences ?? field.blocks).find(
|
||||
(curBlock) => typeof curBlock !== 'string' && curBlock.slug === blockTypeToMatch,
|
||||
) as Block | undefined)
|
||||
|
||||
if (block) {
|
||||
;(row as JsonObject).blockType = blockTypeToMatch
|
||||
|
||||
@@ -20,6 +20,7 @@ export const sanitizeGlobal = async (
|
||||
* so that you can sanitize them together, after the config has been sanitized.
|
||||
*/
|
||||
richTextSanitizationPromises?: Array<(config: SanitizedConfig) => Promise<void>>,
|
||||
_validRelationships?: string[],
|
||||
): Promise<SanitizedGlobalConfig> => {
|
||||
const { collections } = config
|
||||
|
||||
@@ -64,7 +65,8 @@ export const sanitizeGlobal = async (
|
||||
}
|
||||
|
||||
// Sanitize fields
|
||||
const validRelationships = collections.map((c) => c.slug) || []
|
||||
const validRelationships = _validRelationships ?? config.collections.map((c) => c.slug) ?? []
|
||||
|
||||
global.fields = await sanitizeFields({
|
||||
config,
|
||||
fields: global.fields,
|
||||
|
||||
@@ -75,7 +75,7 @@ import { generateImportMap, type ImportMap } from './bin/generateImportMap/index
|
||||
import { checkPayloadDependencies } from './checkPayloadDependencies.js'
|
||||
import localOperations from './collections/operations/local/index.js'
|
||||
import { consoleEmailAdapter } from './email/consoleEmailAdapter.js'
|
||||
import { fieldAffectsData } from './fields/config/types.js'
|
||||
import { fieldAffectsData, type FlattenedBlock } from './fields/config/types.js'
|
||||
import localGlobalOperations from './globals/operations/local/index.js'
|
||||
import { getJobsLocalAPI } from './queues/localAPI.js'
|
||||
import { isNextBuild } from './utilities/isNextBuild.js'
|
||||
@@ -106,6 +106,9 @@ export interface GeneratedTypes {
|
||||
}
|
||||
}
|
||||
|
||||
blocksUntyped: {
|
||||
[slug: string]: JsonObject
|
||||
}
|
||||
collectionsJoinsUntyped: {
|
||||
[slug: string]: {
|
||||
[schemaPath: string]: CollectionSlug
|
||||
@@ -151,6 +154,11 @@ type ResolveCollectionType<T> = 'collections' extends keyof T
|
||||
: // @ts-expect-error
|
||||
T['collectionsUntyped']
|
||||
|
||||
type ResolveBlockType<T> = 'blocks' extends keyof T
|
||||
? T['blocks']
|
||||
: // @ts-expect-error
|
||||
T['blocksUntyped']
|
||||
|
||||
type ResolveCollectionSelectType<T> = 'collectionsSelect' extends keyof T
|
||||
? T['collectionsSelect']
|
||||
: // @ts-expect-error
|
||||
@@ -174,6 +182,8 @@ type ResolveGlobalSelectType<T> = 'globalsSelect' extends keyof T
|
||||
// Applying helper types to GeneratedTypes
|
||||
export type TypedCollection = ResolveCollectionType<GeneratedTypes>
|
||||
|
||||
export type TypedBlock = ResolveBlockType<GeneratedTypes>
|
||||
|
||||
export type TypedUploadCollection = NonNever<{
|
||||
[K in keyof TypedCollection]:
|
||||
| 'filename'
|
||||
@@ -198,6 +208,8 @@ export type StringKeyOf<T> = Extract<keyof T, string>
|
||||
// Define the types for slugs using the appropriate collections and globals
|
||||
export type CollectionSlug = StringKeyOf<TypedCollection>
|
||||
|
||||
export type BlockSlug = StringKeyOf<TypedBlock>
|
||||
|
||||
export type UploadCollectionSlug = StringKeyOf<TypedUploadCollection>
|
||||
|
||||
type ResolveDbType<T> = 'db' extends keyof T
|
||||
@@ -247,6 +259,8 @@ export class BasePayload {
|
||||
|
||||
authStrategies: AuthStrategy[]
|
||||
|
||||
blocks: Record<BlockSlug, FlattenedBlock> = {}
|
||||
|
||||
collections: Record<CollectionSlug, Collection> = {}
|
||||
|
||||
config: SanitizedConfig
|
||||
@@ -602,7 +616,7 @@ export class BasePayload {
|
||||
}
|
||||
}
|
||||
|
||||
traverseFields({ callback: findCustomID, fields: collection.fields })
|
||||
traverseFields({ callback: findCustomID, config: this.config, fields: collection.fields })
|
||||
|
||||
this.collections[collection.slug] = {
|
||||
config: collection,
|
||||
@@ -610,6 +624,11 @@ export class BasePayload {
|
||||
}
|
||||
}
|
||||
|
||||
this.blocks = this.config.blocks.reduce((blocks, block) => {
|
||||
blocks[block.slug] = block
|
||||
return blocks
|
||||
}, {})
|
||||
|
||||
// Generate types on startup
|
||||
if (process.env.NODE_ENV !== 'production' && this.config.typescript.autoGenerate !== false) {
|
||||
// We cannot run it directly here, as generate-types imports json-schema-to-typescript, which breaks on turbopack.
|
||||
@@ -811,6 +830,11 @@ export const reload = async (
|
||||
return collections
|
||||
}, {})
|
||||
|
||||
payload.blocks = config.blocks.reduce((blocks, block) => {
|
||||
blocks[block.slug] = block
|
||||
return blocks
|
||||
}, {})
|
||||
|
||||
payload.globals = {
|
||||
config: config.globals,
|
||||
}
|
||||
@@ -1044,6 +1068,7 @@ export {
|
||||
createClientConfig,
|
||||
serverOnlyAdminConfigProperties,
|
||||
serverOnlyConfigProperties,
|
||||
type UnsanitizedClientConfig,
|
||||
} from './config/client.js'
|
||||
export { defaults } from './config/defaults.js'
|
||||
|
||||
|
||||
@@ -294,14 +294,22 @@ export function fieldsToJSONSchema(
|
||||
// Check for a case where no blocks are provided.
|
||||
// We need to generate an empty array for this case, note that JSON schema 4 doesn't support empty arrays
|
||||
// so the best we can get is `unknown[]`
|
||||
const hasBlocks = Boolean(field.blocks.length)
|
||||
const hasBlocks = Boolean(
|
||||
field.blockReferences ? field.blockReferences.length : field.blocks.length,
|
||||
)
|
||||
|
||||
fieldSchema = {
|
||||
...baseFieldSchema,
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
items: hasBlocks
|
||||
? {
|
||||
oneOf: field.blocks.map((block) => {
|
||||
oneOf: (field.blockReferences ?? field.blocks).map((block) => {
|
||||
if (typeof block === 'string') {
|
||||
const resolvedBlock = config?.blocks?.find((b) => b.slug === block)
|
||||
return {
|
||||
$ref: `#/definitions/${resolvedBlock.interfaceName ?? resolvedBlock.slug}`,
|
||||
}
|
||||
}
|
||||
const blockFieldSchemas = fieldsToJSONSchema(
|
||||
collectionIDFieldTypes,
|
||||
block.flattenedFields,
|
||||
@@ -732,9 +740,11 @@ export function entityToJSONSchema(
|
||||
}
|
||||
|
||||
export function fieldsToSelectJSONSchema({
|
||||
config,
|
||||
fields,
|
||||
interfaceNameDefinitions,
|
||||
}: {
|
||||
config: SanitizedConfig
|
||||
fields: FlattenedField[]
|
||||
interfaceNameDefinitions: Map<string, JSONSchema4>
|
||||
}): JSONSchema4 {
|
||||
@@ -750,6 +760,7 @@ export function fieldsToSelectJSONSchema({
|
||||
case 'group':
|
||||
case 'tab': {
|
||||
let fieldSchema: JSONSchema4 = fieldsToSelectJSONSchema({
|
||||
config,
|
||||
fields: field.flattenedFields,
|
||||
interfaceNameDefinitions,
|
||||
})
|
||||
@@ -782,8 +793,13 @@ export function fieldsToSelectJSONSchema({
|
||||
properties: {},
|
||||
}
|
||||
|
||||
for (const block of field.blocks) {
|
||||
for (const block of field.blockReferences ?? field.blocks) {
|
||||
if (typeof block === 'string') {
|
||||
continue // TODO
|
||||
}
|
||||
|
||||
let blockSchema = fieldsToSelectJSONSchema({
|
||||
config,
|
||||
fields: block.flattenedFields,
|
||||
interfaceNameDefinitions,
|
||||
})
|
||||
@@ -1038,6 +1054,7 @@ export function configToJSONSchema(
|
||||
i18n,
|
||||
)
|
||||
const select = fieldsToSelectJSONSchema({
|
||||
config,
|
||||
fields: entity.flattenedFields,
|
||||
interfaceNameDefinitions,
|
||||
})
|
||||
@@ -1081,6 +1098,42 @@ export function configToJSONSchema(
|
||||
)
|
||||
: {}
|
||||
|
||||
const blocksDefinition: JSONSchema4 = {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {},
|
||||
required: [],
|
||||
}
|
||||
for (const block of config.blocks) {
|
||||
const blockFieldSchemas = fieldsToJSONSchema(
|
||||
collectionIDFieldTypes,
|
||||
block.flattenedFields,
|
||||
interfaceNameDefinitions,
|
||||
config,
|
||||
i18n,
|
||||
)
|
||||
|
||||
const blockSchema: JSONSchema4 = {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
...blockFieldSchemas.properties,
|
||||
blockType: {
|
||||
const: block.slug,
|
||||
},
|
||||
},
|
||||
required: ['blockType', ...blockFieldSchemas.required],
|
||||
}
|
||||
|
||||
const interfaceName = block.interfaceName ?? block.slug
|
||||
interfaceNameDefinitions.set(interfaceName, blockSchema)
|
||||
|
||||
blocksDefinition.properties[block.slug] = {
|
||||
$ref: `#/definitions/${interfaceName}`,
|
||||
}
|
||||
;(blocksDefinition.required as string[]).push(block.slug)
|
||||
}
|
||||
|
||||
let jsonSchema: JSONSchema4 = {
|
||||
additionalProperties: false,
|
||||
definitions: {
|
||||
@@ -1093,6 +1146,7 @@ export function configToJSONSchema(
|
||||
type: 'object',
|
||||
properties: {
|
||||
auth: generateAuthOperationSchemas(config.collections),
|
||||
blocks: blocksDefinition,
|
||||
collections: generateEntitySchemas(config.collections || []),
|
||||
collectionsJoins: generateCollectionJoinsSchemas(config.collections || []),
|
||||
collectionsSelect: generateEntitySelectSchemas(config.collections || []),
|
||||
@@ -1113,6 +1167,7 @@ export function configToJSONSchema(
|
||||
'auth',
|
||||
'db',
|
||||
'jobs',
|
||||
'blocks',
|
||||
],
|
||||
title: 'Config',
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ClientConfig } from '../config/client.js'
|
||||
// @ts-strict-ignore
|
||||
import type { ClientField } from '../fields/config/client.js'
|
||||
import type { FieldTypes } from '../fields/config/types.js'
|
||||
@@ -12,7 +13,7 @@ export type FieldSchemaJSON = {
|
||||
type: FieldTypes
|
||||
}[]
|
||||
|
||||
export const fieldSchemaToJSON = (fields: ClientField[]): FieldSchemaJSON => {
|
||||
export const fieldSchemaToJSON = (fields: ClientField[], config: ClientConfig): FieldSchemaJSON => {
|
||||
return fields.reduce((acc, field) => {
|
||||
let result = acc
|
||||
|
||||
@@ -21,13 +22,16 @@ export const fieldSchemaToJSON = (fields: ClientField[]): FieldSchemaJSON => {
|
||||
acc.push({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
fields: fieldSchemaToJSON([
|
||||
...field.fields,
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
},
|
||||
]),
|
||||
fields: fieldSchemaToJSON(
|
||||
[
|
||||
...field.fields,
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
config,
|
||||
),
|
||||
})
|
||||
|
||||
break
|
||||
@@ -36,15 +40,19 @@ export const fieldSchemaToJSON = (fields: ClientField[]): FieldSchemaJSON => {
|
||||
acc.push({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
blocks: field.blocks.reduce((acc, block) => {
|
||||
blocks: (field.blockReferences ?? field.blocks).reduce((acc, _block) => {
|
||||
const block = typeof _block === 'string' ? config.blocksMap[_block] : _block
|
||||
acc[block.slug] = {
|
||||
fields: fieldSchemaToJSON([
|
||||
...block.fields,
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
},
|
||||
]),
|
||||
fields: fieldSchemaToJSON(
|
||||
[
|
||||
...block.fields,
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
config,
|
||||
),
|
||||
}
|
||||
|
||||
return acc
|
||||
@@ -55,14 +63,14 @@ export const fieldSchemaToJSON = (fields: ClientField[]): FieldSchemaJSON => {
|
||||
|
||||
case 'collapsible': // eslint-disable no-fallthrough
|
||||
case 'row':
|
||||
result = result.concat(fieldSchemaToJSON(field.fields))
|
||||
result = result.concat(fieldSchemaToJSON(field.fields, config))
|
||||
break
|
||||
|
||||
case 'group':
|
||||
acc.push({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
fields: fieldSchemaToJSON(field.fields),
|
||||
fields: fieldSchemaToJSON(field.fields, config),
|
||||
})
|
||||
|
||||
break
|
||||
@@ -86,12 +94,12 @@ export const fieldSchemaToJSON = (fields: ClientField[]): FieldSchemaJSON => {
|
||||
tabFields.push({
|
||||
name: tab.name,
|
||||
type: field.type,
|
||||
fields: fieldSchemaToJSON(tab.fields),
|
||||
fields: fieldSchemaToJSON(tab.fields, config),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
tabFields = tabFields.concat(fieldSchemaToJSON(tab.fields))
|
||||
tabFields = tabFields.concat(fieldSchemaToJSON(tab.fields, config))
|
||||
})
|
||||
|
||||
result = result.concat(tabFields)
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
import type { Field, FlattenedField, FlattenedJoinField } from '../fields/config/types.js'
|
||||
import type {
|
||||
Block,
|
||||
Field,
|
||||
FlattenedBlock,
|
||||
FlattenedBlocksField,
|
||||
FlattenedField,
|
||||
FlattenedJoinField,
|
||||
} from '../fields/config/types.js'
|
||||
|
||||
import { tabHasName } from '../fields/config/types.js'
|
||||
|
||||
export const flattenBlock = ({ block }: { block: Block }): FlattenedBlock => {
|
||||
return {
|
||||
...block,
|
||||
flattenedFields: flattenAllFields({ fields: block.fields }),
|
||||
}
|
||||
}
|
||||
|
||||
export const flattenAllFields = ({ fields }: { fields: Field[] }): FlattenedField[] => {
|
||||
const result: FlattenedField[] = []
|
||||
|
||||
@@ -14,17 +28,34 @@ export const flattenAllFields = ({ fields }: { fields: Field[] }): FlattenedFiel
|
||||
}
|
||||
|
||||
case 'blocks': {
|
||||
const blocks = []
|
||||
for (const block of field.blocks) {
|
||||
blocks.push({
|
||||
...block,
|
||||
flattenedFields: flattenAllFields({ fields: block.fields }),
|
||||
})
|
||||
const blocks: FlattenedBlock[] = []
|
||||
let blockReferences: (FlattenedBlock | string)[] | undefined = undefined
|
||||
if (field.blockReferences) {
|
||||
blockReferences = []
|
||||
for (const block of field.blockReferences) {
|
||||
if (typeof block === 'string') {
|
||||
blockReferences.push(block)
|
||||
continue
|
||||
}
|
||||
blockReferences.push(flattenBlock({ block }))
|
||||
}
|
||||
} else {
|
||||
for (const block of field.blocks) {
|
||||
if (typeof block === 'string') {
|
||||
blocks.push(block)
|
||||
continue
|
||||
}
|
||||
blocks.push(flattenBlock({ block }))
|
||||
}
|
||||
}
|
||||
result.push({
|
||||
|
||||
const resultField: FlattenedBlocksField = {
|
||||
...field,
|
||||
blockReferences,
|
||||
blocks,
|
||||
})
|
||||
}
|
||||
|
||||
result.push(resultField)
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
@@ -172,13 +172,18 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
|
||||
})
|
||||
}
|
||||
|
||||
if ('blocks' in field && field.blocks) {
|
||||
if (
|
||||
('blocks' in field && field.blocks) ||
|
||||
('blockReferences' in field && field.blockReferences)
|
||||
) {
|
||||
if (!mutablePolicies[field.name]?.blocks) {
|
||||
mutablePolicies[field.name].blocks = {}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
field.blocks.map(async (block) => {
|
||||
(field.blockReferences ?? field.blocks).map(async (_block) => {
|
||||
const block = typeof _block === 'string' ? payload.blocks[_block] : _block // TODO: Skip over string blocks
|
||||
|
||||
if (!mutablePolicies[field.name].blocks?.[block.slug]) {
|
||||
mutablePolicies[field.name].blocks[block.slug] = {
|
||||
fields: {},
|
||||
|
||||
@@ -1,43 +1,86 @@
|
||||
// @ts-strict-ignore
|
||||
import type { ArrayField, BlocksField, Field, TabAsField } from '../fields/config/types.js'
|
||||
import type { Config, SanitizedConfig } from '../config/types.js'
|
||||
import type { ArrayField, Block, BlocksField, Field, TabAsField } from '../fields/config/types.js'
|
||||
|
||||
import { fieldHasSubFields } from '../fields/config/types.js'
|
||||
|
||||
const traverseArrayOrBlocksField = ({
|
||||
callback,
|
||||
callbackStack,
|
||||
config,
|
||||
data,
|
||||
field,
|
||||
fillEmpty,
|
||||
leavesFirst,
|
||||
parentRef,
|
||||
}: {
|
||||
callback: TraverseFieldsCallback
|
||||
callbackStack: TraverseFieldsCallback[]
|
||||
config: Config | SanitizedConfig
|
||||
data: Record<string, unknown>[]
|
||||
field: ArrayField | BlocksField
|
||||
fillEmpty: boolean
|
||||
leavesFirst: boolean
|
||||
parentRef?: unknown
|
||||
}) => {
|
||||
if (fillEmpty) {
|
||||
if (field.type === 'array') {
|
||||
traverseFields({ callback, fields: field.fields, parentRef })
|
||||
traverseFields({
|
||||
callback,
|
||||
callbackStack,
|
||||
config,
|
||||
fields: field.fields,
|
||||
isTopLevel: false,
|
||||
leavesFirst,
|
||||
parentRef,
|
||||
})
|
||||
}
|
||||
if (field.type === 'blocks') {
|
||||
field.blocks.forEach((block) => {
|
||||
traverseFields({ callback, fields: block.fields, parentRef })
|
||||
})
|
||||
for (const _block of field.blockReferences ?? field.blocks) {
|
||||
// TODO: iterate over blocks mapped to block slug in v4, or pass through payload.blocks
|
||||
const block =
|
||||
typeof _block === 'string' ? config.blocks.find((b) => b.slug === _block) : _block
|
||||
traverseFields({
|
||||
callback,
|
||||
callbackStack,
|
||||
config,
|
||||
fields: block.fields,
|
||||
isTopLevel: false,
|
||||
leavesFirst,
|
||||
parentRef,
|
||||
})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
for (const ref of data) {
|
||||
let fields: Field[]
|
||||
if (field.type === 'blocks' && typeof ref?.blockType === 'string') {
|
||||
const block = field.blocks.find((block) => block.slug === ref.blockType)
|
||||
// TODO: iterate over blocks mapped to block slug in v4, or pass through payload.blocks
|
||||
const block = field.blockReferences
|
||||
? ((config.blocks.find((b) => b.slug === ref.blockType) ??
|
||||
field.blockReferences.find(
|
||||
(b) => typeof b !== 'string' && b.slug === ref.blockType,
|
||||
)) as Block)
|
||||
: field.blocks.find((b) => b.slug === ref.blockType)
|
||||
|
||||
fields = block?.fields
|
||||
} else if (field.type === 'array') {
|
||||
fields = field.fields
|
||||
}
|
||||
|
||||
if (fields) {
|
||||
traverseFields({ callback, fields, fillEmpty, parentRef, ref })
|
||||
traverseFields({
|
||||
callback,
|
||||
callbackStack,
|
||||
config,
|
||||
fields,
|
||||
fillEmpty,
|
||||
isTopLevel: false,
|
||||
leavesFirst,
|
||||
parentRef,
|
||||
ref,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,8 +106,18 @@ export type TraverseFieldsCallback = (args: {
|
||||
|
||||
type TraverseFieldsArgs = {
|
||||
callback: TraverseFieldsCallback
|
||||
callbackStack?: TraverseFieldsCallback[]
|
||||
config: Config | SanitizedConfig
|
||||
fields: (Field | TabAsField)[]
|
||||
fillEmpty?: boolean
|
||||
isTopLevel?: boolean
|
||||
/**
|
||||
* @default false
|
||||
*
|
||||
* if this is `true`, the callback functions of the leaf fields will be called before the parent fields.
|
||||
* The return value of the callback function will be ignored.
|
||||
*/
|
||||
leavesFirst?: boolean
|
||||
parentRef?: Record<string, unknown> | unknown
|
||||
ref?: Record<string, unknown> | unknown
|
||||
}
|
||||
@@ -80,12 +133,20 @@ type TraverseFieldsArgs = {
|
||||
*/
|
||||
export const traverseFields = ({
|
||||
callback,
|
||||
callbackStack: _callbackStack = [],
|
||||
config,
|
||||
fields,
|
||||
fillEmpty = true,
|
||||
isTopLevel = true,
|
||||
leavesFirst = false,
|
||||
parentRef = {},
|
||||
ref = {},
|
||||
}: TraverseFieldsArgs): void => {
|
||||
fields.some((field) => {
|
||||
let callbackStack: TraverseFieldsCallback[] = []
|
||||
if (!isTopLevel) {
|
||||
callbackStack = _callbackStack
|
||||
}
|
||||
let skip = false
|
||||
const next = () => {
|
||||
skip = true
|
||||
@@ -95,8 +156,10 @@ export const traverseFields = ({
|
||||
return
|
||||
}
|
||||
|
||||
if (callback && callback({ field, next, parentRef, ref })) {
|
||||
if (!leavesFirst && callback && callback({ field, next, parentRef, ref })) {
|
||||
return true
|
||||
} else if (leavesFirst) {
|
||||
callbackStack.push(callback)
|
||||
}
|
||||
|
||||
if (skip) {
|
||||
@@ -130,6 +193,7 @@ export const traverseFields = ({
|
||||
|
||||
if (
|
||||
callback &&
|
||||
!leavesFirst &&
|
||||
callback({
|
||||
field: { ...tab, type: 'tab' },
|
||||
next,
|
||||
@@ -138,6 +202,8 @@ export const traverseFields = ({
|
||||
})
|
||||
) {
|
||||
return true
|
||||
} else if (leavesFirst) {
|
||||
callbackStack.push(callback)
|
||||
}
|
||||
|
||||
tabRef = tabRef[tab.name]
|
||||
@@ -147,8 +213,12 @@ export const traverseFields = ({
|
||||
if (tabRef[key] && typeof tabRef[key] === 'object') {
|
||||
traverseFields({
|
||||
callback,
|
||||
callbackStack,
|
||||
config,
|
||||
fields: tab.fields,
|
||||
fillEmpty,
|
||||
isTopLevel: false,
|
||||
leavesFirst,
|
||||
parentRef: currentParentRef,
|
||||
ref: tabRef[key],
|
||||
})
|
||||
@@ -158,6 +228,7 @@ export const traverseFields = ({
|
||||
} else {
|
||||
if (
|
||||
callback &&
|
||||
!leavesFirst &&
|
||||
callback({
|
||||
field: { ...tab, type: 'tab' },
|
||||
next,
|
||||
@@ -166,14 +237,20 @@ export const traverseFields = ({
|
||||
})
|
||||
) {
|
||||
return true
|
||||
} else if (leavesFirst) {
|
||||
callbackStack.push(callback)
|
||||
}
|
||||
}
|
||||
|
||||
if (!tab.localized) {
|
||||
traverseFields({
|
||||
callback,
|
||||
callbackStack,
|
||||
config,
|
||||
fields: tab.fields,
|
||||
fillEmpty,
|
||||
isTopLevel: false,
|
||||
leavesFirst,
|
||||
parentRef: currentParentRef,
|
||||
ref: tabRef,
|
||||
})
|
||||
@@ -226,8 +303,12 @@ export const traverseFields = ({
|
||||
if (currentRef[key]) {
|
||||
traverseFields({
|
||||
callback,
|
||||
callbackStack,
|
||||
config,
|
||||
fields: field.fields,
|
||||
fillEmpty,
|
||||
isTopLevel: false,
|
||||
leavesFirst,
|
||||
parentRef: currentParentRef,
|
||||
ref: currentRef[key],
|
||||
})
|
||||
@@ -254,30 +335,46 @@ export const traverseFields = ({
|
||||
|
||||
traverseArrayOrBlocksField({
|
||||
callback,
|
||||
callbackStack,
|
||||
config,
|
||||
data: localeData,
|
||||
field,
|
||||
fillEmpty,
|
||||
leavesFirst,
|
||||
parentRef: currentParentRef,
|
||||
})
|
||||
}
|
||||
} else if (Array.isArray(currentRef)) {
|
||||
traverseArrayOrBlocksField({
|
||||
callback,
|
||||
callbackStack,
|
||||
config,
|
||||
data: currentRef as Record<string, unknown>[],
|
||||
field,
|
||||
fillEmpty,
|
||||
leavesFirst,
|
||||
parentRef: currentParentRef,
|
||||
})
|
||||
}
|
||||
} else if (currentRef && typeof currentRef === 'object' && 'fields' in field) {
|
||||
traverseFields({
|
||||
callback,
|
||||
callbackStack,
|
||||
config,
|
||||
fields: field.fields,
|
||||
fillEmpty,
|
||||
isTopLevel: false,
|
||||
leavesFirst,
|
||||
parentRef: currentParentRef,
|
||||
ref: currentRef,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (isTopLevel) {
|
||||
callbackStack.reverse().forEach((cb) => {
|
||||
cb({ field, next, parentRef, ref })
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -161,6 +161,7 @@ export const multiTenantPlugin =
|
||||
* Modify enabled collections
|
||||
*/
|
||||
addFilterOptionsToFields({
|
||||
config: incomingConfig,
|
||||
fields: collection.fields,
|
||||
tenantEnabledCollectionSlugs: collectionSlugs,
|
||||
tenantEnabledGlobalSlugs: globalCollectionSlugs,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { Field, FilterOptionsProps, RelationshipField } from 'payload'
|
||||
import type { Config, Field, FilterOptionsProps, RelationshipField, SanitizedConfig } from 'payload'
|
||||
|
||||
import { getCollectionIDType } from './getCollectionIDType.js'
|
||||
import { getTenantFromCookie } from './getTenantFromCookie.js'
|
||||
|
||||
type AddFilterOptionsToFieldsArgs = {
|
||||
config: Config | SanitizedConfig
|
||||
fields: Field[]
|
||||
tenantEnabledCollectionSlugs: string[]
|
||||
tenantEnabledGlobalSlugs: string[]
|
||||
@@ -12,6 +13,7 @@ type AddFilterOptionsToFieldsArgs = {
|
||||
}
|
||||
|
||||
export function addFilterOptionsToFields({
|
||||
config,
|
||||
fields,
|
||||
tenantEnabledCollectionSlugs,
|
||||
tenantEnabledGlobalSlugs,
|
||||
@@ -59,6 +61,7 @@ export function addFilterOptionsToFields({
|
||||
field.type === 'group'
|
||||
) {
|
||||
addFilterOptionsToFields({
|
||||
config,
|
||||
fields: field.fields,
|
||||
tenantEnabledCollectionSlugs,
|
||||
tenantEnabledGlobalSlugs,
|
||||
@@ -68,20 +71,30 @@ export function addFilterOptionsToFields({
|
||||
}
|
||||
|
||||
if (field.type === 'blocks') {
|
||||
field.blocks.forEach((block) => {
|
||||
addFilterOptionsToFields({
|
||||
fields: block.fields,
|
||||
tenantEnabledCollectionSlugs,
|
||||
tenantEnabledGlobalSlugs,
|
||||
tenantFieldName,
|
||||
tenantsCollectionSlug,
|
||||
})
|
||||
;(field.blockReferences ?? field.blocks).forEach((_block) => {
|
||||
const block =
|
||||
typeof _block === 'string'
|
||||
? // TODO: iterate over blocks mapped to block slug in v4, or pass through payload.blocks
|
||||
config?.blocks?.find((b) => b.slug === _block)
|
||||
: _block
|
||||
|
||||
if (block?.fields) {
|
||||
addFilterOptionsToFields({
|
||||
config,
|
||||
fields: block.fields,
|
||||
tenantEnabledCollectionSlugs,
|
||||
tenantEnabledGlobalSlugs,
|
||||
tenantFieldName,
|
||||
tenantsCollectionSlug,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (field.type === 'tabs') {
|
||||
field.tabs.forEach((tab) => {
|
||||
addFilterOptionsToFields({
|
||||
config,
|
||||
fields: tab.fields,
|
||||
tenantEnabledCollectionSlugs,
|
||||
tenantEnabledGlobalSlugs,
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Pill,
|
||||
RenderFields,
|
||||
SectionTitle,
|
||||
useConfig,
|
||||
useDocumentForm,
|
||||
useDocumentInfo,
|
||||
useEditDepth,
|
||||
@@ -28,7 +29,12 @@ const baseClass = 'lexical-block'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { $getNodeByKey } from 'lexical'
|
||||
import { type BlocksFieldClient, type CollapsedPreferences, type FormState } from 'payload'
|
||||
import {
|
||||
type BlocksFieldClient,
|
||||
type ClientBlock,
|
||||
type CollapsedPreferences,
|
||||
type FormState,
|
||||
} from 'payload'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import type { BlockFields } from '../../server/nodes/BlocksNode.js'
|
||||
@@ -72,6 +78,8 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
const editDepth = useEditDepth()
|
||||
const [errorCount, setErrorCount] = React.useState(0)
|
||||
|
||||
const { config } = useConfig()
|
||||
|
||||
const drawerSlug = formatDrawerSlug({
|
||||
slug: `lexical-blocks-create-${uuidFromContext}-${formData.id}`,
|
||||
depth: editDepth,
|
||||
@@ -212,7 +220,11 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
componentMapRenderedBlockPath
|
||||
]?.[0] as BlocksFieldClient
|
||||
|
||||
const clientBlock = blocksField?.blocks?.[0]
|
||||
const clientBlock: ClientBlock | undefined = blocksField.blockReferences
|
||||
? typeof blocksField?.blockReferences?.[0] === 'string'
|
||||
? config.blocksMap[blocksField?.blockReferences?.[0]]
|
||||
: blocksField?.blockReferences?.[0]
|
||||
: blocksField?.blocks?.[0]
|
||||
|
||||
const { i18n, t } = useTranslation<object, string>()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React, { createContext, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
const baseClass = 'inline-block'
|
||||
|
||||
import type { BlocksFieldClient, Data, FormState } from 'payload'
|
||||
import type { BlocksFieldClient, ClientBlock, Data, FormState } from 'payload'
|
||||
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
FormSubmit,
|
||||
RenderFields,
|
||||
ShimmerEffect,
|
||||
useConfig,
|
||||
useDocumentForm,
|
||||
useDocumentInfo,
|
||||
useEditDepth,
|
||||
@@ -120,6 +121,7 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
|
||||
|
||||
const inlineBlockElemElemRef = useRef<HTMLDivElement | null>(null)
|
||||
const { id, collectionSlug, getDocPreferences, globalSlug } = useDocumentInfo()
|
||||
const { config } = useConfig()
|
||||
|
||||
const componentMapRenderedBlockPath = `${schemaPath}.lexical_internal_feature.blocks.lexical_inline_blocks.${formData.blockType}`
|
||||
|
||||
@@ -129,7 +131,11 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
|
||||
componentMapRenderedBlockPath
|
||||
]?.[0] as BlocksFieldClient
|
||||
|
||||
const clientBlock = blocksField?.blocks?.[0]
|
||||
const clientBlock: ClientBlock | undefined = blocksField.blockReferences
|
||||
? typeof blocksField?.blockReferences?.[0] === 'string'
|
||||
? config.blocksMap[blocksField?.blockReferences?.[0]]
|
||||
: blocksField?.blockReferences?.[0]
|
||||
: blocksField?.blocks?.[0]
|
||||
|
||||
const clientBlockFields = clientBlock?.fields ?? []
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import { InlineBlockNode } from './nodes/InlineBlocksNode.js'
|
||||
import { INSERT_BLOCK_COMMAND, INSERT_INLINE_BLOCK_COMMAND } from './plugin/commands.js'
|
||||
import { BlocksPlugin } from './plugin/index.js'
|
||||
export const BlocksFeatureClient = createClientFeature(
|
||||
({ featureClientSchemaMap, props, schemaPath }) => {
|
||||
({ config, featureClientSchemaMap, props, schemaPath }) => {
|
||||
const schemaMapRenderedBlockPathPrefix = `${schemaPath}.lexical_internal_feature.blocks.lexical_blocks`
|
||||
const schemaMapRenderedInlineBlockPathPrefix = `${schemaPath}.lexical_internal_feature.blocks.lexical_inline_blocks`
|
||||
const clientSchema = featureClientSchemaMap['blocks']
|
||||
@@ -47,13 +47,21 @@ export const BlocksFeatureClient = createClientFeature(
|
||||
|
||||
const clientBlocks: ClientBlock[] = blocksFields
|
||||
.map((field) => {
|
||||
return field.blocks[0]
|
||||
return field.blockReferences
|
||||
? typeof field.blockReferences[0] === 'string'
|
||||
? config.blocksMap[field.blockReferences[0]]
|
||||
: field.blockReferences[0]
|
||||
: field.blocks[0]
|
||||
})
|
||||
.filter((block) => block !== undefined)
|
||||
|
||||
const clientInlineBlocks: ClientBlock[] = inlineBlocksFields
|
||||
.map((field) => {
|
||||
return field.blocks[0]
|
||||
return field.blockReferences
|
||||
? typeof field.blockReferences[0] === 'string'
|
||||
? config.blocksMap[field.blockReferences[0]]
|
||||
: field.blockReferences[0]
|
||||
: field.blocks[0]
|
||||
})
|
||||
.filter((block) => block !== undefined)
|
||||
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import type { Block, BlocksField, Config, FieldSchemaMap, FlattenedBlocksField } from 'payload'
|
||||
import type {
|
||||
Block,
|
||||
BlocksField,
|
||||
BlockSlug,
|
||||
Config,
|
||||
FieldSchemaMap,
|
||||
FlattenedBlocksField,
|
||||
} from 'payload'
|
||||
|
||||
import { fieldsToJSONSchema, flattenAllFields, sanitizeFields } from 'payload'
|
||||
|
||||
@@ -12,12 +19,12 @@ import { ServerInlineBlockNode } from './nodes/InlineBlocksNode.js'
|
||||
import { blockValidationHOC } from './validate.js'
|
||||
|
||||
export type BlocksFeatureProps = {
|
||||
blocks?: Block[]
|
||||
inlineBlocks?: Block[]
|
||||
blocks?: (Block | BlockSlug)[] | Block[]
|
||||
inlineBlocks?: (Block | BlockSlug)[] | Block[]
|
||||
}
|
||||
|
||||
export const BlocksFeature = createServerFeature<BlocksFeatureProps, BlocksFeatureProps>({
|
||||
feature: async ({ config: _config, isRoot, parentIsLocalized, props }) => {
|
||||
feature: async ({ config: _config, isRoot, parentIsLocalized, props: _props }) => {
|
||||
const validRelationships = _config.collections.map((c) => c.slug) || []
|
||||
|
||||
const sanitized = await sanitizeFields({
|
||||
@@ -26,12 +33,14 @@ export const BlocksFeature = createServerFeature<BlocksFeatureProps, BlocksFeatu
|
||||
{
|
||||
name: 'lexical_blocks',
|
||||
type: 'blocks',
|
||||
blocks: props.blocks ?? [],
|
||||
blockReferences: _props.blocks ?? [],
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
name: 'lexical_inline_blocks',
|
||||
type: 'blocks',
|
||||
blocks: props.inlineBlocks ?? [],
|
||||
blockReferences: _props.inlineBlocks ?? [],
|
||||
blocks: [],
|
||||
},
|
||||
],
|
||||
parentIsLocalized,
|
||||
@@ -39,8 +48,31 @@ export const BlocksFeature = createServerFeature<BlocksFeatureProps, BlocksFeatu
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
props.blocks = (sanitized[0] as BlocksField).blocks
|
||||
props.inlineBlocks = (sanitized[1] as BlocksField).blocks
|
||||
const blockConfigs: Block[] = []
|
||||
for (const _block of (sanitized[0] as BlocksField).blockReferences ??
|
||||
(sanitized[0] as BlocksField).blocks) {
|
||||
const block =
|
||||
typeof _block === 'string' ? _config?.blocks?.find((b) => b.slug === _block) : _block
|
||||
if (!block) {
|
||||
throw new Error(
|
||||
`Block not found for slug: ${typeof _block === 'string' ? _block : _block?.slug}`,
|
||||
)
|
||||
}
|
||||
blockConfigs.push(block)
|
||||
}
|
||||
|
||||
const inlineBlockConfigs: Block[] = []
|
||||
for (const _block of (sanitized[1] as BlocksField).blockReferences ??
|
||||
(sanitized[1] as BlocksField).blocks) {
|
||||
const block =
|
||||
typeof _block === 'string' ? _config?.blocks?.find((b) => b.slug === _block) : _block
|
||||
if (!block) {
|
||||
throw new Error(
|
||||
`Block not found for slug: ${typeof _block === 'string' ? _block : _block?.slug}`,
|
||||
)
|
||||
}
|
||||
inlineBlockConfigs.push(block)
|
||||
}
|
||||
|
||||
return {
|
||||
ClientFeature: '@payloadcms/richtext-lexical/client#BlocksFeatureClient',
|
||||
@@ -53,30 +85,34 @@ export const BlocksFeature = createServerFeature<BlocksFeatureProps, BlocksFeatu
|
||||
i18n,
|
||||
interfaceNameDefinitions,
|
||||
}) => {
|
||||
if (!props?.blocks?.length && !props?.inlineBlocks?.length) {
|
||||
if (!blockConfigs?.length && !inlineBlockConfigs?.length) {
|
||||
return currentSchema
|
||||
}
|
||||
|
||||
const fields: FlattenedBlocksField[] = []
|
||||
|
||||
if (props?.blocks?.length) {
|
||||
if (blockConfigs?.length) {
|
||||
fields.push({
|
||||
name: field?.name + '_lexical_blocks',
|
||||
type: 'blocks',
|
||||
blocks: props.blocks.map((block) => ({
|
||||
...block,
|
||||
flattenedFields: flattenAllFields({ fields: block.fields }),
|
||||
})),
|
||||
blocks: blockConfigs.map((block) => {
|
||||
return {
|
||||
...block,
|
||||
flattenedFields: flattenAllFields({ fields: block.fields }),
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
if (props?.inlineBlocks?.length) {
|
||||
if (inlineBlockConfigs?.length) {
|
||||
fields.push({
|
||||
name: field?.name + '_lexical_inline_blocks',
|
||||
type: 'blocks',
|
||||
blocks: props.inlineBlocks.map((block) => ({
|
||||
...block,
|
||||
flattenedFields: flattenAllFields({ fields: block.fields }),
|
||||
})),
|
||||
blocks: inlineBlockConfigs.map((block) => {
|
||||
return {
|
||||
...block,
|
||||
flattenedFields: flattenAllFields({ fields: block.fields }),
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -95,15 +131,15 @@ export const BlocksFeature = createServerFeature<BlocksFeatureProps, BlocksFeatu
|
||||
return currentSchema
|
||||
},
|
||||
},
|
||||
generateSchemaMap: ({ props }) => {
|
||||
generateSchemaMap: ({ config }) => {
|
||||
/**
|
||||
* Add sub-fields to the schemaMap. E.g. if you have an array field as part of the block, and it runs addRow, it will request these
|
||||
* sub-fields from the component map. Thus, we need to put them in the component map here.
|
||||
*/
|
||||
const schemaMap: FieldSchemaMap = new Map()
|
||||
|
||||
if (props?.blocks?.length) {
|
||||
for (const block of props.blocks) {
|
||||
if (blockConfigs?.length) {
|
||||
for (const block of blockConfigs) {
|
||||
const blockFields = [...block.fields]
|
||||
|
||||
if (block?.admin?.components) {
|
||||
@@ -129,9 +165,9 @@ export const BlocksFeature = createServerFeature<BlocksFeatureProps, BlocksFeatu
|
||||
}
|
||||
}
|
||||
|
||||
if (props?.inlineBlocks?.length) {
|
||||
if (inlineBlockConfigs?.length) {
|
||||
// To generate block schemaMap which generates things like the componentMap for admin.Label
|
||||
for (const block of props.inlineBlocks) {
|
||||
for (const block of inlineBlockConfigs) {
|
||||
const blockFields = [...block.fields]
|
||||
|
||||
if (block?.admin?.components) {
|
||||
@@ -163,8 +199,8 @@ export const BlocksFeature = createServerFeature<BlocksFeatureProps, BlocksFeatu
|
||||
},
|
||||
i18n,
|
||||
markdownTransformers: getBlockMarkdownTransformers({
|
||||
blocks: props.blocks,
|
||||
inlineBlocks: props.inlineBlocks,
|
||||
blocks: blockConfigs,
|
||||
inlineBlocks: inlineBlockConfigs,
|
||||
}),
|
||||
|
||||
nodes: [
|
||||
@@ -172,12 +208,12 @@ export const BlocksFeature = createServerFeature<BlocksFeatureProps, BlocksFeatu
|
||||
// @ts-expect-error - TODO: fix this
|
||||
getSubFields: ({ node }) => {
|
||||
if (!node) {
|
||||
if (props?.blocks?.length) {
|
||||
if (blockConfigs?.length) {
|
||||
return [
|
||||
{
|
||||
name: 'lexical_blocks',
|
||||
type: 'blocks',
|
||||
blocks: props.blocks,
|
||||
blocks: blockConfigs,
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -186,26 +222,26 @@ export const BlocksFeature = createServerFeature<BlocksFeatureProps, BlocksFeatu
|
||||
|
||||
const blockType = node.fields.blockType
|
||||
|
||||
const block = props.blocks?.find((block) => block.slug === blockType)
|
||||
const block = blockConfigs?.find((block) => block.slug === blockType)
|
||||
return block?.fields
|
||||
},
|
||||
getSubFieldsData: ({ node }) => {
|
||||
return node?.fields
|
||||
},
|
||||
graphQLPopulationPromises: [blockPopulationPromiseHOC(props.blocks)],
|
||||
graphQLPopulationPromises: [blockPopulationPromiseHOC(blockConfigs)],
|
||||
node: ServerBlockNode,
|
||||
validations: [blockValidationHOC(props.blocks)],
|
||||
validations: [blockValidationHOC(blockConfigs)],
|
||||
}),
|
||||
createNode({
|
||||
// @ts-expect-error - TODO: fix this
|
||||
getSubFields: ({ node }) => {
|
||||
if (!node) {
|
||||
if (props?.inlineBlocks?.length) {
|
||||
if (inlineBlockConfigs?.length) {
|
||||
return [
|
||||
{
|
||||
name: 'lexical_inline_blocks',
|
||||
type: 'blocks',
|
||||
blocks: props.inlineBlocks,
|
||||
blocks: inlineBlockConfigs,
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -214,18 +250,18 @@ export const BlocksFeature = createServerFeature<BlocksFeatureProps, BlocksFeatu
|
||||
|
||||
const blockType = node.fields.blockType
|
||||
|
||||
const block = props.inlineBlocks?.find((block) => block.slug === blockType)
|
||||
const block = inlineBlockConfigs?.find((block) => block.slug === blockType)
|
||||
return block?.fields
|
||||
},
|
||||
getSubFieldsData: ({ node }) => {
|
||||
return node?.fields
|
||||
},
|
||||
graphQLPopulationPromises: [blockPopulationPromiseHOC(props.inlineBlocks)],
|
||||
graphQLPopulationPromises: [blockPopulationPromiseHOC(inlineBlockConfigs)],
|
||||
node: ServerInlineBlockNode,
|
||||
validations: [blockValidationHOC(props.inlineBlocks)],
|
||||
validations: [blockValidationHOC(inlineBlockConfigs)],
|
||||
}),
|
||||
],
|
||||
sanitizedServerFeatureProps: props,
|
||||
sanitizedServerFeatureProps: _props,
|
||||
}
|
||||
},
|
||||
key: 'blocks',
|
||||
|
||||
@@ -6,6 +6,11 @@ import type { NodeValidation } from '../../typesServer.js'
|
||||
import type { BlockFields, SerializedBlockNode } from './nodes/BlocksNode.js'
|
||||
import type { SerializedInlineBlockNode } from './nodes/InlineBlocksNode.js'
|
||||
|
||||
/**
|
||||
* Runs validation for blocks. This function will determine if the rich text field itself is valid. It does not handle
|
||||
* block field error paths - this is done by the `beforeChangeTraverseFields` call in the `beforeChange` hook, called from the
|
||||
* rich text adapter.
|
||||
*/
|
||||
export const blockValidationHOC = (
|
||||
blocks: Block[],
|
||||
): NodeValidation<SerializedBlockNode | SerializedInlineBlockNode> => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SerializedEditorState } from 'lexical'
|
||||
import type { Field, FieldAffectingData, RichTextField } from 'payload'
|
||||
import type { Field, FieldAffectingData, PayloadRequest, RichTextField } from 'payload'
|
||||
|
||||
import type { SanitizedServerEditorConfig } from '../../../../lexical/config/types.js'
|
||||
import type { AdapterProps, LexicalRichTextAdapter } from '../../../../types.js'
|
||||
|
||||
@@ -268,6 +268,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
originalNode: originalNodeIDMap[id],
|
||||
parentRichTextFieldPath: path,
|
||||
parentRichTextFieldSchemaPath: schemaPath,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
previousNode: previousNodeIDMap[id]!,
|
||||
req,
|
||||
})
|
||||
@@ -281,8 +282,10 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
if (subFieldFn && subFieldDataFn) {
|
||||
const subFields = subFieldFn({ node, req })
|
||||
const nodeSiblingData = subFieldDataFn({ node, req }) ?? {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
const nodeSiblingDoc = subFieldDataFn({ node: originalNodeIDMap[id]!, req }) ?? {}
|
||||
const nodePreviousSiblingDoc =
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
subFieldDataFn({ node: previousNodeIDMap[id]!, req }) ?? {}
|
||||
|
||||
if (subFields?.length) {
|
||||
@@ -540,6 +543,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
originalNodeWithLocales: originalNodeWithLocalesIDMap[id],
|
||||
parentRichTextFieldPath: path,
|
||||
parentRichTextFieldSchemaPath: schemaPath,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
previousNode: previousNodeIDMap[id]!,
|
||||
req,
|
||||
skipValidation: skipValidation!,
|
||||
@@ -557,10 +561,12 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
const nodeSiblingData = subFieldDataFn({ node, req }) ?? {}
|
||||
const nodeSiblingDocWithLocales =
|
||||
subFieldDataFn({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
node: originalNodeWithLocalesIDMap[id]!,
|
||||
req,
|
||||
}) ?? {}
|
||||
const nodePreviousSiblingDoc =
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
subFieldDataFn({ node: previousNodeIDMap[id]!, req }) ?? {}
|
||||
|
||||
if (subFields?.length) {
|
||||
@@ -756,6 +762,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
if (subFieldFn && subFieldDataFn) {
|
||||
const subFields = subFieldFn({ node, req })
|
||||
const nodeSiblingData = subFieldDataFn({ node, req }) ?? {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
const nodeSiblingDoc = subFieldDataFn({ node: originalNodeIDMap[id]!, req }) ?? {}
|
||||
|
||||
if (subFields?.length) {
|
||||
|
||||
@@ -75,6 +75,7 @@ async function migrateGlobal({
|
||||
const found = migrateDocument({
|
||||
document,
|
||||
fields: global.fields,
|
||||
payload,
|
||||
})
|
||||
|
||||
if (found) {
|
||||
@@ -159,6 +160,7 @@ async function migrateCollection({
|
||||
const found = migrateDocument({
|
||||
document,
|
||||
fields: collection.fields,
|
||||
payload,
|
||||
})
|
||||
|
||||
if (found) {
|
||||
@@ -189,13 +191,16 @@ async function migrateCollection({
|
||||
function migrateDocument({
|
||||
document,
|
||||
fields,
|
||||
payload,
|
||||
}: {
|
||||
document: Record<string, unknown>
|
||||
fields: Field[]
|
||||
payload: Payload
|
||||
}): boolean {
|
||||
return !!migrateDocumentFieldsRecursively({
|
||||
data: document,
|
||||
fields,
|
||||
found: 0,
|
||||
payload,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Field } from 'payload'
|
||||
import type { Field, FlattenedBlock, Payload } from 'payload'
|
||||
|
||||
import { fieldAffectsData, fieldHasSubFields, fieldIsArrayType, tabHasName } from 'payload/shared'
|
||||
|
||||
@@ -15,12 +15,14 @@ type NestedRichTextFieldsArgs = {
|
||||
|
||||
fields: Field[]
|
||||
found: number
|
||||
payload: Payload
|
||||
}
|
||||
|
||||
export const migrateDocumentFieldsRecursively = ({
|
||||
data,
|
||||
fields,
|
||||
found,
|
||||
payload,
|
||||
}: NestedRichTextFieldsArgs): number => {
|
||||
for (const field of fields) {
|
||||
if (fieldHasSubFields(field) && !fieldIsArrayType(field)) {
|
||||
@@ -29,12 +31,14 @@ export const migrateDocumentFieldsRecursively = ({
|
||||
data: data[field.name] as Record<string, unknown>,
|
||||
fields: field.fields,
|
||||
found,
|
||||
payload,
|
||||
})
|
||||
} else {
|
||||
found += migrateDocumentFieldsRecursively({
|
||||
data,
|
||||
fields: field.fields,
|
||||
found,
|
||||
payload,
|
||||
})
|
||||
}
|
||||
} else if (field.type === 'tabs') {
|
||||
@@ -43,17 +47,25 @@ export const migrateDocumentFieldsRecursively = ({
|
||||
data: (tabHasName(tab) ? data[tab.name] : data) as Record<string, unknown>,
|
||||
fields: tab.fields,
|
||||
found,
|
||||
payload,
|
||||
})
|
||||
})
|
||||
} else if (Array.isArray(data[field.name])) {
|
||||
if (field.type === 'blocks') {
|
||||
;(data[field.name] as Array<Record<string, unknown>>).forEach((row) => {
|
||||
const block = field.blocks.find(({ slug }) => slug === row?.blockType)
|
||||
const blockTypeToMatch: string = row?.blockType as string
|
||||
const block =
|
||||
payload?.blocks[blockTypeToMatch] ??
|
||||
((field.blockReferences ?? field.blocks).find(
|
||||
(block) => typeof block !== 'string' && block.slug === blockTypeToMatch,
|
||||
) as FlattenedBlock | undefined)
|
||||
|
||||
if (block) {
|
||||
found += migrateDocumentFieldsRecursively({
|
||||
data: row,
|
||||
fields: block.fields,
|
||||
found,
|
||||
payload,
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -65,6 +77,7 @@ export const migrateDocumentFieldsRecursively = ({
|
||||
data: row,
|
||||
fields: field.fields,
|
||||
found,
|
||||
payload,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ async function upgradeGlobal({
|
||||
const found = upgradeDocument({
|
||||
document,
|
||||
fields: global.fields,
|
||||
payload,
|
||||
})
|
||||
|
||||
if (found) {
|
||||
@@ -129,6 +130,7 @@ async function upgradeCollection({
|
||||
const found = upgradeDocument({
|
||||
document,
|
||||
fields: collection.fields,
|
||||
payload,
|
||||
})
|
||||
|
||||
if (found) {
|
||||
@@ -148,13 +150,16 @@ async function upgradeCollection({
|
||||
function upgradeDocument({
|
||||
document,
|
||||
fields,
|
||||
payload,
|
||||
}: {
|
||||
document: Record<string, unknown>
|
||||
fields: Field[]
|
||||
payload: Payload
|
||||
}): boolean {
|
||||
return !!upgradeDocumentFieldsRecursively({
|
||||
data: document,
|
||||
fields,
|
||||
found: 0,
|
||||
payload,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SerializedEditorState } from 'lexical'
|
||||
import type { Field } from 'payload'
|
||||
import type { Field, FlattenedBlock, Payload } from 'payload'
|
||||
|
||||
import { createHeadlessEditor } from '@lexical/headless'
|
||||
import { fieldAffectsData, fieldHasSubFields, fieldIsArrayType, tabHasName } from 'payload/shared'
|
||||
@@ -13,12 +13,14 @@ type NestedRichTextFieldsArgs = {
|
||||
|
||||
fields: Field[]
|
||||
found: number
|
||||
payload: Payload
|
||||
}
|
||||
|
||||
export const upgradeDocumentFieldsRecursively = ({
|
||||
data,
|
||||
fields,
|
||||
found,
|
||||
payload,
|
||||
}: NestedRichTextFieldsArgs): number => {
|
||||
for (const field of fields) {
|
||||
if (fieldHasSubFields(field) && !fieldIsArrayType(field)) {
|
||||
@@ -27,12 +29,14 @@ export const upgradeDocumentFieldsRecursively = ({
|
||||
data: data[field.name] as Record<string, unknown>,
|
||||
fields: field.fields,
|
||||
found,
|
||||
payload,
|
||||
})
|
||||
} else {
|
||||
found += upgradeDocumentFieldsRecursively({
|
||||
data,
|
||||
fields: field.fields,
|
||||
found,
|
||||
payload,
|
||||
})
|
||||
}
|
||||
} else if (field.type === 'tabs') {
|
||||
@@ -41,17 +45,26 @@ export const upgradeDocumentFieldsRecursively = ({
|
||||
data: (tabHasName(tab) ? data[tab.name] : data) as Record<string, unknown>,
|
||||
fields: tab.fields,
|
||||
found,
|
||||
payload,
|
||||
})
|
||||
})
|
||||
} else if (Array.isArray(data[field.name])) {
|
||||
if (field.type === 'blocks') {
|
||||
;(data[field.name] as Record<string, unknown>[]).forEach((row) => {
|
||||
const block = field.blocks.find(({ slug }) => slug === row?.blockType)
|
||||
const blockTypeToMatch: string = row?.blockType as string
|
||||
|
||||
const block =
|
||||
payload.blocks[blockTypeToMatch] ??
|
||||
((field.blockReferences ?? field.blocks).find(
|
||||
(block) => typeof block !== 'string' && block.slug === blockTypeToMatch,
|
||||
) as FlattenedBlock | undefined)
|
||||
|
||||
if (block) {
|
||||
found += upgradeDocumentFieldsRecursively({
|
||||
data: row,
|
||||
fields: block.fields,
|
||||
found,
|
||||
payload,
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -63,6 +76,7 @@ export const upgradeDocumentFieldsRecursively = ({
|
||||
data: row,
|
||||
fields: field.fields,
|
||||
found,
|
||||
payload,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Field, PayloadRequest, PopulateType } from 'payload'
|
||||
import type { Field, FlattenedBlock, PayloadRequest, PopulateType } from 'payload'
|
||||
|
||||
import { fieldAffectsData, fieldHasSubFields, fieldIsArrayType, tabHasName } from 'payload/shared'
|
||||
|
||||
@@ -172,7 +172,11 @@ export const recurseNestedFields = ({
|
||||
} else if (Array.isArray(data[field.name])) {
|
||||
if (field.type === 'blocks') {
|
||||
data[field.name].forEach((row, i) => {
|
||||
const block = field.blocks.find(({ slug }) => slug === row?.blockType)
|
||||
const block =
|
||||
req.payload.blocks[row?.blockType] ??
|
||||
((field.blockReferences ?? field.blocks).find(
|
||||
(block) => typeof block !== 'string' && block.slug === row?.blockType,
|
||||
) as FlattenedBlock | undefined)
|
||||
if (block) {
|
||||
recurseNestedFields({
|
||||
currentDepth,
|
||||
|
||||
@@ -4,19 +4,27 @@ import type { BlocksFieldClient, DefaultCellComponentProps } from 'payload'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React from 'react'
|
||||
|
||||
import { useConfig } from '../../../../../providers/Config/index.js'
|
||||
import { useTranslation } from '../../../../../providers/Translation/index.js'
|
||||
|
||||
export interface BlocksCellProps extends DefaultCellComponentProps<BlocksFieldClient> {}
|
||||
|
||||
export const BlocksCell: React.FC<BlocksCellProps> = ({ cellData, field: { blocks, labels } }) => {
|
||||
export const BlocksCell: React.FC<BlocksCellProps> = ({
|
||||
cellData,
|
||||
field: { blockReferences, blocks, labels },
|
||||
}) => {
|
||||
const { i18n } = useTranslation()
|
||||
const { config } = useConfig()
|
||||
|
||||
const selectedBlocks = Array.isArray(cellData) ? cellData.map(({ blockType }) => blockType) : []
|
||||
|
||||
const translatedBlockLabels = blocks?.map((b) => ({
|
||||
slug: b.slug,
|
||||
label: getTranslation(b.labels.singular, i18n),
|
||||
}))
|
||||
const translatedBlockLabels = (blockReferences ?? blocks)?.map((b) => {
|
||||
const block = typeof b === 'string' ? config.blocksMap[b] : b
|
||||
return {
|
||||
slug: block.slug,
|
||||
label: getTranslation(block.labels.singular, i18n),
|
||||
}
|
||||
})
|
||||
|
||||
let label = `0 ${getTranslation(labels?.plural, i18n)}`
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ const baseClass = 'blocks-field'
|
||||
type BlocksFieldProps = {
|
||||
addRow: (rowIndex: number, blockType: string) => Promise<void> | void
|
||||
block: ClientBlock
|
||||
blocks: ClientBlock[]
|
||||
blocks: (ClientBlock | string)[] | ClientBlock[]
|
||||
duplicateRow: (rowIndex: number) => void
|
||||
errorCount: number
|
||||
fields: ClientField[]
|
||||
|
||||
@@ -9,14 +9,15 @@ import React, { useEffect, useState } from 'react'
|
||||
import { Drawer } from '../../../elements/Drawer/index.js'
|
||||
import { ThumbnailCard } from '../../../elements/ThumbnailCard/index.js'
|
||||
import { DefaultBlockImage } from '../../../graphics/DefaultBlockImage/index.js'
|
||||
import { useConfig } from '../../../providers/Config/index.js'
|
||||
import { useTranslation } from '../../../providers/Translation/index.js'
|
||||
import { BlockSearch } from './BlockSearch/index.js'
|
||||
import './index.scss'
|
||||
import { BlockSearch } from './BlockSearch/index.js'
|
||||
|
||||
export type Props = {
|
||||
readonly addRow: (index: number, blockType?: string) => Promise<void> | void
|
||||
readonly addRowIndex: number
|
||||
readonly blocks: ClientBlock[]
|
||||
readonly blocks: (ClientBlock | string)[]
|
||||
readonly drawerSlug: string
|
||||
readonly labels: Labels
|
||||
}
|
||||
@@ -40,6 +41,7 @@ export const BlocksDrawer: React.FC<Props> = (props) => {
|
||||
const [filteredBlocks, setFilteredBlocks] = useState(blocks)
|
||||
const { closeModal, isModalOpen } = useModal()
|
||||
const { i18n, t } = useTranslation()
|
||||
const { config } = useConfig()
|
||||
|
||||
useEffect(() => {
|
||||
if (!isModalOpen(drawerSlug)) {
|
||||
@@ -50,7 +52,8 @@ export const BlocksDrawer: React.FC<Props> = (props) => {
|
||||
useEffect(() => {
|
||||
const searchTermToUse = searchTerm.toLowerCase()
|
||||
|
||||
const matchingBlocks = blocks?.reduce((matchedBlocks, block) => {
|
||||
const matchingBlocks = blocks?.reduce((matchedBlocks, _block) => {
|
||||
const block = typeof _block === 'string' ? config.blocksMap[_block] : _block
|
||||
const blockLabel = getBlockLabel(block, i18n)
|
||||
if (blockLabel.includes(searchTermToUse)) {
|
||||
matchedBlocks.push(block)
|
||||
@@ -59,7 +62,7 @@ export const BlocksDrawer: React.FC<Props> = (props) => {
|
||||
}, [])
|
||||
|
||||
setFilteredBlocks(matchingBlocks)
|
||||
}, [searchTerm, blocks, i18n])
|
||||
}, [searchTerm, blocks, i18n, config.blocksMap])
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
@@ -69,7 +72,9 @@ export const BlocksDrawer: React.FC<Props> = (props) => {
|
||||
<BlockSearch setSearchTerm={setSearchTerm} />
|
||||
<div className={`${baseClass}__blocks-wrapper`}>
|
||||
<ul className={`${baseClass}__blocks`}>
|
||||
{filteredBlocks?.map((block, index) => {
|
||||
{filteredBlocks?.map((_block, index) => {
|
||||
const block = typeof _block === 'string' ? config.blocksMap[_block] : _block
|
||||
|
||||
const { slug, imageAltText, imageURL, labels: blockLabels } = block
|
||||
|
||||
return (
|
||||
|
||||
@@ -10,7 +10,7 @@ import { BlocksDrawer } from './BlocksDrawer/index.js'
|
||||
|
||||
export const RowActions: React.FC<{
|
||||
readonly addRow: (rowIndex: number, blockType: string) => Promise<void> | void
|
||||
readonly blocks: ClientBlock[]
|
||||
readonly blocks: (ClientBlock | string)[]
|
||||
readonly blockType: string
|
||||
readonly duplicateRow: (rowIndex: number, blockType: string) => void
|
||||
readonly fields: ClientField[]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import type { BlocksFieldClientComponent } from 'payload'
|
||||
import type { BlocksFieldClientComponent, ClientBlock } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React, { Fragment, useCallback } from 'react'
|
||||
@@ -39,6 +39,7 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => {
|
||||
field: {
|
||||
name,
|
||||
admin: { className, description, isSortable = true } = {},
|
||||
blockReferences,
|
||||
blocks,
|
||||
label,
|
||||
labels: labelsFromProps,
|
||||
@@ -62,6 +63,7 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => {
|
||||
const { code: locale } = useLocale()
|
||||
const {
|
||||
config: { localization },
|
||||
config,
|
||||
} = useConfig()
|
||||
const drawerSlug = useDrawerSlug('blocks-drawer')
|
||||
const submitted = useFormSubmitted()
|
||||
@@ -260,7 +262,11 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => {
|
||||
>
|
||||
{rows.map((row, i) => {
|
||||
const { blockType, isLoading } = row
|
||||
const blockConfig = blocks.find((block) => block.slug === blockType)
|
||||
const blockConfig: ClientBlock =
|
||||
config.blocksMap[blockType] ??
|
||||
((blockReferences ?? blocks).find(
|
||||
(block) => typeof block !== 'string' && block.slug === blockType,
|
||||
) as ClientBlock)
|
||||
|
||||
if (blockConfig) {
|
||||
const rowPath = `${path}.${i}`
|
||||
@@ -276,7 +282,7 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => {
|
||||
{...draggableSortableItemProps}
|
||||
addRow={addRow}
|
||||
block={blockConfig}
|
||||
blocks={blocks}
|
||||
blocks={blockReferences ?? blocks}
|
||||
duplicateRow={duplicateRow}
|
||||
errorCount={rowErrorCount}
|
||||
fields={blockConfig.fields}
|
||||
@@ -346,7 +352,7 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => {
|
||||
<BlocksDrawer
|
||||
addRow={addRow}
|
||||
addRowIndex={rows?.length || 0}
|
||||
blocks={blocks}
|
||||
blocks={blockReferences ?? blocks}
|
||||
drawerSlug={drawerSlug}
|
||||
labels={labels}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import type {
|
||||
ClientConfig,
|
||||
ClientField,
|
||||
JoinFieldClient,
|
||||
JoinFieldClientComponent,
|
||||
@@ -30,11 +31,13 @@ const ObjectId = (ObjectIdImport.default ||
|
||||
*/
|
||||
const getInitialDrawerData = ({
|
||||
collectionSlug,
|
||||
config,
|
||||
docID,
|
||||
fields,
|
||||
segments,
|
||||
}: {
|
||||
collectionSlug: string
|
||||
config: ClientConfig
|
||||
docID: number | string
|
||||
fields: ClientField[]
|
||||
segments: string[]
|
||||
@@ -68,6 +71,7 @@ const getInitialDrawerData = ({
|
||||
return {
|
||||
[field.name]: getInitialDrawerData({
|
||||
collectionSlug,
|
||||
config,
|
||||
docID,
|
||||
fields: field.fields,
|
||||
segments: nextSegments,
|
||||
@@ -78,6 +82,7 @@ const getInitialDrawerData = ({
|
||||
if (field.type === 'array') {
|
||||
const initialData = getInitialDrawerData({
|
||||
collectionSlug,
|
||||
config,
|
||||
docID,
|
||||
fields: field.fields,
|
||||
segments: nextSegments,
|
||||
@@ -91,9 +96,12 @@ const getInitialDrawerData = ({
|
||||
}
|
||||
|
||||
if (field.type === 'blocks') {
|
||||
for (const block of field.blocks) {
|
||||
for (const _block of field.blockReferences ?? field.blocks) {
|
||||
const block = typeof _block === 'string' ? config.blocksMap[_block] : _block
|
||||
|
||||
const blockInitialData = getInitialDrawerData({
|
||||
collectionSlug,
|
||||
config,
|
||||
docID,
|
||||
fields: block.fields,
|
||||
segments: nextSegments,
|
||||
@@ -127,7 +135,7 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
|
||||
|
||||
const { id: docID, docConfig } = useDocumentInfo()
|
||||
|
||||
const { getEntityConfig } = useConfig()
|
||||
const { config, getEntityConfig } = useConfig()
|
||||
|
||||
const { customComponents: { AfterInput, BeforeInput, Description, Label } = {}, value } =
|
||||
useField<PaginatedDocs>({
|
||||
@@ -168,11 +176,12 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
|
||||
|
||||
return getInitialDrawerData({
|
||||
collectionSlug: docConfig?.slug,
|
||||
config,
|
||||
docID,
|
||||
fields: relatedCollection.fields,
|
||||
segments: field.on.split('.'),
|
||||
})
|
||||
}, [getEntityConfig, field.collection, field.on, docConfig?.slug, docID])
|
||||
}, [getEntityConfig, field.collection, field.on, docConfig?.slug, docID, config])
|
||||
|
||||
if (!docConfig) {
|
||||
return null
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
Field,
|
||||
FieldSchemaMap,
|
||||
FieldState,
|
||||
FlattenedBlock,
|
||||
FormState,
|
||||
FormStateWithoutComponents,
|
||||
PayloadRequest,
|
||||
@@ -371,7 +372,13 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
|
||||
const { promises, rowMetadata } = blocksValue.reduce(
|
||||
(acc, row, i: number) => {
|
||||
const block = field.blocks.find((blockType) => blockType.slug === row.blockType)
|
||||
const blockTypeToMatch: string = row.blockType
|
||||
const block =
|
||||
req.payload.blocks[blockTypeToMatch] ??
|
||||
((field.blockReferences ?? field.blocks).find(
|
||||
(blockType) => typeof blockType !== 'string' && blockType.slug === blockTypeToMatch,
|
||||
) as FlattenedBlock | undefined)
|
||||
|
||||
if (!block) {
|
||||
throw new Error(
|
||||
`Block with type "${row.blockType}" was found in block data, but no block with that type is defined in the config for field with schema path ${schemaPath}.`,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Data, Field, PayloadRequest, TabAsField, User } from 'payload'
|
||||
import type { Data, Field, FlattenedBlock, PayloadRequest, TabAsField, User } from 'payload'
|
||||
|
||||
import { getDefaultValue } from 'payload'
|
||||
import { fieldAffectsData, tabHasName } from 'payload/shared'
|
||||
@@ -80,8 +80,12 @@ export const defaultValuePromise = async <T>({
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row) => {
|
||||
const blockTypeToMatch = row.blockType
|
||||
const block = field.blocks.find((blockType) => blockType.slug === blockTypeToMatch)
|
||||
const blockTypeToMatch: string = row.blockType
|
||||
const block =
|
||||
req.payload.blocks[blockTypeToMatch] ??
|
||||
((field.blockReferences ?? field.blocks).find(
|
||||
(blockType) => typeof blockType !== 'string' && blockType.slug === blockTypeToMatch,
|
||||
) as FlattenedBlock | undefined)
|
||||
|
||||
if (block) {
|
||||
row.blockType = blockTypeToMatch
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import type { ClientComponentProps, ClientField, FieldPaths, ServerComponentProps } from 'payload'
|
||||
import type {
|
||||
ClientComponentProps,
|
||||
ClientField,
|
||||
FieldPaths,
|
||||
FlattenedBlock,
|
||||
ServerComponentProps,
|
||||
} from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { createClientField, MissingEditorProp } from 'payload'
|
||||
@@ -135,7 +141,12 @@ export const renderField: RenderFieldMethod = ({
|
||||
|
||||
case 'blocks': {
|
||||
fieldState?.rows?.forEach((row, rowIndex) => {
|
||||
const blockConfig = fieldConfig.blocks.find((block) => block.slug === row.blockType)
|
||||
const blockTypeToMatch: string = row.blockType
|
||||
const blockConfig =
|
||||
req.payload.blocks[blockTypeToMatch] ??
|
||||
((fieldConfig.blockReferences ?? fieldConfig.blocks).find(
|
||||
(block) => typeof block !== 'string' && block.slug === blockTypeToMatch,
|
||||
) as FlattenedBlock | undefined)
|
||||
|
||||
if (blockConfig.admin?.components && 'Label' in blockConfig.admin.components) {
|
||||
if (!fieldState.customComponents) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
ClientGlobalConfig,
|
||||
CollectionSlug,
|
||||
GlobalSlug,
|
||||
UnsanitizedClientConfig,
|
||||
} from 'payload'
|
||||
|
||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
||||
@@ -39,16 +40,34 @@ export type ClientConfigContext = {
|
||||
|
||||
const RootConfigContext = createContext<ClientConfigContext | undefined>(undefined)
|
||||
|
||||
function sanitizeClientConfig(
|
||||
unSanitizedConfig: ClientConfig | UnsanitizedClientConfig,
|
||||
): ClientConfig {
|
||||
if (!unSanitizedConfig?.blocks?.length || (unSanitizedConfig as ClientConfig).blocksMap) {
|
||||
;(unSanitizedConfig as ClientConfig).blocksMap = {}
|
||||
return unSanitizedConfig as ClientConfig
|
||||
}
|
||||
const sanitizedConfig: ClientConfig = { ...unSanitizedConfig } as ClientConfig
|
||||
|
||||
sanitizedConfig.blocksMap = {}
|
||||
|
||||
for (const block of unSanitizedConfig.blocks) {
|
||||
sanitizedConfig.blocksMap[block.slug] = block
|
||||
}
|
||||
|
||||
return sanitizedConfig
|
||||
}
|
||||
|
||||
export const ConfigProvider: React.FC<{
|
||||
readonly children: React.ReactNode
|
||||
readonly config: ClientConfig
|
||||
readonly config: ClientConfig | UnsanitizedClientConfig
|
||||
}> = ({ children, config: configFromProps }) => {
|
||||
const [config, setConfig] = useState<ClientConfig>(configFromProps)
|
||||
const [config, setConfig] = useState<ClientConfig>(() => sanitizeClientConfig(configFromProps))
|
||||
|
||||
// Need to update local config state if config from props changes, for HMR.
|
||||
// That way, config changes will be updated in the UI immediately without needing a refresh.
|
||||
useEffect(() => {
|
||||
setConfig(configFromProps)
|
||||
setConfig(sanitizeClientConfig(configFromProps))
|
||||
}, [configFromProps])
|
||||
|
||||
// Build lookup maps for collections and globals so we can do O(1) lookups by slug
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
|
||||
import {
|
||||
type ClientBlock,
|
||||
type ClientConfig,
|
||||
type ClientField,
|
||||
type ClientFieldSchemaMap,
|
||||
@@ -60,7 +61,14 @@ export const traverseFields = ({
|
||||
break
|
||||
|
||||
case 'blocks':
|
||||
field.blocks.map((block) => {
|
||||
;(field.blockReferences ?? field.blocks).map((_block) => {
|
||||
const block =
|
||||
typeof _block === 'string'
|
||||
? config.blocksMap
|
||||
? config.blocksMap[_block]
|
||||
: config.blocks.find((block) => typeof block !== 'string' && block.slug === _block)
|
||||
: _block
|
||||
|
||||
const blockSchemaPath = `${schemaPath}.${block.slug}`
|
||||
|
||||
clientSchemaMap.set(blockSchemaPath, block)
|
||||
|
||||
@@ -47,7 +47,11 @@ export const traverseFields = ({
|
||||
break
|
||||
|
||||
case 'blocks':
|
||||
field.blocks.map((block) => {
|
||||
;(field.blockReferences ?? field.blocks).map((_block) => {
|
||||
// TODO: iterate over blocks mapped to block slug in v4, or pass through payload.blocks
|
||||
const block =
|
||||
typeof _block === 'string' ? config.blocks.find((b) => b.slug === _block) : _block
|
||||
|
||||
const blockSchemaPath = `${schemaPath}.${block.slug}`
|
||||
|
||||
schemaMap.set(blockSchemaPath, block)
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
type CollectionSlug,
|
||||
type Data,
|
||||
type Field,
|
||||
type FlattenedBlock,
|
||||
formatErrors,
|
||||
type PayloadRequest,
|
||||
} from 'payload'
|
||||
@@ -21,7 +22,12 @@ export type CopyDataFromLocaleArgs = {
|
||||
toLocale: string
|
||||
}
|
||||
|
||||
function iterateFields(fields: Field[], fromLocaleData: Data, toLocaleData: Data): void {
|
||||
function iterateFields(
|
||||
fields: Field[],
|
||||
fromLocaleData: Data,
|
||||
toLocaleData: Data,
|
||||
req: PayloadRequest,
|
||||
): void {
|
||||
fields.map((field) => {
|
||||
if (fieldAffectsData(field)) {
|
||||
switch (field.type) {
|
||||
@@ -46,7 +52,7 @@ function iterateFields(fields: Field[], fromLocaleData: Data, toLocaleData: Data
|
||||
toLocaleData[field.name][index].id = new ObjectId().toHexString()
|
||||
}
|
||||
|
||||
iterateFields(field.fields, fromLocaleData[field.name][index], item)
|
||||
iterateFields(field.fields, fromLocaleData[field.name][index], item, req)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -67,17 +73,19 @@ function iterateFields(fields: Field[], fromLocaleData: Data, toLocaleData: Data
|
||||
// if the field has a value - loop over the data from target
|
||||
if (field.name in toLocaleData) {
|
||||
toLocaleData[field.name].map((blockData: Data, index: number) => {
|
||||
const blockFields = field.blocks.find(
|
||||
({ slug }) => slug === blockData.blockType,
|
||||
)?.fields
|
||||
const block =
|
||||
req.payload.blocks[blockData.blockType] ??
|
||||
((field.blockReferences ?? field.blocks).find(
|
||||
(block) => typeof block !== 'string' && block.slug === blockData.blockType,
|
||||
) as FlattenedBlock | undefined)
|
||||
|
||||
// Generate new IDs if the field is localized to prevent errors with relational DBs.
|
||||
if (field.localized) {
|
||||
toLocaleData[field.name][index].id = new ObjectId().toHexString()
|
||||
}
|
||||
|
||||
if (blockFields?.length) {
|
||||
iterateFields(blockFields, fromLocaleData[field.name][index], blockData)
|
||||
if (block?.fields?.length) {
|
||||
iterateFields(block?.fields, fromLocaleData[field.name][index], blockData, req)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -110,7 +118,7 @@ function iterateFields(fields: Field[], fromLocaleData: Data, toLocaleData: Data
|
||||
|
||||
case 'group': {
|
||||
if (field.name in toLocaleData && fromLocaleData?.[field.name] !== undefined) {
|
||||
iterateFields(field.fields, fromLocaleData[field.name], toLocaleData[field.name])
|
||||
iterateFields(field.fields, fromLocaleData[field.name], toLocaleData[field.name], req)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -119,17 +127,17 @@ function iterateFields(fields: Field[], fromLocaleData: Data, toLocaleData: Data
|
||||
switch (field.type) {
|
||||
case 'collapsible':
|
||||
case 'row':
|
||||
iterateFields(field.fields, fromLocaleData, toLocaleData)
|
||||
iterateFields(field.fields, fromLocaleData, toLocaleData, req)
|
||||
break
|
||||
|
||||
case 'tabs':
|
||||
field.tabs.map((tab) => {
|
||||
if (tabHasName(tab)) {
|
||||
if (tab.name in toLocaleData && fromLocaleData?.[tab.name] !== undefined) {
|
||||
iterateFields(tab.fields, fromLocaleData[tab.name], toLocaleData[tab.name])
|
||||
iterateFields(tab.fields, fromLocaleData[tab.name], toLocaleData[tab.name], req)
|
||||
}
|
||||
} else {
|
||||
iterateFields(tab.fields, fromLocaleData, toLocaleData)
|
||||
iterateFields(tab.fields, fromLocaleData, toLocaleData, req)
|
||||
}
|
||||
})
|
||||
break
|
||||
@@ -138,8 +146,13 @@ function iterateFields(fields: Field[], fromLocaleData: Data, toLocaleData: Data
|
||||
})
|
||||
}
|
||||
|
||||
function mergeData(fromLocaleData: Data, toLocaleData: Data, fields: Field[]): Data {
|
||||
iterateFields(fields, fromLocaleData, toLocaleData)
|
||||
function mergeData(
|
||||
fromLocaleData: Data,
|
||||
toLocaleData: Data,
|
||||
fields: Field[],
|
||||
req: PayloadRequest,
|
||||
): Data {
|
||||
iterateFields(fields, fromLocaleData, toLocaleData, req)
|
||||
|
||||
return toLocaleData
|
||||
}
|
||||
@@ -254,7 +267,12 @@ export const copyDataFromLocale = async (args: CopyDataFromLocaleArgs) => {
|
||||
slug: globalSlug,
|
||||
data: overrideData
|
||||
? fromLocaleData.value
|
||||
: mergeData(fromLocaleData.value, toLocaleData.value, globals[globalSlug].config.fields),
|
||||
: mergeData(
|
||||
fromLocaleData.value,
|
||||
toLocaleData.value,
|
||||
globals[globalSlug].config.fields,
|
||||
req,
|
||||
),
|
||||
locale: toLocale,
|
||||
overrideAccess: false,
|
||||
req,
|
||||
@@ -269,6 +287,7 @@ export const copyDataFromLocale = async (args: CopyDataFromLocaleArgs) => {
|
||||
fromLocaleData.value,
|
||||
toLocaleData.value,
|
||||
collections[collectionSlug].config.fields,
|
||||
req,
|
||||
),
|
||||
locale: toLocale,
|
||||
overrideAccess: false,
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ClientConfig, ImportMap, SanitizedConfig } from 'payload'
|
||||
import { createClientConfig } from 'payload'
|
||||
import { cache } from 'react'
|
||||
|
||||
let cachedClientConfig = global._payload_clientConfig
|
||||
let cachedClientConfig: ClientConfig | null = global._payload_clientConfig
|
||||
|
||||
if (!cachedClientConfig) {
|
||||
cachedClientConfig = global._payload_clientConfig = null
|
||||
|
||||
@@ -22,7 +22,4 @@ export const PostsCollection: CollectionConfig = {
|
||||
}),
|
||||
},
|
||||
],
|
||||
versions: {
|
||||
drafts: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
posts: Post;
|
||||
media: Media;
|
||||
@@ -82,7 +83,7 @@ export interface Config {
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
defaultIDType: number;
|
||||
};
|
||||
globals: {
|
||||
menu: Menu;
|
||||
@@ -122,7 +123,7 @@ export interface UserAuthOperations {
|
||||
* via the `definition` "posts".
|
||||
*/
|
||||
export interface Post {
|
||||
id: string;
|
||||
id: number;
|
||||
title?: string | null;
|
||||
content?: {
|
||||
root: {
|
||||
@@ -141,14 +142,13 @@ export interface Post {
|
||||
} | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "media".
|
||||
*/
|
||||
export interface Media {
|
||||
id: string;
|
||||
id: number;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
@@ -192,7 +192,7 @@ export interface Media {
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
id: number;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
@@ -209,24 +209,24 @@ export interface User {
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
id: number;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'media';
|
||||
value: string | Media;
|
||||
value: number | Media;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
value: number | User;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
value: number | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -236,10 +236,10 @@ export interface PayloadLockedDocument {
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string;
|
||||
id: number;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
value: number | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
@@ -259,7 +259,7 @@ export interface PayloadPreference {
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string;
|
||||
id: number;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
@@ -274,7 +274,6 @@ export interface PostsSelect<T extends boolean = true> {
|
||||
content?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
@@ -379,7 +378,7 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
* via the `definition` "menu".
|
||||
*/
|
||||
export interface Menu {
|
||||
id: string;
|
||||
id: number;
|
||||
globalText?: string | null;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
|
||||
@@ -64,6 +64,7 @@ export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
uploads: Upload;
|
||||
posts: Post;
|
||||
|
||||
2
test/benchmark-blocks/.gitignore
vendored
Normal file
2
test/benchmark-blocks/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/media
|
||||
/media-gif
|
||||
61
test/benchmark-blocks/blocks/blocks.ts
Normal file
61
test/benchmark-blocks/blocks/blocks.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { Block, BlocksField, BlockSlug } from 'payload'
|
||||
|
||||
export const generateBlocks = (
|
||||
blockCount: number,
|
||||
useReferences?: boolean,
|
||||
): (Block | BlockSlug)[] => {
|
||||
const blocks: (Block | BlockSlug)[] = []
|
||||
|
||||
for (let i = 0; i < blockCount; i++) {
|
||||
if (useReferences) {
|
||||
blocks.push(`block_${i}` as BlockSlug)
|
||||
} else {
|
||||
blocks.push({
|
||||
slug: `block_${i}`,
|
||||
fields: [
|
||||
{
|
||||
name: 'field1',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'field2',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'field3',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'field4',
|
||||
type: 'number',
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return blocks
|
||||
}
|
||||
export const generateBlockFields = (
|
||||
blockCount: number,
|
||||
containerCount: number,
|
||||
useReferences?: boolean,
|
||||
): BlocksField[] => {
|
||||
const fields: BlocksField[] = []
|
||||
for (let i = 0; i < containerCount; i++) {
|
||||
const block: BlocksField = {
|
||||
name: `blocksfield_${i}`,
|
||||
type: 'blocks',
|
||||
blocks: [],
|
||||
}
|
||||
|
||||
if (useReferences) {
|
||||
block.blockReferences = generateBlocks(blockCount, true)
|
||||
} else {
|
||||
block.blocks = generateBlocks(blockCount) as any
|
||||
}
|
||||
fields.push(block)
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
33
test/benchmark-blocks/collections/Media/index.ts
Normal file
33
test/benchmark-blocks/collections/Media/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const mediaSlug = 'media'
|
||||
|
||||
export const MediaCollection: CollectionConfig = {
|
||||
slug: mediaSlug,
|
||||
access: {
|
||||
create: () => true,
|
||||
read: () => true,
|
||||
},
|
||||
fields: [],
|
||||
upload: {
|
||||
crop: true,
|
||||
focalPoint: true,
|
||||
imageSizes: [
|
||||
{
|
||||
name: 'thumbnail',
|
||||
height: 200,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
name: 'medium',
|
||||
height: 800,
|
||||
width: 800,
|
||||
},
|
||||
{
|
||||
name: 'large',
|
||||
height: 1200,
|
||||
width: 1200,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
28
test/benchmark-blocks/collections/Posts/index.ts
Normal file
28
test/benchmark-blocks/collections/Posts/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const postsSlug = 'posts'
|
||||
|
||||
export const PostsCollection: CollectionConfig = {
|
||||
slug: postsSlug,
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [...defaultFeatures],
|
||||
}),
|
||||
},
|
||||
],
|
||||
versions: {
|
||||
drafts: true,
|
||||
},
|
||||
}
|
||||
56
test/benchmark-blocks/config.ts
Normal file
56
test/benchmark-blocks/config.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { devUser } from '../credentials.js'
|
||||
import { generateBlockFields, generateBlocks } from './blocks/blocks.js'
|
||||
import { MediaCollection } from './collections/Media/index.js'
|
||||
import { PostsCollection, postsSlug } from './collections/Posts/index.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
const USE_BLOCK_REFERENCES = true
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
collections: [
|
||||
PostsCollection,
|
||||
{
|
||||
slug: 'pages',
|
||||
access: {
|
||||
create: () => true,
|
||||
read: () => true,
|
||||
},
|
||||
fields: generateBlockFields(40, 30 * 20, USE_BLOCK_REFERENCES),
|
||||
},
|
||||
MediaCollection,
|
||||
],
|
||||
admin: {
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
editor: lexicalEditor({}),
|
||||
// @ts-expect-error
|
||||
blocks: USE_BLOCK_REFERENCES ? generateBlocks(30 * 20, false) : undefined,
|
||||
onInit: async (payload) => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: postsSlug,
|
||||
data: {
|
||||
title: 'example post',
|
||||
},
|
||||
})
|
||||
},
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
})
|
||||
19
test/benchmark-blocks/eslint.config.js
Normal file
19
test/benchmark-blocks/eslint.config.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { rootParserOptions } from '../../eslint.config.js'
|
||||
import { testEslintConfig } from '../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.Config} Config */
|
||||
|
||||
/** @type {Config[]} */
|
||||
export const index = [
|
||||
...testEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
...rootParserOptions,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
35810
test/benchmark-blocks/payload-types.ts
Normal file
35810
test/benchmark-blocks/payload-types.ts
Normal file
File diff suppressed because it is too large
Load Diff
1902
test/benchmark-blocks/schema.graphql
Normal file
1902
test/benchmark-blocks/schema.graphql
Normal file
File diff suppressed because it is too large
Load Diff
13
test/benchmark-blocks/tsconfig.eslint.json
Normal file
13
test/benchmark-blocks/tsconfig.eslint.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
// extend your base config to share compilerOptions, etc
|
||||
//"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
// ensure that nobody can accidentally use this config for a build
|
||||
"noEmit": true
|
||||
},
|
||||
"include": [
|
||||
// whatever paths you intend to lint
|
||||
"./**/*.ts",
|
||||
"./**/*.tsx"
|
||||
]
|
||||
}
|
||||
3
test/benchmark-blocks/tsconfig.json
Normal file
3
test/benchmark-blocks/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
}
|
||||
9
test/benchmark-blocks/types.d.ts
vendored
Normal file
9
test/benchmark-blocks/types.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { RequestContext as OriginalRequestContext } from 'payload'
|
||||
|
||||
declare module 'payload' {
|
||||
// Create a new interface that merges your additional fields with the original one
|
||||
export interface RequestContext extends OriginalRequestContext {
|
||||
myObject?: string
|
||||
// ...
|
||||
}
|
||||
}
|
||||
12
test/dev.ts
12
test/dev.ts
@@ -35,10 +35,18 @@ const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
const {
|
||||
_: [testSuiteArg = '_community'],
|
||||
_: [_testSuiteArg = '_community'],
|
||||
...args
|
||||
} = minimist(process.argv.slice(2))
|
||||
|
||||
let testSuiteArg: string | undefined
|
||||
let testSuiteConfigOverride: string | undefined
|
||||
if (_testSuiteArg.includes('#')) {
|
||||
;[testSuiteArg, testSuiteConfigOverride] = _testSuiteArg.split('#')
|
||||
} else {
|
||||
testSuiteArg = _testSuiteArg
|
||||
}
|
||||
|
||||
if (!testSuiteArg || !fs.existsSync(path.resolve(dirname, testSuiteArg))) {
|
||||
console.log(chalk.red(`ERROR: The test folder "${testSuiteArg}" does not exist`))
|
||||
process.exit(0)
|
||||
@@ -50,7 +58,7 @@ if (args.turbo === true) {
|
||||
process.env.TURBOPACK = '1'
|
||||
}
|
||||
|
||||
const { beforeTest } = await createTestHooks(testSuiteArg)
|
||||
const { beforeTest } = await createTestHooks(testSuiteArg, testSuiteConfigOverride)
|
||||
await beforeTest()
|
||||
|
||||
const { rootDir, adminRoute } = getNextRootDir(testSuiteArg)
|
||||
|
||||
171
test/fields/baseConfig.ts
Normal file
171
test/fields/baseConfig.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import type { CollectionConfig, Config } from 'payload'
|
||||
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
import ArrayFields from './collections/Array/index.js'
|
||||
import BlockFields from './collections/Blocks/index.js'
|
||||
import CheckboxFields from './collections/Checkbox/index.js'
|
||||
import CodeFields from './collections/Code/index.js'
|
||||
import CollapsibleFields from './collections/Collapsible/index.js'
|
||||
import ConditionalLogic from './collections/ConditionalLogic/index.js'
|
||||
import { CustomRowID } from './collections/CustomID/CustomRowID.js'
|
||||
import { CustomTabID } from './collections/CustomID/CustomTabID.js'
|
||||
import { CustomID } from './collections/CustomID/index.js'
|
||||
import DateFields from './collections/Date/index.js'
|
||||
import EmailFields from './collections/Email/index.js'
|
||||
import GroupFields from './collections/Group/index.js'
|
||||
import IndexedFields from './collections/Indexed/index.js'
|
||||
import JSONFields from './collections/JSON/index.js'
|
||||
import {
|
||||
getLexicalFieldsCollection,
|
||||
lexicalBlocks,
|
||||
lexicalInlineBlocks,
|
||||
} from './collections/Lexical/index.js'
|
||||
import { LexicalAccessControl } from './collections/LexicalAccessControl/index.js'
|
||||
import { LexicalInBlock } from './collections/LexicalInBlock/index.js'
|
||||
import { LexicalLocalizedFields } from './collections/LexicalLocalized/index.js'
|
||||
import { LexicalMigrateFields } from './collections/LexicalMigrate/index.js'
|
||||
import { LexicalObjectReferenceBugCollection } from './collections/LexicalObjectReferenceBug/index.js'
|
||||
import { LexicalRelationshipsFields } from './collections/LexicalRelationships/index.js'
|
||||
import NumberFields from './collections/Number/index.js'
|
||||
import PointFields from './collections/Point/index.js'
|
||||
import RadioFields from './collections/Radio/index.js'
|
||||
import RelationshipFields from './collections/Relationship/index.js'
|
||||
import RichTextFields from './collections/RichText/index.js'
|
||||
import RowFields from './collections/Row/index.js'
|
||||
import SelectFields from './collections/Select/index.js'
|
||||
import SelectVersionsFields from './collections/SelectVersions/index.js'
|
||||
import TabsFields from './collections/Tabs/index.js'
|
||||
import { TabsFields2 } from './collections/Tabs2/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 UploadsMulti from './collections/UploadMulti/index.js'
|
||||
import UploadsMultiPoly from './collections/UploadMultiPoly/index.js'
|
||||
import UploadsPoly from './collections/UploadPoly/index.js'
|
||||
import UploadRestricted from './collections/UploadRestricted/index.js'
|
||||
import Uploads3 from './collections/Uploads3/index.js'
|
||||
import TabsWithRichText from './globals/TabsWithRichText.js'
|
||||
import { clearAndSeedEverything } from './seed.js'
|
||||
|
||||
export const collectionSlugs: CollectionConfig[] = [
|
||||
getLexicalFieldsCollection({
|
||||
blocks: lexicalBlocks,
|
||||
inlineBlocks: lexicalInlineBlocks,
|
||||
}),
|
||||
LexicalMigrateFields,
|
||||
LexicalLocalizedFields,
|
||||
LexicalObjectReferenceBugCollection,
|
||||
{
|
||||
slug: 'users',
|
||||
admin: {
|
||||
useAsTitle: 'email',
|
||||
},
|
||||
auth: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'canViewConditionalField',
|
||||
type: 'checkbox',
|
||||
defaultValue: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
LexicalInBlock,
|
||||
LexicalAccessControl,
|
||||
SelectVersionsFields,
|
||||
ArrayFields,
|
||||
BlockFields,
|
||||
CheckboxFields,
|
||||
CodeFields,
|
||||
CollapsibleFields,
|
||||
ConditionalLogic,
|
||||
CustomID,
|
||||
CustomTabID,
|
||||
CustomRowID,
|
||||
DateFields,
|
||||
EmailFields,
|
||||
RadioFields,
|
||||
GroupFields,
|
||||
RowFields,
|
||||
IndexedFields,
|
||||
JSONFields,
|
||||
NumberFields,
|
||||
PointFields,
|
||||
RelationshipFields,
|
||||
LexicalRelationshipsFields,
|
||||
RichTextFields,
|
||||
SelectFields,
|
||||
TabsFields2,
|
||||
TabsFields,
|
||||
TextFields,
|
||||
Uploads,
|
||||
Uploads2,
|
||||
Uploads3,
|
||||
UploadsMulti,
|
||||
UploadsPoly,
|
||||
UploadsMultiPoly,
|
||||
UploadRestricted,
|
||||
UIFields,
|
||||
]
|
||||
|
||||
export const baseConfig: Partial<Config> = {
|
||||
collections: collectionSlugs,
|
||||
globals: [TabsWithRichText],
|
||||
blocks: [
|
||||
{
|
||||
slug: 'ConfigBlockTest',
|
||||
fields: [
|
||||
{
|
||||
name: 'deduplicatedText',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
custom: {
|
||||
client: {
|
||||
'new-value': 'client available',
|
||||
},
|
||||
server: {
|
||||
'new-server-value': 'only available on server',
|
||||
},
|
||||
},
|
||||
admin: {
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
components: {
|
||||
afterNavLinks: ['/components/AfterNavLinks.js#AfterNavLinks'],
|
||||
},
|
||||
custom: {
|
||||
client: {
|
||||
'new-value': 'client available',
|
||||
},
|
||||
},
|
||||
timezones: {
|
||||
supportedTimezones: ({ defaultTimezones }) => [
|
||||
...defaultTimezones,
|
||||
{ label: '(GMT-6) Monterrey, Nuevo Leon', value: 'America/Monterrey' },
|
||||
],
|
||||
defaultTimezone: 'America/Monterrey',
|
||||
},
|
||||
},
|
||||
localization: {
|
||||
defaultLocale: 'en',
|
||||
fallback: true,
|
||||
locales: ['en', 'es'],
|
||||
},
|
||||
onInit: async (payload) => {
|
||||
if (process.env.SEED_IN_CONFIG_ONINIT !== 'false') {
|
||||
await clearAndSeedEverything(payload)
|
||||
}
|
||||
},
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
}
|
||||
@@ -196,7 +196,9 @@ describe('Block fields', () => {
|
||||
// ensure the block was appended to the rows
|
||||
const firstRow = page.locator('#field-i18nBlocks .blocks-field__row').first()
|
||||
await expect(firstRow).toBeVisible()
|
||||
await expect(firstRow.locator('.blocks-field__block-pill-text')).toContainText('Text en')
|
||||
await expect(firstRow.locator('.blocks-field__block-pill-textInI18nBlock')).toContainText(
|
||||
'Text en',
|
||||
)
|
||||
})
|
||||
|
||||
test('should render custom block row label', async () => {
|
||||
|
||||
@@ -53,7 +53,7 @@ export const getBlocksField = (prefix?: string): BlocksField => ({
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'text',
|
||||
slug: 'textRequired',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
@@ -64,6 +64,7 @@ export const getBlocksField = (prefix?: string): BlocksField => ({
|
||||
},
|
||||
{
|
||||
slug: 'number',
|
||||
interfaceName: 'NumberBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'number',
|
||||
@@ -155,7 +156,7 @@ const BlockFields: CollectionConfig = {
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'text',
|
||||
slug: 'textInI18nBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
@@ -313,7 +314,7 @@ const BlockFields: CollectionConfig = {
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'block',
|
||||
slug: 'blockWithMinRows',
|
||||
fields: [
|
||||
{
|
||||
name: 'blockTitle',
|
||||
@@ -396,6 +397,18 @@ const BlockFields: CollectionConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'deduplicatedBlocks',
|
||||
type: 'blocks',
|
||||
blockReferences: ['ConfigBlockTest'],
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
name: 'deduplicatedBlocks2',
|
||||
type: 'blocks',
|
||||
blockReferences: ['ConfigBlockTest'],
|
||||
blocks: [],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export const getBlocksFieldSeedData = (prefix?: string): any => [
|
||||
},
|
||||
{
|
||||
blockName: 'Second sub block',
|
||||
blockType: 'text',
|
||||
blockType: 'textRequired',
|
||||
text: 'second sub block',
|
||||
},
|
||||
],
|
||||
@@ -40,11 +40,11 @@ export const blocksDoc: Partial<BlockField> = {
|
||||
blocksWithMinRows: [
|
||||
{
|
||||
blockTitle: 'first row',
|
||||
blockType: 'block',
|
||||
blockType: 'blockWithMinRows',
|
||||
},
|
||||
{
|
||||
blockTitle: 'second row',
|
||||
blockType: 'block',
|
||||
blockType: 'blockWithMinRows',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ export const TextBlock: Block = {
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
slug: 'text',
|
||||
slug: 'textRequired',
|
||||
}
|
||||
|
||||
export const RadioButtonsBlock: Block = {
|
||||
@@ -371,10 +371,10 @@ export const SelectFieldBlock: Block = {
|
||||
}
|
||||
|
||||
export const SubBlockBlock: Block = {
|
||||
slug: 'subBlock',
|
||||
slug: 'subBlockLexical',
|
||||
fields: [
|
||||
{
|
||||
name: 'subBlocks',
|
||||
name: 'subBlocksLexical',
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
|
||||
@@ -24,13 +24,13 @@ import {
|
||||
saveDocAndAssert,
|
||||
} from '../../../../../helpers.js'
|
||||
import { AdminUrlUtil } from '../../../../../helpers/adminUrlUtil.js'
|
||||
import { trackNetworkRequests } from '../../../../../helpers/e2e/trackNetworkRequests.js'
|
||||
import { initPayloadE2ENoConfig } from '../../../../../helpers/initPayloadE2ENoConfig.js'
|
||||
import { reInitializeDB } from '../../../../../helpers/reInitializeDB.js'
|
||||
import { RESTClient } from '../../../../../helpers/rest.js'
|
||||
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../../../../../playwright.config.js'
|
||||
import { lexicalFieldsSlug } from '../../../../slugs.js'
|
||||
import { lexicalDocData } from '../../data.js'
|
||||
import { trackNetworkRequests } from '../../../../../helpers/e2e/trackNetworkRequests.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const currentFolder = path.dirname(filename)
|
||||
@@ -106,7 +106,7 @@ async function createInlineBlock({
|
||||
*/
|
||||
await page.keyboard.press(' ')
|
||||
await page.keyboard.press('/')
|
||||
await page.keyboard.type(name.includes(' ') ? name.split(' ')[0] : name)
|
||||
await page.keyboard.type(name.includes(' ') ? (name.split(' ')?.[0] ?? name) : name)
|
||||
|
||||
// Create Rich Text Block
|
||||
const slashMenuPopover = page.locator('#slash-menu .slash-menu-popup')
|
||||
@@ -163,8 +163,8 @@ async function createInlineBlock({
|
||||
}
|
||||
|
||||
async function assertLexicalDoc({
|
||||
fn,
|
||||
depth = 0,
|
||||
fn,
|
||||
}: {
|
||||
depth?: number
|
||||
fn: (args: {
|
||||
@@ -299,7 +299,7 @@ describe('lexicalBlocks', () => {
|
||||
|
||||
// Check if the API result is correct
|
||||
await assertLexicalDoc({
|
||||
fn: async ({ lexicalWithBlocks }) => {
|
||||
fn: ({ lexicalWithBlocks }) => {
|
||||
const rscBlock: SerializedBlockNode = lexicalWithBlocks.root
|
||||
.children[14] as SerializedBlockNode
|
||||
const paragraphNode: SerializedParagraphNode = lexicalWithBlocks.root
|
||||
@@ -360,13 +360,15 @@ describe('lexicalBlocks', () => {
|
||||
const dependsOnSiblingData = newBlock.locator('#field-group__dependsOnSiblingData').first()
|
||||
const dependsOnBlockData = newBlock.locator('#field-group__dependsOnBlockData').first()
|
||||
|
||||
await expect(page.locator('.payload-toast-container .payload-toast-item')).toBeHidden()
|
||||
|
||||
return {
|
||||
topLevelDocTextField,
|
||||
blockTextField,
|
||||
blockGroupTextField,
|
||||
blockTextField,
|
||||
dependsOnDocData,
|
||||
dependsOnSiblingData,
|
||||
dependsOnBlockData,
|
||||
dependsOnSiblingData,
|
||||
topLevelDocTextField,
|
||||
newBlock,
|
||||
}
|
||||
}
|
||||
@@ -549,14 +551,15 @@ describe('lexicalBlocks', () => {
|
||||
.locator('#field-group__textDependsOnSiblingData')
|
||||
.first()
|
||||
const dependsOnBlockData = newBlock.locator('#field-group__textDependsOnBlockData').first()
|
||||
await expect(page.locator('.payload-toast-container .payload-toast-item')).toBeHidden()
|
||||
|
||||
return {
|
||||
topLevelDocTextField,
|
||||
blockTextField,
|
||||
blockGroupTextField,
|
||||
blockTextField,
|
||||
dependsOnDocData,
|
||||
dependsOnSiblingData,
|
||||
dependsOnBlockData,
|
||||
topLevelDocTextField,
|
||||
newBlock,
|
||||
}
|
||||
}
|
||||
@@ -570,7 +573,7 @@ describe('lexicalBlocks', () => {
|
||||
await expect(page.locator('.payload-toast-container')).toHaveText(
|
||||
'The following fields are invalid: Lexical With Blocks, LexicalWithBlocks > Group > Text Depends On Doc Data',
|
||||
)
|
||||
await expect(page.locator('.payload-toast-container')).not.toBeVisible()
|
||||
await expect(page.locator('.payload-toast-container .payload-toast-item')).toBeHidden()
|
||||
|
||||
await trackNetworkRequests(
|
||||
page,
|
||||
@@ -593,7 +596,7 @@ describe('lexicalBlocks', () => {
|
||||
await expect(page.locator('.payload-toast-container')).toHaveText(
|
||||
'The following fields are invalid: Lexical With Blocks, LexicalWithBlocks > Group > Text Depends On Sibling Data',
|
||||
)
|
||||
await expect(page.locator('.payload-toast-container')).not.toBeVisible()
|
||||
await expect(page.locator('.payload-toast-container .payload-toast-item')).toBeHidden()
|
||||
|
||||
await trackNetworkRequests(
|
||||
page,
|
||||
@@ -617,7 +620,7 @@ describe('lexicalBlocks', () => {
|
||||
'The following fields are invalid: Lexical With Blocks, LexicalWithBlocks > Group > Text Depends On Block Data',
|
||||
)
|
||||
|
||||
await expect(page.locator('.payload-toast-container')).not.toBeVisible()
|
||||
await expect(page.locator('.payload-toast-container .payload-toast-item')).toBeHidden()
|
||||
|
||||
await trackNetworkRequests(
|
||||
page,
|
||||
@@ -701,7 +704,7 @@ describe('lexicalBlocks', () => {
|
||||
await saveDocAndAssert(page)
|
||||
|
||||
await assertLexicalDoc({
|
||||
fn: async ({ lexicalWithBlocks }) => {
|
||||
fn: ({ lexicalWithBlocks }) => {
|
||||
const blockNode: SerializedBlockNode = lexicalWithBlocks.root
|
||||
.children[4] as SerializedBlockNode
|
||||
|
||||
@@ -761,7 +764,7 @@ describe('lexicalBlocks', () => {
|
||||
await saveDocAndAssert(page)
|
||||
|
||||
await assertLexicalDoc({
|
||||
fn: async ({ lexicalWithBlocks }) => {
|
||||
fn: ({ lexicalWithBlocks }) => {
|
||||
const blockNode: SerializedBlockNode = lexicalWithBlocks.root
|
||||
.children[4] as SerializedBlockNode
|
||||
const paragraphNodeInBlockNodeRichText = blockNode.fields.richTextField.root.children[1]
|
||||
@@ -836,7 +839,7 @@ describe('lexicalBlocks', () => {
|
||||
|
||||
// Make sure it's being returned from the API as well
|
||||
await assertLexicalDoc({
|
||||
fn: async ({ lexicalWithBlocks }) => {
|
||||
fn: ({ lexicalWithBlocks }) => {
|
||||
expect(
|
||||
(
|
||||
(lexicalWithBlocks.root.children[0] as SerializedParagraphNode)
|
||||
@@ -888,13 +891,17 @@ describe('lexicalBlocks', () => {
|
||||
.first()
|
||||
await expect(popoverHeading2Button).toBeVisible()
|
||||
|
||||
// scroll slash menu down
|
||||
await popoverHeading2Button.hover()
|
||||
await page.mouse.wheel(0, 250)
|
||||
|
||||
await expect(async () => {
|
||||
// Make sure that, even though it's "visible", it's not actually covered by something else due to z-index issues
|
||||
const popoverHeading2ButtonBoundingBox = await popoverHeading2Button.boundingBox()
|
||||
expect(popoverHeading2ButtonBoundingBox).not.toBeNull()
|
||||
expect(popoverHeading2ButtonBoundingBox).not.toBeUndefined()
|
||||
expect(popoverHeading2ButtonBoundingBox.height).toBeGreaterThan(0)
|
||||
expect(popoverHeading2ButtonBoundingBox.width).toBeGreaterThan(0)
|
||||
expect(popoverHeading2ButtonBoundingBox?.height).toBeGreaterThan(0)
|
||||
expect(popoverHeading2ButtonBoundingBox?.width).toBeGreaterThan(0)
|
||||
|
||||
// Now click the button to see if it actually works. Simulate an actual mouse click instead of using .click()
|
||||
// by using page.mouse and the correct coordinates
|
||||
@@ -903,8 +910,8 @@ describe('lexicalBlocks', () => {
|
||||
// This is why we use page.mouse.click() here. It's the most effective way of detecting such a z-index issue
|
||||
// and usually the only method which works.
|
||||
|
||||
const x = popoverHeading2ButtonBoundingBox.x
|
||||
const y = popoverHeading2ButtonBoundingBox.y
|
||||
const x = popoverHeading2ButtonBoundingBox?.x ?? 0
|
||||
const y = popoverHeading2ButtonBoundingBox?.y ?? 0
|
||||
|
||||
await page.mouse.click(x, y, { button: 'left' })
|
||||
|
||||
@@ -954,13 +961,16 @@ describe('lexicalBlocks', () => {
|
||||
await page.keyboard.type('text123')
|
||||
await expect(newContentTextArea).toHaveText('text123')
|
||||
|
||||
await wait(1000)
|
||||
|
||||
await saveDocAndAssert(page)
|
||||
|
||||
await assertLexicalDoc({
|
||||
fn: async ({ lexicalWithBlocks }) => {
|
||||
fn: ({ lexicalWithBlocks }) => {
|
||||
const blockNode: SerializedBlockNode = lexicalWithBlocks.root
|
||||
.children[5] as SerializedBlockNode
|
||||
const subBlocks = blockNode.fields.subBlocks
|
||||
|
||||
const subBlocks = blockNode.fields.subBlocksLexical
|
||||
|
||||
expect(subBlocks).toHaveLength(2)
|
||||
|
||||
@@ -1200,7 +1210,7 @@ describe('lexicalBlocks', () => {
|
||||
await saveDocAndAssert(page)
|
||||
|
||||
await assertLexicalDoc({
|
||||
fn: async ({ lexicalWithBlocks }) => {
|
||||
fn: ({ lexicalWithBlocks }) => {
|
||||
const radio1: SerializedBlockNode = lexicalWithBlocks.root
|
||||
.children[8] as SerializedBlockNode
|
||||
const radio2: SerializedBlockNode = lexicalWithBlocks.root
|
||||
@@ -1437,7 +1447,7 @@ describe('lexicalBlocks', () => {
|
||||
await expect(tab2Text1Field).toHaveValue('Some text2 changed')
|
||||
|
||||
await assertLexicalDoc({
|
||||
fn: async ({ lexicalWithBlocks }) => {
|
||||
fn: ({ lexicalWithBlocks }) => {
|
||||
const tabBlockData: SerializedBlockNode = lexicalWithBlocks.root
|
||||
.children[13] as SerializedBlockNode
|
||||
|
||||
@@ -1455,7 +1465,7 @@ describe('lexicalBlocks', () => {
|
||||
await codeEditor.scrollIntoViewIfNeeded()
|
||||
await expect(codeEditor).toBeVisible()
|
||||
|
||||
const height = (await codeEditor.boundingBox()).height
|
||||
const height = (await codeEditor.boundingBox())?.height
|
||||
|
||||
await expect(() => {
|
||||
expect(height).toBe(56)
|
||||
@@ -1463,7 +1473,7 @@ describe('lexicalBlocks', () => {
|
||||
await codeEditor.click()
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
const height2 = (await codeEditor.boundingBox()).height
|
||||
const height2 = (await codeEditor.boundingBox())?.height
|
||||
await expect(() => {
|
||||
expect(height2).toBe(74)
|
||||
}).toPass()
|
||||
@@ -1494,8 +1504,8 @@ describe('lexicalBlocks', () => {
|
||||
test('ensure inline blocks can be created and its values can be mutated from outside their form', async () => {
|
||||
const { richTextField } = await navigateToLexicalFields()
|
||||
const { inlineBlockDrawer, saveDrawer } = await createInlineBlock({
|
||||
richTextField,
|
||||
name: 'My Inline Block',
|
||||
richTextField,
|
||||
})
|
||||
|
||||
// Click on react select in drawer, select 'value1'
|
||||
@@ -1510,7 +1520,7 @@ describe('lexicalBlocks', () => {
|
||||
await saveDocAndAssert(page)
|
||||
// Check if the API result is correct
|
||||
await assertLexicalDoc({
|
||||
fn: async ({ lexicalWithBlocks }) => {
|
||||
fn: ({ lexicalWithBlocks }) => {
|
||||
const firstParagraph: SerializedParagraphNode = lexicalWithBlocks.root
|
||||
.children[0] as SerializedParagraphNode
|
||||
const inlineBlock: SerializedInlineBlockNode = firstParagraph
|
||||
@@ -1548,13 +1558,13 @@ describe('lexicalBlocks', () => {
|
||||
// Save and check api result
|
||||
await saveDocAndAssert(page)
|
||||
await assertLexicalDoc({
|
||||
fn: async ({ lexicalWithBlocks }) => {
|
||||
fn: ({ lexicalWithBlocks }) => {
|
||||
const firstParagraph: SerializedParagraphNode = lexicalWithBlocks.root
|
||||
.children[0] as SerializedParagraphNode
|
||||
const inlineBlock: SerializedInlineBlockNode = firstParagraph
|
||||
.children[1] as SerializedInlineBlockNode
|
||||
|
||||
await expect(inlineBlock.fields.key).toBe('value2')
|
||||
expect(inlineBlock.fields.key).toBe('value2')
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -1562,8 +1572,8 @@ describe('lexicalBlocks', () => {
|
||||
test('ensure upload fields within inline blocks store and populate correctly', async () => {
|
||||
const { richTextField } = await navigateToLexicalFields()
|
||||
const { inlineBlockDrawer, saveDrawer } = await createInlineBlock({
|
||||
richTextField,
|
||||
name: 'Avatar Group',
|
||||
richTextField,
|
||||
})
|
||||
|
||||
// Click button that says Add Avatar
|
||||
|
||||
@@ -165,8 +165,8 @@ export function generateLexicalRichText(): TypedEditorState<
|
||||
fields: {
|
||||
id: '65298b2bdb4ef8c744a7faac',
|
||||
blockName: 'Block Node, with Blocks Field, With RichText Field, With Relationship Node',
|
||||
blockType: 'subBlock',
|
||||
subBlocks: [
|
||||
blockType: 'subBlockLexical',
|
||||
subBlocksLexical: [
|
||||
{
|
||||
id: '65298b2edb4ef8c744a7faad',
|
||||
richText: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ServerEditorConfig } from '@payloadcms/richtext-lexical'
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import type { Block, BlockSlug, CollectionConfig } from 'payload'
|
||||
|
||||
import {
|
||||
BlocksFeature,
|
||||
@@ -37,364 +37,372 @@ import {
|
||||
} from './blocks.js'
|
||||
import { ModifyInlineBlockFeature } from './ModifyInlineBlockFeature/feature.server.js'
|
||||
|
||||
const editorConfig: ServerEditorConfig = {
|
||||
features: [
|
||||
...defaultEditorFeatures,
|
||||
//TestRecorderFeature(),
|
||||
TreeViewFeature(),
|
||||
//HTMLConverterFeature(),
|
||||
FixedToolbarFeature(),
|
||||
LinkFeature({
|
||||
fields: ({ defaultFields }) => [
|
||||
...defaultFields,
|
||||
{
|
||||
name: 'rel',
|
||||
type: 'select',
|
||||
admin: {
|
||||
description:
|
||||
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
|
||||
},
|
||||
hasMany: true,
|
||||
label: 'Rel Attribute',
|
||||
options: ['noopener', 'noreferrer', 'nofollow'],
|
||||
},
|
||||
],
|
||||
}),
|
||||
UploadFeature({
|
||||
collections: {
|
||||
uploads: {
|
||||
fields: [
|
||||
{
|
||||
name: 'caption',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor(),
|
||||
},
|
||||
],
|
||||
export const lexicalBlocks: (Block | BlockSlug)[] = [
|
||||
ValidationBlock,
|
||||
FilterOptionsBlock,
|
||||
AsyncHooksBlock,
|
||||
RichTextBlock,
|
||||
TextBlock,
|
||||
UploadAndRichTextBlock,
|
||||
SelectFieldBlock,
|
||||
RelationshipBlock,
|
||||
RelationshipHasManyBlock,
|
||||
SubBlockBlock,
|
||||
RadioButtonsBlock,
|
||||
ConditionalLayoutBlock,
|
||||
TabBlock,
|
||||
CodeBlock,
|
||||
{
|
||||
slug: 'myBlock',
|
||||
admin: {
|
||||
components: {},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'key',
|
||||
label: () => {
|
||||
return 'Key'
|
||||
},
|
||||
type: 'select',
|
||||
options: ['value1', 'value2', 'value3'],
|
||||
},
|
||||
}),
|
||||
ModifyInlineBlockFeature(),
|
||||
BlocksFeature({
|
||||
blocks: [
|
||||
ValidationBlock,
|
||||
FilterOptionsBlock,
|
||||
AsyncHooksBlock,
|
||||
RichTextBlock,
|
||||
TextBlock,
|
||||
UploadAndRichTextBlock,
|
||||
SelectFieldBlock,
|
||||
RelationshipBlock,
|
||||
RelationshipHasManyBlock,
|
||||
SubBlockBlock,
|
||||
RadioButtonsBlock,
|
||||
ConditionalLayoutBlock,
|
||||
TabBlock,
|
||||
CodeBlock,
|
||||
{
|
||||
slug: 'myBlock',
|
||||
admin: {
|
||||
components: {},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'key',
|
||||
label: () => {
|
||||
return 'Key'
|
||||
},
|
||||
type: 'select',
|
||||
options: ['value1', 'value2', 'value3'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'myBlockWithLabel',
|
||||
admin: {
|
||||
components: {
|
||||
Label: '/collections/Lexical/blockComponents/LabelComponent.js#LabelComponent',
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'key',
|
||||
label: () => {
|
||||
return 'Key'
|
||||
},
|
||||
type: 'select',
|
||||
options: ['value1', 'value2', 'value3'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'myBlockWithBlock',
|
||||
admin: {
|
||||
components: {
|
||||
Block: '/collections/Lexical/blockComponents/BlockComponent.js#BlockComponent',
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'key',
|
||||
label: () => {
|
||||
return 'Key'
|
||||
},
|
||||
type: 'select',
|
||||
options: ['value1', 'value2', 'value3'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'BlockRSC',
|
||||
|
||||
admin: {
|
||||
components: {
|
||||
Block: '/collections/Lexical/blockComponents/BlockComponentRSC.js#BlockComponentRSC',
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'key',
|
||||
label: () => {
|
||||
return 'Key'
|
||||
},
|
||||
type: 'select',
|
||||
options: ['value1', 'value2', 'value3'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'myBlockWithBlockAndLabel',
|
||||
admin: {
|
||||
components: {
|
||||
Block: '/collections/Lexical/blockComponents/BlockComponent.js#BlockComponent',
|
||||
Label: '/collections/Lexical/blockComponents/LabelComponent.js#LabelComponent',
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'key',
|
||||
label: () => {
|
||||
return 'Key'
|
||||
},
|
||||
type: 'select',
|
||||
options: ['value1', 'value2', 'value3'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
inlineBlocks: [
|
||||
{
|
||||
slug: 'AvatarGroup',
|
||||
interfaceName: 'AvatarGroupBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'avatars',
|
||||
type: 'array',
|
||||
minRows: 1,
|
||||
maxRows: 6,
|
||||
fields: [
|
||||
{
|
||||
name: 'image',
|
||||
type: 'upload',
|
||||
relationTo: 'uploads',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'myInlineBlock',
|
||||
admin: {
|
||||
components: {},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'key',
|
||||
label: () => {
|
||||
return 'Key'
|
||||
},
|
||||
type: 'select',
|
||||
options: ['value1', 'value2', 'value3'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'myInlineBlockWithLabel',
|
||||
admin: {
|
||||
components: {
|
||||
Label: '/collections/Lexical/inlineBlockComponents/LabelComponent.js#LabelComponent',
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'key',
|
||||
label: () => {
|
||||
return 'Key'
|
||||
},
|
||||
type: 'select',
|
||||
options: ['value1', 'value2', 'value3'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'myInlineBlockWithBlock',
|
||||
admin: {
|
||||
components: {
|
||||
Block: '/collections/Lexical/inlineBlockComponents/BlockComponent.js#BlockComponent',
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'key',
|
||||
label: () => {
|
||||
return 'Key'
|
||||
},
|
||||
type: 'select',
|
||||
options: ['value1', 'value2', 'value3'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'myInlineBlockWithBlockAndLabel',
|
||||
admin: {
|
||||
components: {
|
||||
Block: '/collections/Lexical/inlineBlockComponents/BlockComponent.js#BlockComponent',
|
||||
Label: '/collections/Lexical/inlineBlockComponents/LabelComponent.js#LabelComponent',
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'key',
|
||||
label: () => {
|
||||
return 'Key'
|
||||
},
|
||||
type: 'select',
|
||||
options: ['value1', 'value2', 'value3'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
EXPERIMENTAL_TableFeature(),
|
||||
],
|
||||
}
|
||||
|
||||
export const LexicalFields: CollectionConfig = {
|
||||
slug: lexicalFieldsSlug,
|
||||
access: {
|
||||
read: () => true,
|
||||
],
|
||||
},
|
||||
admin: {
|
||||
listSearchableFields: ['title', 'richTextLexicalCustomFields'],
|
||||
useAsTitle: 'title',
|
||||
{
|
||||
slug: 'myBlockWithLabel',
|
||||
admin: {
|
||||
components: {
|
||||
Label: '/collections/Lexical/blockComponents/LabelComponent.js#LabelComponent',
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'key',
|
||||
label: () => {
|
||||
return 'Key'
|
||||
},
|
||||
type: 'select',
|
||||
options: ['value1', 'value2', 'value3'],
|
||||
},
|
||||
],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
{
|
||||
slug: 'myBlockWithBlock',
|
||||
admin: {
|
||||
components: {
|
||||
Block: '/collections/Lexical/blockComponents/BlockComponent.js#BlockComponent',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'lexicalRootEditor',
|
||||
type: 'richText',
|
||||
fields: [
|
||||
{
|
||||
name: 'key',
|
||||
label: () => {
|
||||
return 'Key'
|
||||
},
|
||||
type: 'select',
|
||||
options: ['value1', 'value2', 'value3'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'BlockRSC',
|
||||
|
||||
admin: {
|
||||
components: {
|
||||
Block: '/collections/Lexical/blockComponents/BlockComponentRSC.js#BlockComponentRSC',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'lexicalSimple',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
//TestRecorderFeature(),
|
||||
TreeViewFeature(),
|
||||
BlocksFeature({
|
||||
blocks: [
|
||||
RichTextBlock,
|
||||
TextBlock,
|
||||
UploadAndRichTextBlock,
|
||||
SelectFieldBlock,
|
||||
RelationshipBlock,
|
||||
RelationshipHasManyBlock,
|
||||
SubBlockBlock,
|
||||
RadioButtonsBlock,
|
||||
ConditionalLayoutBlock,
|
||||
fields: [
|
||||
{
|
||||
name: 'key',
|
||||
label: () => {
|
||||
return 'Key'
|
||||
},
|
||||
type: 'select',
|
||||
options: ['value1', 'value2', 'value3'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'myBlockWithBlockAndLabel',
|
||||
admin: {
|
||||
components: {
|
||||
Block: '/collections/Lexical/blockComponents/BlockComponent.js#BlockComponent',
|
||||
Label: '/collections/Lexical/blockComponents/LabelComponent.js#LabelComponent',
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'key',
|
||||
label: () => {
|
||||
return 'Key'
|
||||
},
|
||||
type: 'select',
|
||||
options: ['value1', 'value2', 'value3'],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const lexicalInlineBlocks: (Block | BlockSlug)[] = [
|
||||
{
|
||||
slug: 'AvatarGroup',
|
||||
interfaceName: 'AvatarGroupBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'avatars',
|
||||
type: 'array',
|
||||
minRows: 1,
|
||||
maxRows: 6,
|
||||
fields: [
|
||||
{
|
||||
name: 'image',
|
||||
type: 'upload',
|
||||
relationTo: 'uploads',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'myInlineBlock',
|
||||
admin: {
|
||||
components: {},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'key',
|
||||
label: () => {
|
||||
return 'Key'
|
||||
},
|
||||
type: 'select',
|
||||
options: ['value1', 'value2', 'value3'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'myInlineBlockWithLabel',
|
||||
admin: {
|
||||
components: {
|
||||
Label: '/collections/Lexical/inlineBlockComponents/LabelComponent.js#LabelComponent',
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'key',
|
||||
label: () => {
|
||||
return 'Key'
|
||||
},
|
||||
type: 'select',
|
||||
options: ['value1', 'value2', 'value3'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'myInlineBlockWithBlock',
|
||||
admin: {
|
||||
components: {
|
||||
Block: '/collections/Lexical/inlineBlockComponents/BlockComponent.js#BlockComponent',
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'key',
|
||||
label: () => {
|
||||
return 'Key'
|
||||
},
|
||||
type: 'select',
|
||||
options: ['value1', 'value2', 'value3'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'myInlineBlockWithBlockAndLabel',
|
||||
admin: {
|
||||
components: {
|
||||
Block: '/collections/Lexical/inlineBlockComponents/BlockComponent.js#BlockComponent',
|
||||
Label: '/collections/Lexical/inlineBlockComponents/LabelComponent.js#LabelComponent',
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'key',
|
||||
label: () => {
|
||||
return 'Key'
|
||||
},
|
||||
type: 'select',
|
||||
options: ['value1', 'value2', 'value3'],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const getLexicalFieldsCollection: (args: {
|
||||
blocks: (Block | BlockSlug)[]
|
||||
inlineBlocks: (Block | BlockSlug)[]
|
||||
}) => CollectionConfig = ({ blocks, inlineBlocks }) => {
|
||||
const editorConfig: ServerEditorConfig = {
|
||||
features: [
|
||||
...defaultEditorFeatures,
|
||||
//TestRecorderFeature(),
|
||||
TreeViewFeature(),
|
||||
//HTMLConverterFeature(),
|
||||
FixedToolbarFeature(),
|
||||
LinkFeature({
|
||||
fields: ({ defaultFields }) => [
|
||||
...defaultFields,
|
||||
{
|
||||
name: 'rel',
|
||||
type: 'select',
|
||||
admin: {
|
||||
description:
|
||||
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
|
||||
},
|
||||
hasMany: true,
|
||||
label: 'Rel Attribute',
|
||||
options: ['noopener', 'noreferrer', 'nofollow'],
|
||||
},
|
||||
],
|
||||
}),
|
||||
UploadFeature({
|
||||
collections: {
|
||||
uploads: {
|
||||
fields: [
|
||||
{
|
||||
name: 'caption',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor(),
|
||||
},
|
||||
],
|
||||
}),
|
||||
HeadingFeature({ enabledHeadingSizes: ['h2', 'h4'] }),
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
ModifyInlineBlockFeature(),
|
||||
BlocksFeature({
|
||||
blocks,
|
||||
inlineBlocks,
|
||||
}),
|
||||
EXPERIMENTAL_TableFeature(),
|
||||
],
|
||||
}
|
||||
return {
|
||||
slug: lexicalFieldsSlug,
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
{
|
||||
type: 'ui',
|
||||
name: 'clearLexicalState',
|
||||
admin: {
|
||||
components: {
|
||||
Field: {
|
||||
path: '/collections/Lexical/components/ClearState.js#ClearState',
|
||||
clientProps: {
|
||||
fieldName: 'lexicalSimple',
|
||||
admin: {
|
||||
listSearchableFields: ['title', 'richTextLexicalCustomFields'],
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'lexicalRootEditor',
|
||||
type: 'richText',
|
||||
},
|
||||
{
|
||||
name: 'lexicalSimple',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
//TestRecorderFeature(),
|
||||
TreeViewFeature(),
|
||||
BlocksFeature({
|
||||
blocks: [
|
||||
RichTextBlock,
|
||||
TextBlock,
|
||||
UploadAndRichTextBlock,
|
||||
SelectFieldBlock,
|
||||
RelationshipBlock,
|
||||
RelationshipHasManyBlock,
|
||||
SubBlockBlock,
|
||||
RadioButtonsBlock,
|
||||
ConditionalLayoutBlock,
|
||||
],
|
||||
}),
|
||||
HeadingFeature({ enabledHeadingSizes: ['h2', 'h4'] }),
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
type: 'ui',
|
||||
name: 'clearLexicalState',
|
||||
admin: {
|
||||
components: {
|
||||
Field: {
|
||||
path: '/collections/Lexical/components/ClearState.js#ClearState',
|
||||
clientProps: {
|
||||
fieldName: 'lexicalSimple',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'lexicalWithBlocks',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
admin: {
|
||||
hideGutter: false,
|
||||
},
|
||||
features: editorConfig.features,
|
||||
}),
|
||||
required: true,
|
||||
},
|
||||
//{
|
||||
// name: 'rendered',
|
||||
// type: 'ui',
|
||||
// admin: {
|
||||
// components: {
|
||||
// Field: './collections/Lexical/LexicalRendered.js#LexicalRendered',
|
||||
// },
|
||||
// },
|
||||
//},
|
||||
{
|
||||
name: 'lexicalWithBlocks_markdown',
|
||||
type: 'textarea',
|
||||
hooks: {
|
||||
afterRead: [
|
||||
async ({ data, req, siblingData }) => {
|
||||
const yourSanitizedEditorConfig = await sanitizeServerEditorConfig(
|
||||
editorConfig,
|
||||
req.payload.config,
|
||||
)
|
||||
|
||||
const headlessEditor = createHeadlessEditor({
|
||||
nodes: getEnabledNodes({
|
||||
editorConfig: yourSanitizedEditorConfig,
|
||||
}),
|
||||
})
|
||||
|
||||
const yourEditorState: SerializedEditorState = siblingData.lexicalWithBlocks
|
||||
try {
|
||||
headlessEditor.update(
|
||||
() => {
|
||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState))
|
||||
},
|
||||
{ discrete: true },
|
||||
)
|
||||
} catch (e) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
// Export to markdown
|
||||
let markdown: string = ''
|
||||
headlessEditor.getEditorState().read(() => {
|
||||
markdown = $convertToMarkdownString(
|
||||
yourSanitizedEditorConfig?.features?.markdownTransformers,
|
||||
)
|
||||
})
|
||||
return markdown
|
||||
{
|
||||
name: 'lexicalWithBlocks',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
admin: {
|
||||
hideGutter: false,
|
||||
},
|
||||
],
|
||||
features: editorConfig.features,
|
||||
}),
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
//{
|
||||
// name: 'rendered',
|
||||
// type: 'ui',
|
||||
// admin: {
|
||||
// components: {
|
||||
// Field: './collections/Lexical/LexicalRendered.js#LexicalRendered',
|
||||
// },
|
||||
// },
|
||||
//},
|
||||
{
|
||||
name: 'lexicalWithBlocks_markdown',
|
||||
type: 'textarea',
|
||||
hooks: {
|
||||
afterRead: [
|
||||
async ({ data, req, siblingData }) => {
|
||||
const yourSanitizedEditorConfig = await sanitizeServerEditorConfig(
|
||||
editorConfig,
|
||||
req.payload.config,
|
||||
)
|
||||
|
||||
const headlessEditor = createHeadlessEditor({
|
||||
nodes: getEnabledNodes({
|
||||
editorConfig: yourSanitizedEditorConfig,
|
||||
}),
|
||||
})
|
||||
|
||||
const yourEditorState: SerializedEditorState = siblingData.lexicalWithBlocks
|
||||
try {
|
||||
headlessEditor.update(
|
||||
() => {
|
||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState))
|
||||
},
|
||||
{ discrete: true },
|
||||
)
|
||||
} catch (e) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
// Export to markdown
|
||||
let markdown: string = ''
|
||||
headlessEditor.getEditorState().read(() => {
|
||||
markdown = $convertToMarkdownString(
|
||||
yourSanitizedEditorConfig?.features?.markdownTransformers,
|
||||
)
|
||||
})
|
||||
return markdown
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export function generateLexicalLocalizedRichText(text1: string, text2: string, b
|
||||
blockName: '',
|
||||
textLocalized: text2,
|
||||
counter: 1,
|
||||
blockType: 'block',
|
||||
blockType: 'blockLexicalLocalized',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -32,7 +32,7 @@ export const LexicalLocalizedFields: CollectionConfig = {
|
||||
BlocksFeature({
|
||||
blocks: [
|
||||
{
|
||||
slug: 'block',
|
||||
slug: 'blockLexicalLocalized',
|
||||
fields: [
|
||||
{
|
||||
name: 'textLocalized',
|
||||
@@ -80,7 +80,7 @@ export const LexicalLocalizedFields: CollectionConfig = {
|
||||
BlocksFeature({
|
||||
blocks: [
|
||||
{
|
||||
slug: 'block',
|
||||
slug: 'blockLexicalLocalized2',
|
||||
fields: [
|
||||
{
|
||||
name: 'textLocalized',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user