From 39ba39c237c92f88aa014251a9ccafd16e8de0cb Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Wed, 17 Apr 2024 11:46:47 -0400 Subject: [PATCH] feat(richtext-lexical)!: rework how population works and saves data, improve node typing --- .../graphql/src/schema/buildObjectType.ts | 11 ++- packages/payload/src/admin/RichText.ts | 23 +++-- .../src/fields/hooks/afterRead/promise.ts | 16 +-- .../fields/hooks/afterRead/traverseFields.ts | 3 + packages/payload/src/types/index.ts | 4 + .../features/blockquote/feature.server.ts | 10 +- .../field/features/blocks/component/index.tsx | 1 + .../field/features/blocks/feature.server.ts | 5 +- .../features/blocks/populationPromise.ts | 9 +- .../field/features/heading/feature.server.ts | 8 +- .../features/horizontalrule/feature.server.ts | 9 +- .../src/field/features/link/feature.server.ts | 44 ++++++--- .../field/features/link/populationPromise.ts | 73 +++++++------- .../lists/checklist/feature.server.ts | 9 +- .../lists/orderedlist/feature.server.ts | 9 +- .../lists/unorderedlist/feature.server.ts | 9 +- .../converter/converters/upload/converter.ts | 6 +- .../converters/relationship/converter.ts | 6 +- .../converter/converters/upload/converter.ts | 6 +- .../features/relationship/drawer/index.tsx | 12 +-- .../features/relationship/feature.server.ts | 5 +- .../relationship/nodes/RelationshipNode.tsx | 22 ++--- .../components/RelationshipComponent.tsx | 5 +- .../relationship/populationPromise.ts | 12 +-- .../src/field/features/typeUtilities.ts | 13 +++ .../src/field/features/types.ts | 99 ++++++++++++------- .../field/features/upload/component/index.tsx | 6 +- .../field/features/upload/drawer/index.tsx | 12 +-- .../field/features/upload/feature.server.ts | 43 +++++--- .../features/upload/nodes/UploadNode.tsx | 21 ++-- .../field/features/upload/plugin/index.tsx | 8 +- .../features/upload/populationPromise.ts | 38 ++++--- .../src/field/features/upload/validate.ts | 8 +- .../field/lexical/config/server/sanitize.ts | 28 ++++-- .../src/field/lexical/utils/url.ts | 2 + packages/richtext-lexical/src/index.ts | 47 ++++----- .../richtext-lexical/src/populate/populate.ts | 2 +- ...s => populateLexicalPopulationPromises.ts} | 66 ++++++------- .../src/populate/recurseNestedFields.ts | 13 ++- .../src/data/recurseNestedFields.ts | 24 ++--- .../src/data/richTextRelationshipPromise.ts | 27 +++-- packages/richtext-slate/src/index.tsx | 7 +- .../calculateDefaultValues/promise.ts | 1 + test/fields/collections/Lexical/e2e.spec.ts | 28 ++++-- .../Lexical/generateLexicalRichText.ts | 24 ++--- .../collections/LexicalLocalized/index.ts | 95 ++++++++++++++++++ .../LexicalLocalized/textToLexicalJSON.ts | 54 ++++++++++ .../RichText/generateLexicalRichText.ts | 12 +-- test/fields/config.ts | 2 + test/fields/lexical.int.spec.ts | 9 +- test/fields/seed.ts | 72 +++++++++++++- test/fields/slugs.ts | 51 +++++----- 52 files changed, 709 insertions(+), 420 deletions(-) create mode 100644 packages/richtext-lexical/src/field/features/typeUtilities.ts rename packages/richtext-lexical/src/populate/{richTextRelationshipPromise.ts => populateLexicalPopulationPromises.ts} (64%) create mode 100644 test/fields/collections/LexicalLocalized/index.ts create mode 100644 test/fields/collections/LexicalLocalized/textToLexicalJSON.ts diff --git a/packages/graphql/src/schema/buildObjectType.ts b/packages/graphql/src/schema/buildObjectType.ts index 3b334b496..b231cab68 100644 --- a/packages/graphql/src/schema/buildObjectType.ts +++ b/packages/graphql/src/schema/buildObjectType.ts @@ -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] diff --git a/packages/payload/src/admin/RichText.ts b/packages/payload/src/admin/RichText.ts index 6f67ae8b9..8ae9ce8c5 100644 --- a/packages/payload/src/admin/RichText.ts +++ b/packages/payload/src/admin/RichText.ts @@ -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 - incomingEditorState: Value - siblingDoc: Record - }) => Promise | null generateComponentMap: (args: { WithServerSideProps: WithServerSideProps config: SanitizedConfig @@ -40,6 +31,7 @@ type RichTextAdapterBase< schemaMap: Map schemaPath: string }) => Map + hooks?: FieldBase['hooks'] outputSchema?: ({ collectionIDFieldTypes, config, @@ -56,11 +48,18 @@ type RichTextAdapterBase< interfaceNameDefinitions: Map 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 + fieldPromises: Promise[] findMany: boolean flattenLocales: boolean overrideAccess?: boolean @@ -68,7 +67,7 @@ type RichTextAdapterBase< req: PayloadRequest showHiddenFields: boolean siblingDoc: Record - }) => Promise | null + }) => void validate: Validate< Value, Value, diff --git a/packages/payload/src/fields/hooks/afterRead/promise.ts b/packages/payload/src/fields/hooks/afterRead/promise.ts index c867a3543..60046cee1 100644 --- a/packages/payload/src/fields/hooks/afterRead/promise.ts +++ b/packages/payload/src/fields/hooks/afterRead/promise.ts @@ -18,6 +18,9 @@ type Args = { doc: Record fallbackLocale: null | string field: Field | TabAsField + /** + * fieldPromises are used for things like field hooks. They should be awaited before awaiting populationPromises + */ fieldPromises: Promise[] 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 } diff --git a/packages/payload/src/fields/hooks/afterRead/traverseFields.ts b/packages/payload/src/fields/hooks/afterRead/traverseFields.ts index 31df7bcb7..a83ea131c 100644 --- a/packages/payload/src/fields/hooks/afterRead/traverseFields.ts +++ b/packages/payload/src/fields/hooks/afterRead/traverseFields.ts @@ -12,6 +12,9 @@ type Args = { depth: number doc: Record fallbackLocale: null | string + /** + * fieldPromises are used for things like field hooks. They should be awaited before awaiting populationPromises + */ fieldPromises: Promise[] fields: (Field | TabAsField)[] findMany: boolean diff --git a/packages/payload/src/types/index.ts b/packages/payload/src/types/index.ts index f7d66ebd4..a4740aef8 100644 --- a/packages/payload/src/types/index.ts +++ b/packages/payload/src/types/index.ts @@ -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 = 0 extends 1 & T ? Y : N // This is a commonly used trick to detect 'any' +export type IsAny = IfAny +export type ReplaceAny = IsAny extends true ? DefaultType : T diff --git a/packages/richtext-lexical/src/field/features/blockquote/feature.server.ts b/packages/richtext-lexical/src/field/features/blockquote/feature.server.ts index 7fdf16ef6..633b4da50 100644 --- a/packages/richtext-lexical/src/field/features/blockquote/feature.server.ts +++ b/packages/richtext-lexical/src/field/features/blockquote/feature.server.ts @@ -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 { @@ -33,10 +33,10 @@ export const BlockQuoteFeature: FeatureProviderProviderServer${childrenText}` }, nodeTypes: [QuoteNode.getType()], - } as HTMLConverter, + }, }, node: QuoteNode, - }, + }), ], serverFeatureProps: props, } diff --git a/packages/richtext-lexical/src/field/features/blocks/component/index.tsx b/packages/richtext-lexical/src/field/features/blocks/component/index.tsx index 3bdb403d7..f3f2346a0 100644 --- a/packages/richtext-lexical/src/field/features/blocks/component/index.tsx +++ b/packages/richtext-lexical/src/field/features/blocks/component/index.tsx @@ -125,6 +125,7 @@ export const BlockComponent: React.FC = (props) => { reducedBlock && initialState !== false && (
[] = [] - // 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 diff --git a/packages/richtext-lexical/src/field/features/heading/feature.server.ts b/packages/richtext-lexical/src/field/features/heading/feature.server.ts index 98bb783d9..89ec9825a 100644 --- a/packages/richtext-lexical/src/field/features/heading/feature.server.ts +++ b/packages/richtext-lexical/src/field/features/heading/feature.server.ts @@ -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 + '' }, nodeTypes: [HeadingNode.getType()], - } as HTMLConverter, + }, }, node: HeadingNode, - }, + }), ], serverFeatureProps: props, } diff --git a/packages/richtext-lexical/src/field/features/horizontalrule/feature.server.ts b/packages/richtext-lexical/src/field/features/horizontalrule/feature.server.ts index ce24ba293..47e4a72c5 100644 --- a/packages/richtext-lexical/src/field/features/horizontalrule/feature.server.ts +++ b/packages/richtext-lexical/src/field/features/horizontalrule/feature.server.ts @@ -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 { return `
` }, nodeTypes: [HorizontalRuleNode.getType()], - } as HTMLConverter, + }, }, node: HorizontalRuleNode, - }, + }), ], serverFeatureProps: props, } diff --git a/packages/richtext-lexical/src/field/features/link/feature.server.ts b/packages/richtext-lexical/src/field/features/link/feature.server.ts index 42acc66c6..fe7a96dd1 100644 --- a/packages/richtext-lexical/src/field/features/link/feature.server.ts +++ b/packages/richtext-lexical/src/field/features/link/feature.server.ts @@ -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 { - if (!props?.fields || !Array.isArray(props.fields) || props.fields.length === 0) { - return null - } - const schemaMap = new Map() - - 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() + + const validRelationships = config.collections.map((c) => c.slug) || [] + schemaMap.set('fields', transformedFields) traverseFields({ @@ -96,7 +101,7 @@ export const LinkFeature: FeatureProviderProviderServer { @@ -123,12 +128,19 @@ export const LinkFeature: FeatureProviderProviderServer${childrenText}` }, nodeTypes: [AutoLinkNode.getType()], - } as HTMLConverter, + }, + }, + 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${childrenText}` }, nodeTypes: [LinkNode.getType()], - } as HTMLConverter, + }, }, node: LinkNode, populationPromises: [linkPopulationPromiseHOC(props)], - }, + }), ], serverFeatureProps: props, } diff --git a/packages/richtext-lexical/src/field/features/link/populationPromise.ts b/packages/richtext-lexical/src/field/features/link/populationPromise.ts index fef43c7e3..77466c19b 100644 --- a/packages/richtext-lexical/src/field/features/link/populationPromise.ts +++ b/packages/richtext-lexical/src/field/features/link/populationPromise.ts @@ -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 => { - const linkPopulationPromise: PopulationPromise = ({ + return ({ context, currentDepth, depth, editorPopulationPromises, - field, + fieldPromises, findMany, flattenLocales, node, @@ -21,53 +24,55 @@ export const linkPopulationPromiseHOC = ( populationPromises, req, showHiddenFields, - siblingDoc, }) => { - const promises: Promise[] = [] + // 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 } diff --git a/packages/richtext-lexical/src/field/features/lists/checklist/feature.server.ts b/packages/richtext-lexical/src/field/features/lists/checklist/feature.server.ts index 28f179ad5..7492fc370 100644 --- a/packages/richtext-lexical/src/field/features/lists/checklist/feature.server.ts +++ b/packages/richtext-lexical/src/field/features/lists/checklist/feature.server.ts @@ -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 { 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 = ({ enabledCollectionSlugs } const onSelect = useCallback( ({ collectionSlug, docID }) => { insertRelationship({ - id: docID, editor, relationTo: collectionSlug, replaceNodeKey, + value: docID, }) closeDrawer() }, diff --git a/packages/richtext-lexical/src/field/features/relationship/feature.server.ts b/packages/richtext-lexical/src/field/features/relationship/feature.server.ts index 49e1f6d08..8cba3f190 100644 --- a/packages/richtext-lexical/src/field/features/relationship/feature.server.ts +++ b/packages/richtext-lexical/src/field/features/relationship/feature.server.ts @@ -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, } diff --git a/packages/richtext-lexical/src/field/features/relationship/nodes/RelationshipNode.tsx b/packages/richtext-lexical/src/field/features/relationship/nodes/RelationshipNode.tsx index 85190e0ef..0fd3bb009 100644 --- a/packages/richtext-lexical/src/field/features/relationship/nodes/RelationshipNode.tsx +++ b/packages/richtext-lexical/src/field/features/relationship/nodes/RelationshipNode.tsx @@ -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 @@ -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 ( ) } - 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 { diff --git a/packages/richtext-lexical/src/field/features/relationship/nodes/components/RelationshipComponent.tsx b/packages/richtext-lexical/src/field/features/relationship/nodes/components/RelationshipComponent.tsx index 3618e4b72..660367067 100644 --- a/packages/richtext-lexical/src/field/features/relationship/nodes/components/RelationshipComponent.tsx +++ b/packages/richtext-lexical/src/field/features/relationship/nodes/components/RelationshipComponent.tsx @@ -38,10 +38,7 @@ type Props = { const Component: React.FC = (props) => { const { children, - data: { - relationTo, - value: { id }, - }, + data: { relationTo, value: id }, nodeKey, } = props diff --git a/packages/richtext-lexical/src/field/features/relationship/populationPromise.ts b/packages/richtext-lexical/src/field/features/relationship/populationPromise.ts index 54ed8685a..bdf3a6332 100644 --- a/packages/richtext-lexical/src/field/features/relationship/populationPromise.ts +++ b/packages/richtext-lexical/src/field/features/relationship/populationPromise.ts @@ -9,18 +9,20 @@ export const relationshipPopulationPromise: PopulationPromise { - const promises: Promise[] = [] + 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( + node: NodeWithHooks, +): NodeWithHooks { + return node +} diff --git a/packages/richtext-lexical/src/field/features/types.ts b/packages/richtext-lexical/src/field/features/types.ts index 5a25b64c0..fa5b32f4f 100644 --- a/packages/richtext-lexical/src/field/features/types.ts +++ b/packages/richtext-lexical/src/field/features/types.ts @@ -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> field: RichTextField + /** + * fieldPromises are used for things like field hooks. They will be awaited before awaiting populationPromises + */ + fieldPromises: Promise[] findMany: boolean flattenLocales: boolean node: T @@ -46,7 +57,7 @@ export type PopulationPromise -}) => Promise[] +}) => void export type NodeValidation = ({ node, @@ -171,6 +182,44 @@ export type ClientComponentProps = ClientFeatureProps & { order: number } +export type FieldNodeHookArgs = { + 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 = ( + args: FieldNodeHookArgs, +) => Promise | T + +// Define the node with hooks that use the node's exportJSON return type +export type NodeWithHooks = { + converters?: { + html?: HTMLConverter['exportJSON']>> + } + hooks?: { + afterChange?: Array['exportJSON']>>> + afterRead?: Array['exportJSON']>>> + beforeChange?: Array['exportJSON']>>> + /** + * Runs before a document is duplicated to prevent errors in unique fields or return null to use defaultValue. + */ + beforeDuplicate?: Array['exportJSON']>>> + beforeValidate?: Array['exportJSON']>>> + } + node: Klass | LexicalNodeReplacement + populationPromises?: Array< + PopulationPromise['exportJSON']>> + > + validations?: Array['exportJSON']>>> +} + export type ServerFeature = { ClientComponent?: React.FC> /** @@ -215,26 +264,8 @@ export type ServerFeature = { isRequired: boolean }) => JSONSchema4 } - hooks?: { - afterReadPromise?: ({ - field, - incomingEditorState, - siblingDoc, - }: { - field: RichTextField - incomingEditorState: SerializedEditorState - siblingDoc: Record - }) => Promise | null - } markdownTransformers?: Transformer[] - nodes?: Array<{ - converters?: { - html?: HTMLConverter - } - node: Klass | LexicalNodeReplacement - populationPromises?: Array - validations?: Array - }> + nodes?: Array /** 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 - incomingEditorState: SerializedEditorState - siblingDoc: Record - }) => Promise | null - > - } - /** The node types mapped to their populationPromises */ + /** The node types mapped to their hooks */ + + hooks?: { + afterChange?: Map>> + afterRead?: Map>> + beforeChange?: Map>> + /** + * Runs before a document is duplicated to prevent errors in unique fields or return null to use defaultValue. + */ + beforeDuplicate?: Map>> + beforeValidate?: Map>> + } /** The node types mapped to their populationPromises */ populationPromises: Map> /** The node types mapped to their validations */ validations: Map> diff --git a/packages/richtext-lexical/src/field/features/upload/component/index.tsx b/packages/richtext-lexical/src/field/features/upload/component/index.tsx index 0d373045e..d386f1dd8 100644 --- a/packages/richtext-lexical/src/field/features/upload/component/index.tsx +++ b/packages/richtext-lexical/src/field/features/upload/component/index.tsx @@ -65,13 +65,13 @@ const Component: React.FC = (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 = (props) => { - {value?.id && } + {value && } {hasExtraFields ? ( { 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 = ({ enabledCollectionSlugs }) => { const onSelect = useCallback( ({ collectionSlug, docID }) => { insertUpload({ - id: docID, editor, relationTo: collectionSlug, replaceNodeKey, + value: docID, }) closeDrawer() }, diff --git a/packages/richtext-lexical/src/field/features/upload/feature.server.ts b/packages/richtext-lexical/src/field/features/upload/feature.server.ts index cc9f484d4..7cce4f6e5 100644 --- a/packages/richtext-lexical/src/field/features/upload/feature.server.ts +++ b/packages/richtext-lexical/src/field/features/upload/feature.server.ts @@ -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 `` } - 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 `${uploadDocument.filename}` } @@ -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 += `` } @@ -139,16 +152,16 @@ export const UploadFeature: FeatureProviderProviderServer< pictureHTML += '' return pictureHTML } else { - return `` + return `` } }, nodeTypes: [UploadNode.getType()], - } as HTMLConverter, + }, }, node: UploadNode, populationPromises: [uploadPopulationPromiseHOC(props)], validations: [uploadValidation()], - }, + }), ], serverFeatureProps: props, } diff --git a/packages/richtext-lexical/src/field/features/upload/nodes/UploadNode.tsx b/packages/richtext-lexical/src/field/features/upload/nodes/UploadNode.tsx index 3ca2d7618..f9998b506 100644 --- a/packages/richtext-lexical/src/field/features/upload/nodes/UploadNode.tsx +++ b/packages/richtext-lexical/src/field/features/upload/nodes/UploadNode.tsx @@ -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, } } diff --git a/packages/richtext-lexical/src/field/features/upload/plugin/index.tsx b/packages/richtext-lexical/src/field/features/upload/plugin/index.tsx index 44f31445e..6a0794a76 100644 --- a/packages/richtext-lexical/src/field/features/upload/plugin/index.tsx +++ b/packages/richtext-lexical/src/field/features/upload/plugin/index.tsx @@ -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 +export type InsertUploadPayload = Readonly export const INSERT_UPLOAD_COMMAND: LexicalCommand = 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, }, }) diff --git a/packages/richtext-lexical/src/field/features/upload/populationPromise.ts b/packages/richtext-lexical/src/field/features/upload/populationPromise.ts index 16cb3cfe1..05c89b20f 100644 --- a/packages/richtext-lexical/src/field/features/upload/populationPromise.ts +++ b/packages/richtext-lexical/src/field/features/upload/populationPromise.ts @@ -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 => { - const uploadPopulationPromise: PopulationPromise = ({ + return ({ context, currentDepth, depth, editorPopulationPromises, field, + fieldPromises, findMany, flattenLocales, node, @@ -21,17 +24,19 @@ export const uploadPopulationPromiseHOC = ( populationPromises, req, showHiddenFields, - siblingDoc, }) => { - const promises: Promise[] = [] + 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 } diff --git a/packages/richtext-lexical/src/field/features/upload/validate.ts b/packages/richtext-lexical/src/field/features/upload/validate.ts index e7979f4da..13dc17464 100644 --- a/packages/richtext-lexical/src/field/features/upload/validate.ts +++ b/packages/richtext-lexical/src/field/features/upload/validate.ts @@ -6,7 +6,7 @@ import type { SerializedUploadNode } from './nodes/UploadNode.js' import { CAN_USE_DOM } from '../../lexical/utils/canUseDOM.js' export const uploadValidation = (): NodeValidation => { - const uploadValidation: NodeValidation = ({ + return ({ node, validation: { options: { @@ -16,8 +16,10 @@ export const uploadValidation = (): NodeValidation => { }) => { 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 => { return true } - - return uploadValidation } diff --git a/packages/richtext-lexical/src/field/lexical/config/server/sanitize.ts b/packages/richtext-lexical/src/field/lexical/config/server/sanitize.ts index 9c0cf28af..68a57a497 100644 --- a/packages/richtext-lexical/src/field/lexical/config/server/sanitize.ts +++ b/packages/richtext-lexical/src/field/lexical/config/server/sanitize.ts @@ -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) + } }) } diff --git a/packages/richtext-lexical/src/field/lexical/utils/url.ts b/packages/richtext-lexical/src/field/lexical/utils/url.ts index 8c93af960..3c984eb86 100644 --- a/packages/richtext-lexical/src/field/lexical/utils/url.ts +++ b/packages/richtext-lexical/src/field/lexical/utils/url.ts @@ -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 diff --git a/packages/richtext-lexical/src/index.ts b/packages/richtext-lexical/src/index.ts index ff683312d..0942ccf69 100644 --- a/packages/richtext-lexical/src/index.ts +++ b/packages/richtext-lexical/src/index.ts @@ -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((resolve, reject) => { - const promises: Promise[] = [] - - 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, diff --git a/packages/richtext-lexical/src/populate/populate.ts b/packages/richtext-lexical/src/populate/populate.ts index 96bc83694..33bec5c14 100644 --- a/packages/richtext-lexical/src/populate/populate.ts +++ b/packages/richtext-lexical/src/populate/populate.ts @@ -28,7 +28,7 @@ export const populate = async ({ }: Omit & { collection: Collection field: Field - id: string + id: number | string }): Promise => { const dataRef = data as Record diff --git a/packages/richtext-lexical/src/populate/richTextRelationshipPromise.ts b/packages/richtext-lexical/src/populate/populateLexicalPopulationPromises.ts similarity index 64% rename from packages/richtext-lexical/src/populate/richTextRelationshipPromise.ts rename to packages/richtext-lexical/src/populate/populateLexicalPopulationPromises.ts index b16ede8de..bcfd816c5 100644 --- a/packages/richtext-lexical/src/populate/richTextRelationshipPromise.ts +++ b/packages/richtext-lexical/src/populate/populateLexicalPopulationPromises.ts @@ -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['populationPromise'] + RichTextAdapter['populationPromises'] >[0] & { editorPopulationPromises: Map> } type RecurseRichTextArgs = { children: SerializedLexicalNode[] - currentDepth: number - depth: number - editorPopulationPromises: Map> - field: RichTextField - overrideAccess: boolean - promises: Promise[] - req: PayloadRequest - showHiddenFields: boolean - siblingDoc?: Record } 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 => { - 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) } diff --git a/packages/richtext-lexical/src/populate/recurseNestedFields.ts b/packages/richtext-lexical/src/populate/recurseNestedFields.ts index f1b626bfb..6e957773d 100644 --- a/packages/richtext-lexical/src/populate/recurseNestedFields.ts +++ b/packages/richtext-lexical/src/populate/recurseNestedFields.ts @@ -14,12 +14,15 @@ type NestedRichTextFieldsArgs = { * This maps all the population promises to the node types */ editorPopulationPromises: Map> + /** + * fieldPromises are used for things like field hooks. They should be awaited before awaiting populationPromises + */ + fieldPromises: Promise[] fields: Field[] findMany: boolean flattenLocales: boolean overrideAccess: boolean populationPromises: Promise[] - promises: Promise[] req: PayloadRequest showHiddenFields: boolean siblingDoc: Record @@ -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 }) } diff --git a/packages/richtext-slate/src/data/recurseNestedFields.ts b/packages/richtext-slate/src/data/recurseNestedFields.ts index 8f8409ede..78a68fba2 100644 --- a/packages/richtext-slate/src/data/recurseNestedFields.ts +++ b/packages/richtext-slate/src/data/recurseNestedFields.ts @@ -11,7 +11,7 @@ type NestedRichTextFieldsArgs = { depth: number fields: Field[] overrideAccess: boolean - promises: Promise[] + populationPromises: Promise[] 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, }) diff --git a/packages/richtext-slate/src/data/richTextRelationshipPromise.ts b/packages/richtext-slate/src/data/richTextRelationshipPromise.ts index 40d795dbc..80ed8f0e9 100644 --- a/packages/richtext-slate/src/data/richTextRelationshipPromise.ts +++ b/packages/richtext-slate/src/data/richTextRelationshipPromise.ts @@ -5,7 +5,7 @@ import type { AdapterArguments } from '../types.js' import { populate } from './populate.js' import { recurseNestedFields } from './recurseNestedFields.js' -export type Args = Parameters['populationPromise']>[0] +export type Args = Parameters['populationPromises']>[0] type RecurseRichTextArgs = { children: unknown[] @@ -13,7 +13,7 @@ type RecurseRichTextArgs = { depth: number field: RichTextField overrideAccess: boolean - promises: Promise[] + populationPromises: Promise[] 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 => { - const promises = [] - +}: Args) => { recurseRichText({ children: siblingDoc[field.name] as unknown[], currentDepth, depth, field, overrideAccess, - promises, + populationPromises, req, showHiddenFields, }) - - await Promise.all(promises) } diff --git a/packages/richtext-slate/src/index.tsx b/packages/richtext-slate/src/index.tsx index 3a18a8484..5af47ff2e 100644 --- a/packages/richtext-slate/src/index.tsx +++ b/packages/richtext-slate/src/index.tsx @@ -25,11 +25,12 @@ export function slateEditor(args: AdapterArguments): RichTextAdapter = { siblingData: Data } +// TODO: Make this works for rich text subfields export const defaultValuePromise = async ({ id, data, diff --git a/test/fields/collections/Lexical/e2e.spec.ts b/test/fields/collections/Lexical/e2e.spec.ts index 197413321..c9b942734 100644 --- a/test/fields/collections/Lexical/e2e.spec.ts +++ b/test/fields/collections/Lexical/e2e.spec.ts @@ -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) }) }) }) diff --git a/test/fields/collections/Lexical/generateLexicalRichText.ts b/test/fields/collections/Lexical/generateLexicalRichText.ts index 08bedd6bb..70f864e5e 100644 --- a/test/fields/collections/Lexical/generateLexicalRichText.ts +++ b/test/fields/collections/Lexical/generateLexicalRichText.ts @@ -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: [ diff --git a/test/fields/collections/LexicalLocalized/index.ts b/test/fields/collections/LexicalLocalized/index.ts new file mode 100644 index 000000000..af1530381 --- /dev/null +++ b/test/fields/collections/LexicalLocalized/index.ts @@ -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, + }, + ], + }, + ], + }), + ], + }), + }, + ], +} diff --git a/test/fields/collections/LexicalLocalized/textToLexicalJSON.ts b/test/fields/collections/LexicalLocalized/textToLexicalJSON.ts new file mode 100644 index 000000000..06b355ba4 --- /dev/null +++ b/test/fields/collections/LexicalLocalized/textToLexicalJSON.ts @@ -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 +} diff --git a/test/fields/collections/RichText/generateLexicalRichText.ts b/test/fields/collections/RichText/generateLexicalRichText.ts index 6a373b4ad..ef4b2fd1c 100644 --- a/test/fields/collections/RichText/generateLexicalRichText.ts +++ b/test/fields/collections/RichText/generateLexicalRichText.ts @@ -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: { diff --git a/test/fields/config.ts b/test/fields/config.ts index db0fa8f64..180041161 100644 --- a/test/fields/config.ts +++ b/test/fields/config.ts @@ -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: { diff --git a/test/fields/lexical.int.spec.ts b/test/fields/lexical.int.spec.ts index d82755f6d..3f5aff58d 100644 --- a/test/fields/lexical.int.spec.ts +++ b/test/fields/lexical.int.spec.ts @@ -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 () => { diff --git a/test/fields/seed.ts b/test/fields/seed.ts index 867928f4b..e8709412a 100644 --- a/test/fields/seed.ts +++ b/test/fields/seed.ts @@ -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, diff --git a/test/fields/slugs.ts b/test/fields/slugs.ts index 3b5a07426..105c53eaa 100644 --- a/test/fields/slugs.ts +++ b/test/fields/slugs.ts @@ -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,