feat(richtext-lexical)!: rework how population works and saves data, improve node typing

This commit is contained in:
Alessio Gravili
2024-04-17 11:46:47 -04:00
parent 58ea94f6ac
commit 39ba39c237
52 changed files with 709 additions and 420 deletions

View File

@@ -470,19 +470,24 @@ function buildObjectType({
// is run here again, with the provided depth.
// In the graphql find.ts resolver, the depth is then hard-coded to 0.
// Effectively, this means that the populationPromise for GraphQL is only run here, and not in the find.ts resolver / normal population promise.
if (editor?.populationPromise) {
await editor?.populationPromise({
if (editor?.populationPromises) {
const fieldPromises = []
const populationPromises = []
editor?.populationPromises({
context,
depth,
field,
fieldPromises,
findMany: false,
flattenLocales: false,
overrideAccess: false,
populationPromises: [],
populationPromises,
req: context.req,
showHiddenFields: false,
siblingDoc: parent,
})
await Promise.all(fieldPromises)
await Promise.all(populationPromises)
}
return parent[field.name]

View File

@@ -2,7 +2,7 @@ import type { I18n } from '@payloadcms/translations'
import type { JSONSchema4 } from 'json-schema'
import type { SanitizedConfig } from '../config/types.js'
import type { Field, RichTextField, Validate } from '../fields/config/types.js'
import type { Field, FieldBase, RichTextField, Validate } from '../fields/config/types.js'
import type { PayloadRequest, RequestContext } from '../types/index.js'
import type { WithServerSideProps } from './elements/WithServerSideProps.js'
@@ -19,15 +19,6 @@ type RichTextAdapterBase<
AdapterProps = any,
ExtraFieldProperties = {},
> = {
afterReadPromise?: ({
field,
incomingEditorState,
siblingDoc,
}: {
field: RichTextField<Value, AdapterProps, ExtraFieldProperties>
incomingEditorState: Value
siblingDoc: Record<string, unknown>
}) => Promise<void> | null
generateComponentMap: (args: {
WithServerSideProps: WithServerSideProps
config: SanitizedConfig
@@ -40,6 +31,7 @@ type RichTextAdapterBase<
schemaMap: Map<string, Field[]>
schemaPath: string
}) => Map<string, Field[]>
hooks?: FieldBase['hooks']
outputSchema?: ({
collectionIDFieldTypes,
config,
@@ -56,11 +48,18 @@ type RichTextAdapterBase<
interfaceNameDefinitions: Map<string, JSONSchema4>
isRequired: boolean
}) => JSONSchema4
populationPromise?: (data: {
/**
* Like an afterRead hook, but runs for both afterRead AND in the GraphQL resolver. For populating data, this should be used.
*
* To populate stuff / resolve field hooks, mutate the incoming populationPromises or fieldPromises array. They will then be awaited in the correct order within payload itself.
* @param data
*/
populationPromises?: (data: {
context: RequestContext
currentDepth?: number
depth: number
field: RichTextField<Value, AdapterProps, ExtraFieldProperties>
fieldPromises: Promise<void>[]
findMany: boolean
flattenLocales: boolean
overrideAccess?: boolean
@@ -68,7 +67,7 @@ type RichTextAdapterBase<
req: PayloadRequest
showHiddenFields: boolean
siblingDoc: Record<string, unknown>
}) => Promise<void> | null
}) => void
validate: Validate<
Value,
Value,

View File

@@ -18,6 +18,9 @@ type Args = {
doc: Record<string, unknown>
fallbackLocale: null | string
field: Field | TabAsField
/**
* fieldPromises are used for things like field hooks. They should be awaited before awaiting populationPromises
*/
fieldPromises: Promise<void>[]
findMany: boolean
flattenLocales: boolean
@@ -140,12 +143,13 @@ export const promise = async ({
case 'richText': {
const editor: RichTextAdapter = field?.editor
// This is run here AND in the GraphQL Resolver
if (editor?.populationPromise) {
const populationPromise = editor.populationPromise({
if (editor?.populationPromises) {
editor.populationPromises({
context,
currentDepth,
depth,
field,
fieldPromises,
findMany,
flattenLocales,
overrideAccess,
@@ -154,12 +158,8 @@ export const promise = async ({
showHiddenFields,
siblingDoc,
})
if (populationPromise) {
populationPromises.push(populationPromise)
}
}
/*
// This is only run here, independent of depth
if (editor?.afterReadPromise) {
const afterReadPromise = editor?.afterReadPromise({
@@ -171,7 +171,7 @@ export const promise = async ({
if (afterReadPromise) {
populationPromises.push(afterReadPromise)
}
}
}*/ //TODO: HOOKS!
break
}

View File

@@ -12,6 +12,9 @@ type Args = {
depth: number
doc: Record<string, unknown>
fallbackLocale: null | string
/**
* fieldPromises are used for things like field hooks. They should be awaited before awaiting populationPromises
*/
fieldPromises: Promise<void>[]
fields: (Field | TabAsField)[]
findMany: boolean

View File

@@ -109,3 +109,7 @@ export type AllOperations = AuthOperations | Operation | VersionOperations
export function docHasTimestamps(doc: any): doc is TypeWithTimestamps {
return doc?.createdAt && doc?.updatedAt
}
export type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N // This is a commonly used trick to detect 'any'
export type IsAny<T> = IfAny<T, true, false>
export type ReplaceAny<T, DefaultType> = IsAny<T> extends true ? DefaultType : T

View File

@@ -1,10 +1,10 @@
import lexicalRichTextImport, { type SerializedQuoteNode } from '@lexical/rich-text'
import lexicalRichTextImport from '@lexical/rich-text'
const { QuoteNode } = lexicalRichTextImport
import type { HTMLConverter } from '../converters/html/converter/types.js'
import type { FeatureProviderProviderServer } from '../types.js'
import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js'
import { createNode } from '../typeUtilities.js'
import { BlockQuoteFeatureClientComponent } from './feature.client.js'
import { MarkdownTransformer } from './markdownTransformer.js'
@@ -16,7 +16,7 @@ export const BlockQuoteFeature: FeatureProviderProviderServer<undefined, undefin
clientFeatureProps: null,
markdownTransformers: [MarkdownTransformer],
nodes: [
{
createNode({
converters: {
html: {
converter: async ({ converters, node, parent, payload }) => {
@@ -33,10 +33,10 @@ export const BlockQuoteFeature: FeatureProviderProviderServer<undefined, undefin
return `<blockquote>${childrenText}</blockquote>`
},
nodeTypes: [QuoteNode.getType()],
} as HTMLConverter<SerializedQuoteNode>,
},
},
node: QuoteNode,
},
}),
],
serverFeatureProps: props,
}

View File

@@ -125,6 +125,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
reducedBlock &&
initialState !== false && (
<Form
beforeSubmit={[onChange]}
// @ts-expect-error TODO: Fix this
fields={fieldMap}
initialState={initialState}

View File

@@ -8,6 +8,7 @@ import type { FeatureProviderProviderServer } from '../types.js'
import type { BlocksFeatureClientProps } from './feature.client.js'
import { cloneDeep } from '../../lexical/utils/cloneDeep.js'
import { createNode } from '../typeUtilities.js'
import { BlocksFeatureClientComponent } from './feature.client.js'
import { BlockNode } from './nodes/BlocksNode.js'
import { blockPopulationPromiseHOC } from './populationPromise.js'
@@ -131,11 +132,11 @@ export const BlocksFeature: FeatureProviderProviderServer<
},
},
nodes: [
{
createNode({
node: BlockNode,
populationPromises: [blockPopulationPromiseHOC(props)],
validations: [blockValidationHOC(props)],
},
}),
],
serverFeatureProps: props,
}

View File

@@ -16,6 +16,7 @@ export const blockPopulationPromiseHOC = (
currentDepth,
depth,
editorPopulationPromises,
fieldPromises,
findMany,
flattenLocales,
node,
@@ -28,8 +29,6 @@ export const blockPopulationPromiseHOC = (
const blocks: Block[] = props.blocks
const blockFieldData = node.fields
const promises: Promise<void>[] = []
// Sanitize block's fields here. This is done here and not in the feature, because the payload config is available here
const payloadConfig = req.payload.config
const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
@@ -45,7 +44,7 @@ export const blockPopulationPromiseHOC = (
// find block used in this node
const block = props.blocks.find((block) => block.slug === blockFieldData.blockType)
if (!block || !block?.fields?.length || !blockFieldData) {
return promises
return
}
recurseNestedFields({
@@ -54,19 +53,17 @@ export const blockPopulationPromiseHOC = (
data: blockFieldData,
depth,
editorPopulationPromises,
fieldPromises,
fields: block.fields,
findMany,
flattenLocales,
overrideAccess,
populationPromises,
promises,
req,
showHiddenFields,
// The afterReadPromise gets its data from looking for field.name inside the siblingDoc. Thus, here we cannot pass the whole document's siblingDoc, but only the siblingDoc (sibling fields) of the current field.
siblingDoc: blockFieldData,
})
return promises
}
return blockPopulationPromise

View File

@@ -7,6 +7,7 @@ import type { HTMLConverter } from '../converters/html/converter/types.js'
import type { FeatureProviderProviderServer } from '../types.js'
import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js'
import { createNode } from '../typeUtilities.js'
import { HeadingFeatureClientComponent } from './feature.client.js'
import { MarkdownTransformer } from './markdownTransformer.js'
@@ -30,8 +31,7 @@ export const HeadingFeature: FeatureProviderProviderServer<
ClientComponent: HeadingFeatureClientComponent,
markdownTransformers: [MarkdownTransformer(enabledHeadingSizes)],
nodes: [
{
type: HeadingNode.getType(),
createNode({
converters: {
html: {
converter: async ({ converters, node, parent, payload }) => {
@@ -48,10 +48,10 @@ export const HeadingFeature: FeatureProviderProviderServer<
return '<' + node?.tag + '>' + childrenText + '</' + node?.tag + '>'
},
nodeTypes: [HeadingNode.getType()],
} as HTMLConverter<SerializedHeadingNode>,
},
},
node: HeadingNode,
},
}),
],
serverFeatureProps: props,
}

View File

@@ -1,7 +1,6 @@
import type { HTMLConverter } from '../converters/html/converter/types.js'
import type { FeatureProviderProviderServer } from '../types.js'
import type { SerializedHorizontalRuleNode } from './nodes/HorizontalRuleNode.js'
import { createNode } from '../typeUtilities.js'
import { HorizontalRuleFeatureClientComponent } from './feature.client.js'
import { MarkdownTransformer } from './markdownTransformer.js'
import { HorizontalRuleNode } from './nodes/HorizontalRuleNode.js'
@@ -16,17 +15,17 @@ export const HorizontalRuleFeature: FeatureProviderProviderServer<undefined, und
clientFeatureProps: null,
markdownTransformers: [MarkdownTransformer],
nodes: [
{
createNode({
converters: {
html: {
converter: () => {
return `<hr/>`
},
nodeTypes: [HorizontalRuleNode.getType()],
} as HTMLConverter<SerializedHorizontalRuleNode>,
},
},
node: HorizontalRuleNode,
},
}),
],
serverFeatureProps: props,
}

View File

@@ -3,13 +3,13 @@ import type { SanitizedConfig } from 'payload/config'
import type { Field, FieldWithRichTextRequiredEditor } from 'payload/types'
import { traverseFields } from '@payloadcms/next/utilities'
import { deepCopyObject } from 'payload/utilities'
import type { HTMLConverter } from '../converters/html/converter/types.js'
import type { FeatureProviderProviderServer } from '../types.js'
import type { ClientProps } from './feature.client.js'
import type { SerializedAutoLinkNode, SerializedLinkNode } from './nodes/types.js'
import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js'
import { createNode } from '../typeUtilities.js'
import { LinkFeatureClientComponent } from './feature.client.js'
import { AutoLinkNode } from './nodes/AutoLinkNode.js'
import { LinkNode } from './nodes/LinkNode.js'
@@ -67,21 +67,26 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
enabledCollections: props.enabledCollections,
} as ExclusiveLinkCollectionsProps,
generateSchemaMap: ({ config, i18n, props }) => {
if (!props?.fields || !Array.isArray(props.fields) || props.fields.length === 0) {
return null
}
const schemaMap = new Map<string, Field[]>()
const validRelationships = config.collections.map((c) => c.slug) || []
const transformedFields = transformExtraFields(
props.fields,
deepCopyObject(props.fields),
config,
i18n,
props.enabledCollections,
props.disabledCollections,
)
if (
!transformedFields ||
!Array.isArray(transformedFields) ||
transformedFields.length === 0
) {
return null
}
const schemaMap = new Map<string, Field[]>()
const validRelationships = config.collections.map((c) => c.slug) || []
schemaMap.set('fields', transformedFields)
traverseFields({
@@ -96,7 +101,7 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
return schemaMap
},
nodes: [
{
createNode({
converters: {
html: {
converter: async ({ converters, node, parent, payload }) => {
@@ -123,12 +128,19 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
return `<a href="${href}"${rel}>${childrenText}</a>`
},
nodeTypes: [AutoLinkNode.getType()],
} as HTMLConverter<SerializedAutoLinkNode>,
},
},
hooks: {
afterRead: [
({ node }) => {
return node
},
],
},
node: AutoLinkNode,
populationPromises: [linkPopulationPromiseHOC(props)],
},
{
}),
createNode({
converters: {
html: {
converter: async ({ converters, node, parent, payload }) => {
@@ -152,11 +164,11 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
return `<a href="${href}"${rel}>${childrenText}</a>`
},
nodeTypes: [LinkNode.getType()],
} as HTMLConverter<SerializedLinkNode>,
},
},
node: LinkNode,
populationPromises: [linkPopulationPromiseHOC(props)],
},
}),
],
serverFeatureProps: props,
}

View File

@@ -1,19 +1,22 @@
import { sanitizeFields } from 'payload/config'
import { deepCopyObject } from 'payload/utilities'
import type { PopulationPromise } from '../types.js'
import type { LinkFeatureServerProps } from './feature.server.js'
import type { SerializedLinkNode } from './nodes/types.js'
import { populate } from '../../../populate/populate.js'
import { recurseNestedFields } from '../../../populate/recurseNestedFields.js'
import { transformExtraFields } from './plugins/floatingLinkEditor/utilities.js'
export const linkPopulationPromiseHOC = (
props: LinkFeatureServerProps,
): PopulationPromise<SerializedLinkNode> => {
const linkPopulationPromise: PopulationPromise<SerializedLinkNode> = ({
return ({
context,
currentDepth,
depth,
editorPopulationPromises,
field,
fieldPromises,
findMany,
flattenLocales,
node,
@@ -21,53 +24,55 @@ export const linkPopulationPromiseHOC = (
populationPromises,
req,
showHiddenFields,
siblingDoc,
}) => {
const promises: Promise<void>[] = []
// Sanitize link's fields here. This is done here and not in the feature, because the payload config is available here
const payloadConfig = req.payload.config
const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
if (node?.fields?.doc?.value && node?.fields?.doc?.relationTo) {
const collection = req.payload.collections[node?.fields?.doc?.relationTo]
const transformedFields = transformExtraFields(
deepCopyObject(props.fields),
payloadConfig,
req.i18n,
props.enabledCollections,
props.disabledCollections,
)
if (collection) {
promises.push(
populate({
id:
typeof node?.fields?.doc?.value === 'object'
? node?.fields?.doc?.value?.id
: node?.fields?.doc?.value,
collection,
currentDepth,
data: node?.fields?.doc,
depth,
field,
key: 'value',
overrideAccess,
req,
showHiddenFields,
}),
)
}
// TODO: Sanitize & transform ahead of time! On startup!
const sanitizedFields = sanitizeFields({
config: payloadConfig,
fields: transformedFields,
requireFieldLevelRichTextEditor: true,
validRelationships,
})
if (!sanitizedFields?.length) {
return
}
if (Array.isArray(props.fields)) {
/**
* Should populate all fields, including the doc field (for internal links), as it's treated like a normal field
*/
if (Array.isArray(sanitizedFields)) {
recurseNestedFields({
context,
currentDepth,
data: node.fields || {},
data: {
fields: node.fields,
},
depth,
editorPopulationPromises,
fields: props.fields,
fieldPromises,
fields: sanitizedFields,
findMany,
flattenLocales,
overrideAccess,
populationPromises,
promises,
req,
showHiddenFields,
siblingDoc: node.fields || {},
siblingDoc: {
fields: node.fields,
},
})
}
return promises
}
return linkPopulationPromise
}

View File

@@ -3,6 +3,7 @@ const { ListItemNode, ListNode } = lexicalListImport
import type { FeatureProviderProviderServer } from '../../types.js'
import { createNode } from '../../typeUtilities.js'
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter.js'
import { CheckListFeatureClientComponent } from './feature.client.js'
import { CHECK_LIST } from './markdownTransformers.js'
@@ -17,18 +18,18 @@ export const CheckListFeature: FeatureProviderProviderServer<undefined, undefine
featureProviderMap.has('unorderedlist') || featureProviderMap.has('orderedlist')
? []
: [
{
createNode({
converters: {
html: ListHTMLConverter,
},
node: ListNode,
},
{
}),
createNode({
converters: {
html: ListItemHTMLConverter,
},
node: ListItemNode,
},
}),
],
serverFeatureProps: props,
}

View File

@@ -3,6 +3,7 @@ const { ListItemNode, ListNode } = lexicalListImport
import type { FeatureProviderProviderServer } from '../../types.js'
import { createNode } from '../../typeUtilities.js'
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter.js'
import { OrderedListFeatureClientComponent } from './feature.client.js'
import { ORDERED_LIST } from './markdownTransformer.js'
@@ -16,18 +17,18 @@ export const OrderedListFeature: FeatureProviderProviderServer<undefined, undefi
nodes: featureProviderMap.has('unorderedlist')
? []
: [
{
createNode({
converters: {
html: ListHTMLConverter,
},
node: ListNode,
},
{
}),
createNode({
converters: {
html: ListItemHTMLConverter,
},
node: ListItemNode,
},
}),
],
serverFeatureProps: props,
}

View File

@@ -3,6 +3,7 @@ const { ListItemNode, ListNode } = lexicalListImport
import type { FeatureProviderProviderServer } from '../../types.js'
import { createNode } from '../../typeUtilities.js'
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter.js'
import { UnorderedListFeatureClientComponent } from './feature.client.js'
import { UNORDERED_LIST } from './markdownTransformer.js'
@@ -16,18 +17,18 @@ export const UnorderedListFeature: FeatureProviderProviderServer<undefined, unde
ClientComponent: UnorderedListFeatureClientComponent,
markdownTransformers: [UNORDERED_LIST],
nodes: [
{
createNode({
converters: {
html: ListHTMLConverter,
},
node: ListNode,
},
{
}),
createNode({
converters: {
html: ListItemHTMLConverter,
},
node: ListItemNode,
},
}),
],
serverFeatureProps: props,
}

View File

@@ -16,10 +16,8 @@ export const _UploadConverter: LexicalPluginNodeConverter = {
fields,
format: (lexicalPluginNode as any)?.format || '',
relationTo: (lexicalPluginNode as any)?.rawImagePayload?.relationTo,
value: {
id: (lexicalPluginNode as any)?.rawImagePayload?.value?.id || '',
},
version: 1,
value: (lexicalPluginNode as any)?.rawImagePayload?.value?.id || '',
version: 2,
} as const as SerializedUploadNode
},
nodeTypes: ['upload'],

View File

@@ -7,10 +7,8 @@ export const _SlateRelationshipConverter: SlateNodeConverter = {
type: 'relationship',
format: '',
relationTo: slateNode.relationTo,
value: {
id: slateNode?.value?.id || '',
},
version: 1,
value: slateNode?.value?.id || '',
version: 2,
} as const as SerializedRelationshipNode
},
nodeTypes: ['relationship'],

View File

@@ -10,10 +10,8 @@ export const _SlateUploadConverter: SlateNodeConverter = {
},
format: '',
relationTo: slateNode.relationTo,
value: {
id: slateNode.value?.id || '',
},
version: 1,
value: slateNode.value?.id || '',
version: 2,
} as const as SerializedUploadNode
},
nodeTypes: ['upload'],

View File

@@ -15,28 +15,26 @@ import { EnabledRelationshipsCondition } from '../utils/EnabledRelationshipsCond
import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from './commands.js'
const insertRelationship = ({
id,
editor,
relationTo,
replaceNodeKey,
value,
}: {
editor: LexicalEditor
id: string
relationTo: string
replaceNodeKey: null | string
value: number | string
}) => {
if (!replaceNodeKey) {
editor.dispatchCommand(INSERT_RELATIONSHIP_COMMAND, {
relationTo,
value: {
id,
},
value,
})
} else {
editor.update(() => {
const node = $getNodeByKey(replaceNodeKey)
if (node) {
node.replace($createRelationshipNode({ relationTo, value: { id } }))
node.replace($createRelationshipNode({ relationTo, value }))
}
})
}
@@ -75,10 +73,10 @@ const RelationshipDrawerComponent: React.FC<Props> = ({ enabledCollectionSlugs }
const onSelect = useCallback(
({ collectionSlug, docID }) => {
insertRelationship({
id: docID,
editor,
relationTo: collectionSlug,
replaceNodeKey,
value: docID,
})
closeDrawer()
},

View File

@@ -1,5 +1,6 @@
import type { FeatureProviderProviderServer } from '../types.js'
import { createNode } from '../typeUtilities.js'
import { RelationshipFeatureClientComponent } from './feature.client.js'
import { RelationshipNode } from './nodes/RelationshipNode.js'
import { relationshipPopulationPromise } from './populationPromise.js'
@@ -36,11 +37,11 @@ export const RelationshipFeature: FeatureProviderProviderServer<
ClientComponent: RelationshipFeatureClientComponent,
clientFeatureProps: props,
nodes: [
{
createNode({
node: RelationshipNode,
populationPromises: [relationshipPopulationPromise],
// TODO: Add validation similar to upload
},
}),
],
serverFeatureProps: props,
}

View File

@@ -25,11 +25,7 @@ const RelationshipComponent = React.lazy(() =>
export type RelationshipData = {
relationTo: string
value: {
// Actual relationship, populated in afterRead hook
[key: string]: unknown
id: string
}
value: number | string
}
export type SerializedRelationshipNode = Spread<RelationshipData, SerializedDecoratorBlockNode>
@@ -41,9 +37,7 @@ function relationshipElementToNode(domNode: HTMLDivElement): DOMConversionOutput
if (id != null && relationTo != null) {
const node = $createRelationshipNode({
relationTo,
value: {
id,
},
value: id,
})
return { node }
}
@@ -96,6 +90,10 @@ export class RelationshipNode extends DecoratorBlockNode {
}
static importJSON(serializedNode: SerializedRelationshipNode): RelationshipNode {
if (serializedNode.version === 1 && (serializedNode?.value as unknown as { id: string })?.id) {
serializedNode.value = (serializedNode.value as unknown as { id: string }).id
}
const importedData: RelationshipData = {
relationTo: serializedNode.relationTo,
value: serializedNode.value,
@@ -108,6 +106,7 @@ export class RelationshipNode extends DecoratorBlockNode {
static isInline(): false {
return false
}
decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element {
return (
<RelationshipComponent
@@ -118,10 +117,9 @@ export class RelationshipNode extends DecoratorBlockNode {
/>
)
}
exportDOM(): DOMExportOutput {
const element = document.createElement('div')
element.setAttribute('data-lexical-relationship-id', this.__data?.value?.id)
element.setAttribute('data-lexical-relationship-id', String(this.__data?.value))
element.setAttribute('data-lexical-relationship-relationTo', this.__data?.relationTo)
const text = document.createTextNode(this.getTextContent())
@@ -134,7 +132,7 @@ export class RelationshipNode extends DecoratorBlockNode {
...super.exportJSON(),
...this.getData(),
type: this.getType(),
version: 1,
version: 2,
}
}
@@ -143,7 +141,7 @@ export class RelationshipNode extends DecoratorBlockNode {
}
getTextContent(): string {
return `${this.__data?.relationTo} relation to ${this.__data?.value?.id}`
return `${this.__data?.relationTo} relation to ${this.__data?.value}`
}
setData(data: RelationshipData): void {

View File

@@ -38,10 +38,7 @@ type Props = {
const Component: React.FC<Props> = (props) => {
const {
children,
data: {
relationTo,
value: { id },
},
data: { relationTo, value: id },
nodeKey,
} = props

View File

@@ -9,18 +9,20 @@ export const relationshipPopulationPromise: PopulationPromise<SerializedRelation
field,
node,
overrideAccess,
populationPromises,
req,
showHiddenFields,
}) => {
const promises: Promise<void>[] = []
if (node?.value) {
// @ts-expect-error
const id = node?.value?.id || node?.value // for backwards-compatibility
if (node?.value?.id) {
const collection = req.payload.collections[node?.relationTo]
if (collection) {
promises.push(
populationPromises.push(
populate({
id: node.value.id,
id,
collection,
currentDepth,
data: node,
@@ -34,6 +36,4 @@ export const relationshipPopulationPromise: PopulationPromise<SerializedRelation
)
}
}
return promises
}

View File

@@ -0,0 +1,13 @@
import type { LexicalNode } from 'lexical'
import type { NodeWithHooks } from './types.js'
/**
* Utility function to create a node with hooks. You don't have to use this utility, but it improves type inference
* @param node the node
*/
export function createNode<Node extends LexicalNode>(
node: NodeWithHooks<Node>,
): NodeWithHooks<Node> {
return node
}

View File

@@ -6,7 +6,13 @@ import type { SerializedLexicalNode } from 'lexical'
import type { LexicalNodeReplacement } from 'lexical'
import type { RequestContext } from 'payload'
import type { SanitizedConfig } from 'payload/config'
import type { Field, PayloadRequest, RichTextField, ValidateOptions } from 'payload/types'
import type {
Field,
PayloadRequest,
ReplaceAny,
RichTextField,
ValidateOptions,
} from 'payload/types'
import type React from 'react'
import type { AdapterProps } from '../../types.js'
@@ -21,6 +27,7 @@ export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexica
depth,
editorPopulationPromises,
field,
fieldPromises,
findMany,
flattenLocales,
node,
@@ -38,6 +45,10 @@ export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexica
*/
editorPopulationPromises: Map<string, Array<PopulationPromise>>
field: RichTextField<SerializedEditorState, AdapterProps>
/**
* fieldPromises are used for things like field hooks. They will be awaited before awaiting populationPromises
*/
fieldPromises: Promise<void>[]
findMany: boolean
flattenLocales: boolean
node: T
@@ -46,7 +57,7 @@ export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexica
req: PayloadRequest
showHiddenFields: boolean
siblingDoc: Record<string, unknown>
}) => Promise<void>[]
}) => void
export type NodeValidation<T extends SerializedLexicalNode = SerializedLexicalNode> = ({
node,
@@ -171,6 +182,44 @@ export type ClientComponentProps<ClientFeatureProps> = ClientFeatureProps & {
order: number
}
export type FieldNodeHookArgs<T extends SerializedLexicalNode> = {
context: RequestContext
/** Boolean to denote if this hook is running against finding one, or finding many within the afterRead hook. */
findMany?: boolean
/** The value of the field. */
node?: T
/** A string relating to which operation the field type is currently executing within. Useful within beforeValidate, beforeChange, and afterChange hooks to differentiate between create and update operations. */
operation?: 'create' | 'delete' | 'read' | 'update'
/** The Express request object. It is mocked for Local API operations. */
req: PayloadRequest
}
export type FieldNodeHook<T extends SerializedLexicalNode> = (
args: FieldNodeHookArgs<T>,
) => Promise<T> | T
// Define the node with hooks that use the node's exportJSON return type
export type NodeWithHooks<T extends LexicalNode = any> = {
converters?: {
html?: HTMLConverter<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>
}
hooks?: {
afterChange?: Array<FieldNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
afterRead?: Array<FieldNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
beforeChange?: Array<FieldNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
/**
* Runs before a document is duplicated to prevent errors in unique fields or return null to use defaultValue.
*/
beforeDuplicate?: Array<FieldNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
beforeValidate?: Array<FieldNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
}
node: Klass<T> | LexicalNodeReplacement
populationPromises?: Array<
PopulationPromise<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>
>
validations?: Array<NodeValidation<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
}
export type ServerFeature<ServerProps, ClientFeatureProps> = {
ClientComponent?: React.FC<ClientComponentProps<ClientFeatureProps>>
/**
@@ -215,26 +264,8 @@ export type ServerFeature<ServerProps, ClientFeatureProps> = {
isRequired: boolean
}) => JSONSchema4
}
hooks?: {
afterReadPromise?: ({
field,
incomingEditorState,
siblingDoc,
}: {
field: RichTextField<SerializedEditorState, AdapterProps>
incomingEditorState: SerializedEditorState
siblingDoc: Record<string, unknown>
}) => Promise<void> | null
}
markdownTransformers?: Transformer[]
nodes?: Array<{
converters?: {
html?: HTMLConverter
}
node: Klass<LexicalNode> | LexicalNodeReplacement
populationPromises?: Array<PopulationPromise>
validations?: Array<NodeValidation>
}>
nodes?: Array<NodeWithHooks>
/** Props which were passed into your feature will have to be passed here. This will allow them to be used / read in other places of the code, e.g. wherever you can use useEditorConfigContext */
serverFeatureProps: ServerProps
@@ -325,20 +356,18 @@ export type SanitizedServerFeatures = Required<
}) => JSONSchema4
>
}
hooks: {
afterReadPromises: Array<
({
field,
incomingEditorState,
siblingDoc,
}: {
field: RichTextField<SerializedEditorState, AdapterProps>
incomingEditorState: SerializedEditorState
siblingDoc: Record<string, unknown>
}) => Promise<void> | null
>
}
/** The node types mapped to their populationPromises */
/** The node types mapped to their hooks */
hooks?: {
afterChange?: Map<string, Array<FieldNodeHook<SerializedLexicalNode>>>
afterRead?: Map<string, Array<FieldNodeHook<SerializedLexicalNode>>>
beforeChange?: Map<string, Array<FieldNodeHook<SerializedLexicalNode>>>
/**
* Runs before a document is duplicated to prevent errors in unique fields or return null to use defaultValue.
*/
beforeDuplicate?: Map<string, Array<FieldNodeHook<SerializedLexicalNode>>>
beforeValidate?: Map<string, Array<FieldNodeHook<SerializedLexicalNode>>>
} /** The node types mapped to their populationPromises */
populationPromises: Map<string, Array<PopulationPromise>>
/** The node types mapped to their validations */
validations: Map<string, Array<NodeValidation>>

View File

@@ -65,13 +65,13 @@ const Component: React.FC<ElementProps> = (props) => {
const drawerSlug = useDrawerSlug('upload-drawer')
const [DocumentDrawer, DocumentDrawerToggler, { closeDrawer }] = useDocumentDrawer({
id: value?.id,
id: value,
collectionSlug: relatedCollection.slug,
})
// Get the referenced document
const [{ data }, { setParams }] = usePayloadAPI(
`${serverURL}${api}/${relatedCollection.slug}/${value?.id}`,
`${serverURL}${api}/${relatedCollection.slug}/${value}`,
{ initialParams },
)
@@ -172,7 +172,7 @@ const Component: React.FC<ElementProps> = (props) => {
</DocumentDrawerToggler>
</div>
</div>
{value?.id && <DocumentDrawer onSave={updateUpload} />}
{value && <DocumentDrawer onSave={updateUpload} />}
{hasExtraFields ? (
<ExtraFieldsUploadDrawer
drawerSlug={drawerSlug}

View File

@@ -17,21 +17,21 @@ import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from './commands.js'
const baseClass = 'lexical-upload-drawer'
const insertUpload = ({
id,
editor,
relationTo,
replaceNodeKey,
value,
}: {
editor: LexicalEditor
id: string
relationTo: string
replaceNodeKey: null | string
value: number | string
}) => {
if (!replaceNodeKey) {
editor.dispatchCommand(INSERT_UPLOAD_COMMAND, {
id,
fields: null,
relationTo,
value,
})
} else {
editor.update(() => {
@@ -42,9 +42,7 @@ const insertUpload = ({
data: {
fields: null,
relationTo,
value: {
id,
},
value,
},
}),
)
@@ -84,10 +82,10 @@ const UploadDrawerComponent: React.FC<Props> = ({ enabledCollectionSlugs }) => {
const onSelect = useCallback(
({ collectionSlug, docID }) => {
insertUpload({
id: docID,
editor,
relationTo: collectionSlug,
replaceNodeKey,
value: docID,
})
closeDrawer()
},

View File

@@ -1,13 +1,20 @@
import type { Field, FieldWithRichTextRequiredEditor, Payload } from 'payload/types'
import type {
Field,
FieldWithRichTextRequiredEditor,
FileData,
FileSize,
Payload,
TypeWithID,
} from 'payload/types'
import { traverseFields } from '@payloadcms/next/utilities'
import type { HTMLConverter } from '../converters/html/converter/types.js'
import type { FeatureProviderProviderServer } from '../types.js'
import type { UploadFeaturePropsClient } from './feature.client.js'
import { createNode } from '../typeUtilities.js'
import { UploadFeatureClientComponent } from './feature.client.js'
import { type SerializedUploadNode, UploadNode } from './nodes/UploadNode.js'
import { UploadNode } from './nodes/UploadNode.js'
import { uploadPopulationPromiseHOC } from './populationPromise.js'
import { uploadValidation } from './validate.js'
@@ -71,17 +78,21 @@ export const UploadFeature: FeatureProviderProviderServer<
return schemaMap
},
nodes: [
{
createNode({
converters: {
html: {
converter: async ({ node, payload }) => {
// @ts-expect-error
const id = node?.value?.id || node?.value // for backwards-compatibility
if (payload) {
let uploadDocument: any
let uploadDocument: TypeWithID & FileData
try {
uploadDocument = await payload.findByID({
id: node.value.id,
uploadDocument = (await payload.findByID({
id,
collection: node.relationTo,
})
})) as TypeWithID & FileData
} catch (ignored) {
// eslint-disable-next-line no-console
console.error(
@@ -93,12 +104,12 @@ export const UploadFeature: FeatureProviderProviderServer<
return `<img />`
}
const url: string = getAbsoluteURL(uploadDocument?.url as string, payload)
const url = getAbsoluteURL(uploadDocument?.url, payload)
/**
* If the upload is not an image, return a link to the upload
*/
if (!(uploadDocument?.mimeType as string)?.startsWith('image')) {
if (!uploadDocument?.mimeType?.startsWith('image')) {
return `<a href="${url}" rel="noopener noreferrer">${uploadDocument.filename}</a>`
}
@@ -116,7 +127,9 @@ export const UploadFeature: FeatureProviderProviderServer<
// Iterate through each size in the data.sizes object
for (const size in uploadDocument.sizes) {
const imageSize = uploadDocument.sizes[size]
const imageSize: FileSize & {
url?: string
} = uploadDocument.sizes[size]
// Skip if any property of the size object is null
if (
@@ -129,7 +142,7 @@ export const UploadFeature: FeatureProviderProviderServer<
) {
continue
}
const imageSizeURL: string = getAbsoluteURL(imageSize?.url as string, payload)
const imageSizeURL = getAbsoluteURL(imageSize?.url, payload)
pictureHTML += `<source srcset="${imageSizeURL}" media="(max-width: ${imageSize.width}px)" type="${imageSize.mimeType}">`
}
@@ -139,16 +152,16 @@ export const UploadFeature: FeatureProviderProviderServer<
pictureHTML += '</picture>'
return pictureHTML
} else {
return `<img src="${node.value.id}" />`
return `<img src="${id}" />`
}
},
nodeTypes: [UploadNode.getType()],
} as HTMLConverter<SerializedUploadNode>,
},
},
node: UploadNode,
populationPromises: [uploadPopulationPromiseHOC(props)],
validations: [uploadValidation()],
},
}),
],
serverFeatureProps: props,
}

View File

@@ -21,26 +21,13 @@ const RawUploadComponent = React.lazy(() =>
import('../component/index.js').then((module) => ({ default: module.UploadComponent })),
)
export type RawUploadPayload = {
fields: {
// unknown, custom fields:
[key: string]: unknown
}
id: string
relationTo: string
}
export type UploadData = {
fields: {
// unknown, custom fields:
[key: string]: unknown
}
relationTo: string
value: {
// Actual upload data, populated in afterRead hook
[key: string]: unknown
id: string
}
value: number | string
}
function convertUploadElement(domNode: Node): DOMConversionOutput | null {
@@ -93,6 +80,10 @@ export class UploadNode extends DecoratorBlockNode {
}
static importJSON(serializedNode: SerializedUploadNode): UploadNode {
if (serializedNode.version === 1 && (serializedNode?.value as unknown as { id: string })?.id) {
serializedNode.value = (serializedNode.value as unknown as { id: string }).id
}
const importedData: UploadData = {
fields: serializedNode.fields,
relationTo: serializedNode.relationTo,
@@ -126,7 +117,7 @@ export class UploadNode extends DecoratorBlockNode {
...super.exportJSON(),
...this.getData(),
type: this.getType(),
version: 1,
version: 2,
}
}

View File

@@ -18,12 +18,12 @@ import type { LexicalCommand } from 'lexical'
import { useConfig } from '@payloadcms/ui/providers/Config'
import React, { useEffect } from 'react'
import type { RawUploadPayload } from '../nodes/UploadNode.js'
import type { UploadData } from '../nodes/UploadNode.js'
import { UploadDrawer } from '../drawer/index.js'
import { $createUploadNode, UploadNode } from '../nodes/UploadNode.js'
export type InsertUploadPayload = Readonly<RawUploadPayload>
export type InsertUploadPayload = Readonly<UploadData>
export const INSERT_UPLOAD_COMMAND: LexicalCommand<InsertUploadPayload> =
createCommand('INSERT_UPLOAD_COMMAND')
@@ -46,9 +46,7 @@ export function UploadPlugin(): JSX.Element | null {
data: {
fields: payload.fields,
relationTo: payload.relationTo,
value: {
id: payload.id,
},
value: payload.value,
},
})

View File

@@ -1,3 +1,5 @@
import { sanitizeFields } from 'payload/config'
import type { PopulationPromise } from '../types.js'
import type { UploadFeatureProps } from './feature.server.js'
import type { SerializedUploadNode } from './nodes/UploadNode.js'
@@ -8,12 +10,13 @@ import { recurseNestedFields } from '../../../populate/recurseNestedFields.js'
export const uploadPopulationPromiseHOC = (
props?: UploadFeatureProps,
): PopulationPromise<SerializedUploadNode> => {
const uploadPopulationPromise: PopulationPromise<SerializedUploadNode> = ({
return ({
context,
currentDepth,
depth,
editorPopulationPromises,
field,
fieldPromises,
findMany,
flattenLocales,
node,
@@ -21,17 +24,19 @@ export const uploadPopulationPromiseHOC = (
populationPromises,
req,
showHiddenFields,
siblingDoc,
}) => {
const promises: Promise<void>[] = []
const payloadConfig = req.payload.config
if (node?.value?.id) {
if (node?.value) {
const collection = req.payload.collections[node?.relationTo]
if (collection) {
promises.push(
// @ts-expect-error
const id = node?.value?.id || node?.value // for backwards-compatibility
populationPromises.push(
populate({
id: node?.value?.id,
id,
collection,
currentDepth,
data: node,
@@ -45,27 +50,36 @@ export const uploadPopulationPromiseHOC = (
)
}
if (Array.isArray(props?.collections?.[node?.relationTo]?.fields)) {
const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
// TODO: Sanitize & transform ahead of time! On startup!
const sanitizedFields = sanitizeFields({
config: payloadConfig,
fields: props?.collections?.[node?.relationTo]?.fields,
requireFieldLevelRichTextEditor: true,
validRelationships,
})
if (!sanitizedFields?.length) {
return
}
recurseNestedFields({
context,
currentDepth,
data: node.fields || {},
depth,
editorPopulationPromises,
fields: props?.collections?.[node?.relationTo]?.fields,
fieldPromises,
fields: sanitizedFields,
findMany,
flattenLocales,
overrideAccess,
populationPromises,
promises,
req,
showHiddenFields,
siblingDoc: node.fields || {},
})
}
}
return promises
}
return uploadPopulationPromise
}

View File

@@ -6,7 +6,7 @@ import type { SerializedUploadNode } from './nodes/UploadNode.js'
import { CAN_USE_DOM } from '../../lexical/utils/canUseDOM.js'
export const uploadValidation = (): NodeValidation<SerializedUploadNode> => {
const uploadValidation: NodeValidation<SerializedUploadNode> = ({
return ({
node,
validation: {
options: {
@@ -16,8 +16,10 @@ export const uploadValidation = (): NodeValidation<SerializedUploadNode> => {
}) => {
if (!CAN_USE_DOM) {
const idType = payload.collections[node.relationTo].customIDType || payload.db.defaultIDType
// @ts-expect-error
const id = node?.value?.id || node?.value // for backwards-compatibility
if (!isValidID(node.value?.id, idType)) {
if (!isValidID(id, idType)) {
return t('validation:validUploadID')
}
}
@@ -26,6 +28,4 @@ export const uploadValidation = (): NodeValidation<SerializedUploadNode> => {
return true
}
return uploadValidation
}

View File

@@ -16,7 +16,11 @@ export const sanitizeServerFeatures = (
modifyOutputSchemas: [],
},
hooks: {
afterReadPromises: [],
afterChange: new Map(),
afterRead: new Map(),
beforeChange: new Map(),
beforeDuplicate: new Map(),
beforeValidate: new Map(),
},
markdownTransformers: [],
nodes: [],
@@ -33,13 +37,6 @@ export const sanitizeServerFeatures = (
if (feature?.generatedTypes?.modifyOutputSchema) {
sanitized.generatedTypes.modifyOutputSchemas.push(feature.generatedTypes.modifyOutputSchema)
}
if (feature.hooks) {
if (feature.hooks.afterReadPromise) {
sanitized.hooks.afterReadPromises = sanitized.hooks.afterReadPromises.concat(
feature.hooks.afterReadPromise,
)
}
}
if (feature.nodes?.length) {
sanitized.nodes = sanitized.nodes.concat(feature.nodes)
@@ -54,6 +51,21 @@ export const sanitizeServerFeatures = (
if (node?.converters?.html) {
sanitized.converters.html.push(node.converters.html)
}
if (node?.hooks?.afterChange) {
sanitized.hooks.afterChange.set(nodeType, node.hooks.afterChange)
}
if (node?.hooks?.afterRead) {
sanitized.hooks.afterRead.set(nodeType, node.hooks.afterRead)
}
if (node?.hooks?.beforeChange) {
sanitized.hooks.beforeChange.set(nodeType, node.hooks.beforeChange)
}
if (node?.hooks?.beforeDuplicate) {
sanitized.hooks.beforeDuplicate.set(nodeType, node.hooks.beforeDuplicate)
}
if (node?.hooks?.beforeValidate) {
sanitized.hooks.beforeValidate.set(nodeType, node.hooks.beforeValidate)
}
})
}

View File

@@ -21,6 +21,8 @@ export function validateUrl(url: string): boolean {
// TODO Fix UI for link insertion; it should never default to an invalid URL such as https://.
// Maybe show a dialog where they user can type the URL before inserting it.
if (!url) return false
if (url === 'https://') return true
// This makes sure URLs starting with www. instead of https are valid too

View File

@@ -20,7 +20,7 @@ import { sanitizeServerFeatures } from './field/lexical/config/server/sanitize.j
import { cloneDeep } from './field/lexical/utils/cloneDeep.js'
import { getGenerateComponentMap } from './generateComponentMap.js'
import { getGenerateSchemaMap } from './generateSchemaMap.js'
import { richTextRelationshipPromise } from './populate/richTextRelationshipPromise.js'
import { populateLexicalPopulationPromises } from './populate/populateLexicalPopulationPromises.js'
import { richTextValidateHOC } from './validate/index.js'
export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapter {
@@ -64,29 +64,6 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
Component: RichTextField,
toMergeIntoProps: { lexicalEditorConfig: finalSanitizedEditorConfig.lexical },
}),
afterReadPromise: ({ field, incomingEditorState, siblingDoc }) => {
return new Promise<void>((resolve, reject) => {
const promises: Promise<void>[] = []
if (finalSanitizedEditorConfig?.features?.hooks?.afterReadPromises?.length) {
for (const afterReadPromise of finalSanitizedEditorConfig.features.hooks
.afterReadPromises) {
const promise = afterReadPromise({
field,
incomingEditorState,
siblingDoc,
})
if (promise) {
promises.push(promise)
}
}
}
Promise.all(promises)
.then(() => resolve())
.catch((error) => reject(error))
})
},
editorConfig: finalSanitizedEditorConfig,
generateComponentMap: getGenerateComponentMap({
resolvedFeatureMap,
@@ -94,6 +71,13 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
generateSchemaMap: getGenerateSchemaMap({
resolvedFeatureMap,
}),
/* hooks: {
afterChange: finalSanitizedEditorConfig.features.hooks.afterChange,
afterRead: finalSanitizedEditorConfig.features.hooks.afterRead,
beforeChange: finalSanitizedEditorConfig.features.hooks.beforeChange,
beforeDuplicate: finalSanitizedEditorConfig.features.hooks.beforeDuplicate,
beforeValidate: finalSanitizedEditorConfig.features.hooks.beforeValidate,
},*/
outputSchema: ({
collectionIDFieldTypes,
config,
@@ -171,11 +155,12 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
return outputSchema
},
populationPromise({
populationPromises({
context,
currentDepth,
depth,
field,
fieldPromises,
findMany,
flattenLocales,
overrideAccess,
@@ -186,12 +171,13 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
}) {
// check if there are any features with nodes which have populationPromises for this field
if (finalSanitizedEditorConfig?.features?.populationPromises?.size) {
return richTextRelationshipPromise({
populateLexicalPopulationPromises({
context,
currentDepth,
depth,
editorPopulationPromises: finalSanitizedEditorConfig.features.populationPromises,
field,
fieldPromises,
findMany,
flattenLocales,
overrideAccess,
@@ -201,8 +187,6 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
siblingDoc,
})
}
return null
},
validate: richTextValidateHOC({
editorConfig: finalSanitizedEditorConfig,
@@ -311,14 +295,19 @@ export {
RelationshipNode,
type SerializedRelationshipNode,
} from './field/features/relationship/nodes/RelationshipNode.js'
export { createNode } from './field/features/typeUtilities.js'
export type {
ClientComponentProps,
ClientFeature,
ClientFeatureProviderMap,
FeatureProviderClient,
FeatureProviderProviderClient,
FeatureProviderProviderServer,
FeatureProviderServer,
FieldNodeHook,
FieldNodeHookArgs,
NodeValidation,
NodeWithHooks,
PopulationPromise,
ResolvedClientFeature,
ResolvedClientFeatureMap,
@@ -334,8 +323,6 @@ export { UploadFeature } from './field/features/upload/feature.server.js'
export type { UploadFeatureProps } from './field/features/upload/feature.server.js'
export type { RawUploadPayload } from './field/features/upload/nodes/UploadNode.js'
export {
$createUploadNode,
$isUploadNode,

View File

@@ -28,7 +28,7 @@ export const populate = async ({
}: Omit<Arguments, 'field'> & {
collection: Collection
field: Field
id: string
id: number | string
}): Promise<void> => {
const dataRef = data as Record<string, unknown>

View File

@@ -1,26 +1,17 @@
import type { SerializedEditorState, SerializedLexicalNode } from 'lexical'
import type { PayloadRequest, RichTextAdapter, RichTextField } from 'payload/types'
import type { RichTextAdapter } from 'payload/types'
import type { PopulationPromise } from '../field/features/types.js'
import type { AdapterProps } from '../types.js'
export type Args = Parameters<
RichTextAdapter<SerializedEditorState, AdapterProps>['populationPromise']
RichTextAdapter<SerializedEditorState, AdapterProps>['populationPromises']
>[0] & {
editorPopulationPromises: Map<string, Array<PopulationPromise>>
}
type RecurseRichTextArgs = {
children: SerializedLexicalNode[]
currentDepth: number
depth: number
editorPopulationPromises: Map<string, Array<PopulationPromise>>
field: RichTextField<SerializedEditorState, AdapterProps>
overrideAccess: boolean
promises: Promise<void>[]
req: PayloadRequest
showHiddenFields: boolean
siblingDoc?: Record<string, unknown>
}
export const recurseRichText = ({
@@ -30,15 +21,15 @@ export const recurseRichText = ({
depth,
editorPopulationPromises,
field,
fieldPromises,
findMany,
flattenLocales,
overrideAccess = false,
populationPromises,
promises,
req,
showHiddenFields,
siblingDoc,
}: RecurseRichTextArgs & Args): void => {
}: Args & RecurseRichTextArgs): void => {
if (depth <= 0 || currentDepth > depth) {
return
}
@@ -47,23 +38,22 @@ export const recurseRichText = ({
children.forEach((node) => {
if (editorPopulationPromises?.has(node.type)) {
for (const promise of editorPopulationPromises.get(node.type)) {
promises.push(
...promise({
context,
currentDepth,
depth,
editorPopulationPromises,
field,
findMany,
flattenLocales,
node,
overrideAccess,
populationPromises,
req,
showHiddenFields,
siblingDoc,
}),
)
promise({
context,
currentDepth,
depth,
editorPopulationPromises,
field,
fieldPromises,
findMany,
flattenLocales,
node,
overrideAccess,
populationPromises,
req,
showHiddenFields,
siblingDoc,
})
}
}
@@ -75,11 +65,11 @@ export const recurseRichText = ({
depth,
editorPopulationPromises,
field,
fieldPromises,
findMany,
flattenLocales,
overrideAccess,
populationPromises,
promises,
req,
showHiddenFields,
siblingDoc,
@@ -89,12 +79,16 @@ export const recurseRichText = ({
}
}
export const richTextRelationshipPromise = async ({
/**
* Appends all new populationPromises to the populationPromises prop
*/
export const populateLexicalPopulationPromises = ({
context,
currentDepth,
depth,
editorPopulationPromises,
field,
fieldPromises,
findMany,
flattenLocales,
overrideAccess,
@@ -102,9 +96,7 @@ export const richTextRelationshipPromise = async ({
req,
showHiddenFields,
siblingDoc,
}: Args): Promise<void> => {
const promises = []
}: Args) => {
recurseRichText({
children: (siblingDoc[field?.name] as SerializedEditorState)?.root?.children ?? [],
context,
@@ -112,15 +104,13 @@ export const richTextRelationshipPromise = async ({
depth,
editorPopulationPromises,
field,
fieldPromises,
findMany,
flattenLocales,
overrideAccess,
populationPromises,
promises,
req,
showHiddenFields,
siblingDoc,
})
await Promise.all(promises)
}

View File

@@ -14,12 +14,15 @@ type NestedRichTextFieldsArgs = {
* This maps all the population promises to the node types
*/
editorPopulationPromises: Map<string, Array<PopulationPromise>>
/**
* fieldPromises are used for things like field hooks. They should be awaited before awaiting populationPromises
*/
fieldPromises: Promise<void>[]
fields: Field[]
findMany: boolean
flattenLocales: boolean
overrideAccess: boolean
populationPromises: Promise<void>[]
promises: Promise<void>[]
req: PayloadRequest
showHiddenFields: boolean
siblingDoc: Record<string, unknown>
@@ -30,12 +33,12 @@ export const recurseNestedFields = ({
currentDepth = 0,
data,
depth,
fieldPromises,
fields,
findMany,
flattenLocales,
overrideAccess = false,
populationPromises,
promises,
req,
showHiddenFields,
siblingDoc,
@@ -47,7 +50,7 @@ export const recurseNestedFields = ({
depth,
doc: data as any, // Looks like it's only needed for hooks and access control, so doesn't matter what we pass here right now
fallbackLocale: req.fallbackLocale,
fieldPromises: promises, // Not sure if what I pass in here makes sense. But it doesn't seem like it's used at all anyways
fieldPromises,
fields,
findMany,
flattenLocales,
@@ -58,7 +61,7 @@ export const recurseNestedFields = ({
req,
showHiddenFields,
siblingDoc,
triggerAccessControl: false, // TODO: Enable this to support access control
triggerHooks: false, // TODO: Enable this to support hooks
//triggerAccessControl: false, // TODO: Enable this to support access control
//triggerHooks: false, // TODO: Enable this to support hooks
})
}

View File

@@ -11,7 +11,7 @@ type NestedRichTextFieldsArgs = {
depth: number
fields: Field[]
overrideAccess: boolean
promises: Promise<void>[]
populationPromises: Promise<void>[]
req: PayloadRequest
showHiddenFields: boolean
}
@@ -22,7 +22,7 @@ export const recurseNestedFields = ({
depth,
fields,
overrideAccess = false,
promises,
populationPromises,
req,
showHiddenFields,
}: NestedRichTextFieldsArgs): void => {
@@ -34,7 +34,7 @@ export const recurseNestedFields = ({
data[field.name].forEach(({ relationTo, value }, i) => {
const collection = req.payload.collections[relationTo]
if (collection) {
promises.push(
populationPromises.push(
populate({
id: value,
collection,
@@ -54,7 +54,7 @@ export const recurseNestedFields = ({
data[field.name].forEach((id, i) => {
const collection = req.payload.collections[field.relationTo as string]
if (collection) {
promises.push(
populationPromises.push(
populate({
id,
collection,
@@ -78,7 +78,7 @@ export const recurseNestedFields = ({
) {
if (!('hasMany' in field) || !field.hasMany) {
const collection = req.payload.collections[data[field.name].relationTo]
promises.push(
populationPromises.push(
populate({
id: data[field.name].value,
collection,
@@ -97,7 +97,7 @@ export const recurseNestedFields = ({
}
if (typeof data[field.name] !== 'undefined' && typeof field.relationTo === 'string') {
const collection = req.payload.collections[field.relationTo]
promises.push(
populationPromises.push(
populate({
id: data[field.name],
collection,
@@ -120,7 +120,7 @@ export const recurseNestedFields = ({
depth,
fields: field.fields,
overrideAccess,
promises,
populationPromises,
req,
showHiddenFields,
})
@@ -131,7 +131,7 @@ export const recurseNestedFields = ({
depth,
fields: field.fields,
overrideAccess,
promises,
populationPromises,
req,
showHiddenFields,
})
@@ -144,7 +144,7 @@ export const recurseNestedFields = ({
depth,
fields: tab.fields,
overrideAccess,
promises,
populationPromises,
req,
showHiddenFields,
})
@@ -160,7 +160,7 @@ export const recurseNestedFields = ({
depth,
fields: block.fields,
overrideAccess,
promises,
populationPromises,
req,
showHiddenFields,
})
@@ -176,7 +176,7 @@ export const recurseNestedFields = ({
depth,
fields: field.fields,
overrideAccess,
promises,
populationPromises,
req,
showHiddenFields,
})
@@ -193,7 +193,7 @@ export const recurseNestedFields = ({
depth,
field,
overrideAccess,
promises,
populationPromises,
req,
showHiddenFields,
})

View File

@@ -5,7 +5,7 @@ import type { AdapterArguments } from '../types.js'
import { populate } from './populate.js'
import { recurseNestedFields } from './recurseNestedFields.js'
export type Args = Parameters<RichTextAdapter<any[], AdapterArguments>['populationPromise']>[0]
export type Args = Parameters<RichTextAdapter<any[], AdapterArguments>['populationPromises']>[0]
type RecurseRichTextArgs = {
children: unknown[]
@@ -13,7 +13,7 @@ type RecurseRichTextArgs = {
depth: number
field: RichTextField<any[], AdapterArguments, AdapterArguments>
overrideAccess: boolean
promises: Promise<void>[]
populationPromises: Promise<void>[]
req: PayloadRequest
showHiddenFields: boolean
}
@@ -24,7 +24,7 @@ export const recurseRichText = ({
depth,
field,
overrideAccess = false,
promises,
populationPromises,
req,
showHiddenFields,
}: RecurseRichTextArgs): void => {
@@ -38,7 +38,7 @@ export const recurseRichText = ({
const collection = req.payload.collections[element?.relationTo]
if (collection) {
promises.push(
populationPromises.push(
populate({
id: element.value.id,
collection,
@@ -63,7 +63,7 @@ export const recurseRichText = ({
depth,
fields: field.admin.upload.collections[element.relationTo].fields,
overrideAccess,
promises,
populationPromises,
req,
showHiddenFields,
})
@@ -75,7 +75,7 @@ export const recurseRichText = ({
const collection = req.payload.collections[element?.doc?.relationTo]
if (collection) {
promises.push(
populationPromises.push(
populate({
id: element.doc.value,
collection,
@@ -99,7 +99,7 @@ export const recurseRichText = ({
depth,
fields: field.admin?.link?.fields,
overrideAccess,
promises,
populationPromises,
req,
showHiddenFields,
})
@@ -113,7 +113,7 @@ export const recurseRichText = ({
depth,
field,
overrideAccess,
promises,
populationPromises,
req,
showHiddenFields,
})
@@ -122,27 +122,24 @@ export const recurseRichText = ({
}
}
export const richTextRelationshipPromise = async ({
export const richTextRelationshipPromise = ({
currentDepth,
depth,
field,
overrideAccess,
populationPromises,
req,
showHiddenFields,
siblingDoc,
}: Args): Promise<void> => {
const promises = []
}: Args) => {
recurseRichText({
children: siblingDoc[field.name] as unknown[],
currentDepth,
depth,
field,
overrideAccess,
promises,
populationPromises,
req,
showHiddenFields,
})
await Promise.all(promises)
}

View File

@@ -25,11 +25,12 @@ export function slateEditor(args: AdapterArguments): RichTextAdapter<any[], Adap
},
}
},
populationPromise({
populationPromises({
context,
currentDepth,
depth,
field,
fieldPromises,
findMany,
flattenLocales,
overrideAccess,
@@ -44,11 +45,12 @@ export function slateEditor(args: AdapterArguments): RichTextAdapter<any[], Adap
field.admin?.elements?.includes('link') ||
!field?.admin?.elements
) {
return richTextRelationshipPromise({
richTextRelationshipPromise({
context,
currentDepth,
depth,
field,
fieldPromises,
findMany,
flattenLocales,
overrideAccess,
@@ -58,7 +60,6 @@ export function slateEditor(args: AdapterArguments): RichTextAdapter<any[], Adap
siblingDoc,
})
}
return null
},
validate: richTextValidate,
}

View File

@@ -19,6 +19,7 @@ type Args<T> = {
siblingData: Data
}
// TODO: Make this works for rich text subfields
export const defaultValuePromise = async <T>({
id,
data,

View File

@@ -33,9 +33,15 @@ let serverURL: string
/**
* Client-side navigation to the lexical editor from list view
*/
async function navigateToLexicalFields(navigateToListView: boolean = true) {
async function navigateToLexicalFields(
navigateToListView: boolean = true,
localized: boolean = false,
) {
if (navigateToListView) {
const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'lexical-fields')
const url: AdminUrlUtil = new AdminUrlUtil(
serverURL,
localized ? 'lexical-localized-fields' : 'lexical-fields',
)
await page.goto(url.list)
}
@@ -1101,7 +1107,7 @@ describe('lexical', () => {
)
})
test.skip('should respect required error state in deeply nested text field', async () => {
test('should respect required error state in deeply nested text field', async () => {
await navigateToLexicalFields()
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
await richTextField.scrollIntoViewIfNeeded()
@@ -1117,12 +1123,18 @@ describe('lexical', () => {
await page.click('#action-save', { delay: 100 })
await expect(page.locator('.Toastify')).toContainText('The following field is invalid')
const requiredTooltip = conditionalArrayBlock
.locator('.tooltip-content:has-text("This field is required.")')
.first()
await requiredTooltip.scrollIntoViewIfNeeded()
// Check if error is shown next to field
await expect(
conditionalArrayBlock
.locator('.tooltip-content:has-text("This field is required.")')
.first(),
).toBeVisible()
await expect(requiredTooltip).toBeInViewport() // toBeVisible() doesn't work for some reason
})
})
describe('localization', () => {
test.skip('ensure simple localized lexical field works', async () => {
await navigateToLexicalFields(true, true)
})
})
})

View File

@@ -27,7 +27,7 @@ export function generateLexicalRichText() {
{
format: '',
type: 'upload',
version: 1,
version: 2,
fields: {
caption: {
root: {
@@ -57,11 +57,9 @@ export function generateLexicalRichText() {
{
format: '',
type: 'relationship',
version: 1,
version: 2,
relationTo: 'text-fields',
value: {
id: '{{TEXT_DOC_ID}}',
},
value: '{{TEXT_DOC_ID}}',
},
],
direction: 'ltr',
@@ -69,9 +67,7 @@ export function generateLexicalRichText() {
},
},
relationTo: 'uploads',
value: {
id: '{{UPLOAD_DOC_ID}}',
},
value: '{{UPLOAD_DOC_ID}}',
},
{
format: '',
@@ -120,11 +116,9 @@ export function generateLexicalRichText() {
{
format: '',
type: 'relationship',
version: 1,
version: 2,
relationTo: 'rich-text-fields',
value: {
id: '{{RICH_TEXT_DOC_ID}}',
},
value: '{{RICH_TEXT_DOC_ID}}',
},
{
children: [
@@ -173,11 +167,9 @@ export function generateLexicalRichText() {
{
format: '',
type: 'relationship',
version: 1,
version: 2,
relationTo: 'text-fields',
value: {
id: '{{TEXT_DOC_ID}}',
},
value: '{{TEXT_DOC_ID}}',
},
{
children: [

View File

@@ -0,0 +1,95 @@
import type { CollectionConfig } from 'payload/types'
import { BlocksFeature, lexicalEditor } from '@payloadcms/richtext-lexical'
import { lexicalLocalizedFieldsSlug } from '../../slugs.js'
export const LexicalLocalizedFields: CollectionConfig = {
slug: lexicalLocalizedFieldsSlug,
admin: {
useAsTitle: 'title',
listSearchableFields: ['title'],
},
access: {
read: () => true,
},
fields: [
{
name: 'title',
type: 'text',
required: true,
localized: true,
},
{
name: 'lexicalSimple',
type: 'richText',
localized: true,
editor: lexicalEditor({
features: ({ defaultFeatures }) => [...defaultFeatures],
}),
},
{
name: 'lexicalBlocksLocalized',
admin: {
description: 'Localized field with localized block subfields',
},
type: 'richText',
localized: true,
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
BlocksFeature({
blocks: [
{
slug: 'block',
fields: [
{
name: 'textLocalized',
type: 'text',
localized: true,
},
{
name: 'rel',
type: 'relationship',
relationTo: lexicalLocalizedFieldsSlug,
},
],
},
],
}),
],
}),
},
{
name: 'lexicalBlocksSubLocalized',
type: 'richText',
admin: {
description: 'Non-localized field with localized block subfields',
},
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
BlocksFeature({
blocks: [
{
slug: 'block',
fields: [
{
name: 'textLocalized',
type: 'text',
localized: true,
},
{
name: 'rel',
type: 'relationship',
relationTo: lexicalLocalizedFieldsSlug,
},
],
},
],
}),
],
}),
},
],
}

View File

@@ -0,0 +1,54 @@
import type { SerializedRelationshipNode } from '@payloadcms/richtext-lexical'
import type { SerializedEditorState, SerializedParagraphNode, SerializedTextNode } from 'lexical'
import { lexicalLocalizedFieldsSlug } from '../../slugs.js'
export function textToLexicalJSON({
text,
lexicalLocalizedRelID,
}: {
lexicalLocalizedRelID?: number | string
text: string
}) {
const editorJSON: SerializedEditorState = {
root: {
type: 'root',
format: '',
indent: 0,
version: 1,
direction: 'ltr',
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text,
type: 'text',
version: 1,
} as SerializedTextNode,
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
} as SerializedParagraphNode,
],
},
}
if (lexicalLocalizedRelID) {
editorJSON.root.children.push({
format: '',
type: 'relationship',
version: 2,
relationTo: lexicalLocalizedFieldsSlug,
value: lexicalLocalizedRelID,
} as SerializedRelationshipNode)
}
return editorJSON
}

View File

@@ -116,10 +116,8 @@ export function generateLexicalRichText() {
{
format: '',
type: 'relationship',
version: 1,
value: {
id: '{{TEXT_DOC_ID}}',
},
version: 2,
value: '{{TEXT_DOC_ID}}',
relationTo: 'text-fields',
},
{
@@ -230,11 +228,9 @@ export function generateLexicalRichText() {
{
format: '',
type: 'upload',
version: 1,
version: 2,
relationTo: 'uploads',
value: {
id: '{{UPLOAD_DOC_ID}}',
},
value: '{{UPLOAD_DOC_ID}}',
fields: {
caption: {
root: {

View File

@@ -12,6 +12,7 @@ import GroupFields from './collections/Group/index.js'
import IndexedFields from './collections/Indexed/index.js'
import JSONFields from './collections/JSON/index.js'
import { LexicalFields } from './collections/Lexical/index.js'
import { LexicalLocalizedFields } from './collections/LexicalLocalized/index.js'
import { LexicalMigrateFields } from './collections/LexicalMigrate/index.js'
import NumberFields from './collections/Number/index.js'
import PointFields from './collections/Point/index.js'
@@ -31,6 +32,7 @@ import { clearAndSeedEverything } from './seed.js'
export const collectionSlugs: CollectionConfig[] = [
LexicalFields,
LexicalMigrateFields,
LexicalLocalizedFields,
{
slug: 'users',
admin: {

View File

@@ -416,9 +416,10 @@ describe('Lexical', () => {
/**
* Depth 1 population:
*/
expect(subEditorRelationshipNode.value.id).toStrictEqual(createdRichTextDocID)
expect(subEditorRelationshipNode.value).toStrictEqual(createdRichTextDocID)
// But the value should not be populated and only have the id field:
expect(Object.keys(subEditorRelationshipNode.value)).toHaveLength(1)
expect(typeof subEditorRelationshipNode.value).not.toStrictEqual('object')
})
it('should populate relationship nodes inside of a sub-editor from a blocks node with 1 depth', async () => {
@@ -463,9 +464,9 @@ describe('Lexical', () => {
/**
* Depth 2 population:
*/
expect(populatedDocEditorRelationshipNode.value.id).toStrictEqual(createdTextDocID)
expect(populatedDocEditorRelationshipNode.value).toStrictEqual(createdTextDocID)
// But the value should not be populated and only have the id field - that's because it would require a depth of 2
expect(Object.keys(populatedDocEditorRelationshipNode.value)).toHaveLength(1)
expect(populatedDocEditorRelationshipNode.value).not.toStrictEqual('object')
})
it('should populate relationship nodes inside of a sub-editor from a blocks node with depth 2', async () => {

View File

@@ -15,6 +15,7 @@ import { dateDoc } from './collections/Date/shared.js'
import { groupDoc } from './collections/Group/shared.js'
import { jsonDoc } from './collections/JSON/shared.js'
import { lexicalDocData } from './collections/Lexical/data.js'
import { textToLexicalJSON } from './collections/LexicalLocalized/textToLexicalJSON.js'
import { lexicalMigrateDocData } from './collections/LexicalMigrate/data.js'
import { numberDoc } from './collections/Number/shared.js'
import { pointDoc } from './collections/Point/shared.js'
@@ -35,6 +36,7 @@ import {
groupFieldsSlug,
jsonFieldsSlug,
lexicalFieldsSlug,
lexicalLocalizedFieldsSlug,
lexicalMigrateFieldsSlug,
numberFieldsSlug,
pointFieldsSlug,
@@ -49,7 +51,7 @@ import {
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export const seed = async (_payload) => {
export const seed = async (_payload: Payload) => {
if (_payload.db.name === 'mongoose') {
await Promise.all(
_payload.config.collections.map(async (coll) => {
@@ -274,6 +276,74 @@ export const seed = async (_payload) => {
overrideAccess: true,
})
const lexicalLocalizedDoc1 = await _payload.create({
collection: lexicalLocalizedFieldsSlug,
data: {
title: 'Localized Lexical en',
lexicalSimple: textToLexicalJSON({ text: 'English text' }),
lexicalBlocksLocalized: textToLexicalJSON({ text: 'English text' }),
lexicalBlocksSubLocalized: textToLexicalJSON({ text: 'English text' }),
},
locale: 'en',
depth: 0,
overrideAccess: true,
})
await _payload.update({
collection: lexicalLocalizedFieldsSlug,
id: lexicalLocalizedDoc1.id,
data: {
title: 'Localized Lexical es',
lexicalSimple: textToLexicalJSON({ text: 'Spanish text' }),
lexicalBlocksLocalized: textToLexicalJSON({ text: 'Spanish text' }),
lexicalBlocksSubLocalized: textToLexicalJSON({ text: 'Spanish text' }),
},
locale: 'es',
depth: 0,
overrideAccess: true,
})
const lexicalLocalizedDoc2 = await _payload.create({
collection: lexicalLocalizedFieldsSlug,
data: {
title: 'Localized Lexical en 2',
lexicalSimple: textToLexicalJSON({
text: 'English text 2',
lexicalLocalizedRelID: lexicalLocalizedDoc1.id,
}),
lexicalBlocksLocalized: textToLexicalJSON({
text: 'English text 2',
lexicalLocalizedRelID: lexicalLocalizedDoc1.id,
}),
lexicalBlocksSubLocalized: textToLexicalJSON({
text: 'English text 2',
lexicalLocalizedRelID: lexicalLocalizedDoc1.id,
}),
},
locale: 'en',
depth: 0,
overrideAccess: true,
})
await _payload.update({
collection: lexicalLocalizedFieldsSlug,
id: lexicalLocalizedDoc2.id,
data: {
title: 'Localized Lexical es 2',
lexicalSimple: textToLexicalJSON({
text: 'Spanish text 2',
lexicalLocalizedRelID: lexicalLocalizedDoc1.id,
}),
lexicalBlocksLocalized: textToLexicalJSON({
text: 'Spanish text 2',
lexicalLocalizedRelID: lexicalLocalizedDoc1.id,
}),
},
locale: 'es',
depth: 0,
overrideAccess: true,
})
await _payload.create({
collection: lexicalMigrateFieldsSlug,
data: lexicalMigrateDocWithRelId,

View File

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