chore(richtext-lexical): adjust node data formats, adjust population behavior, fix some validation issues

This commit is contained in:
Alessio Gravili
2023-10-07 15:52:12 +02:00
parent 7e9c7707ba
commit ad7c25685e
22 changed files with 195 additions and 181 deletions

View File

@@ -314,7 +314,7 @@ export const upload: Validate<unknown, unknown, UploadField> = (value: string, o
} }
if (!canUseDOM && typeof value !== 'undefined' && value !== null) { if (!canUseDOM && typeof value !== 'undefined' && value !== null) {
const idField = options.payload.collections[options.relationTo].config.fields.find( const idField = options?.payload?.collections[options.relationTo]?.config?.fields?.find(
(field) => fieldAffectsData(field) && field.name === 'id', (field) => fieldAffectsData(field) && field.name === 'id',
) )
const type = getIDType(idField, options?.payload?.db?.defaultIDType) const type = getIDType(idField, options?.payload?.db?.defaultIDType)

View File

@@ -18,6 +18,7 @@ export const blockAfterReadPromiseHOC = (
overrideAccess, overrideAccess,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}) => { }) => {
const promises: Promise<void>[] = [] const promises: Promise<void>[] = []
@@ -47,6 +48,7 @@ export const blockAfterReadPromiseHOC = (
promises, promises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}) })
} }
}) })

View File

@@ -17,16 +17,17 @@ export const linkAfterReadPromiseHOC = (
overrideAccess, overrideAccess,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}) => { }) => {
const promises: Promise<void>[] = [] const promises: Promise<void>[] = []
if (node?.fields?.doc?.value && node?.fields?.doc?.relationTo) { if (node?.fields?.doc?.value?.id && node?.fields?.doc?.relationTo) {
const collection = req.payload.collections[node?.fields?.doc?.relationTo] const collection = req.payload.collections[node?.fields?.doc?.relationTo]
if (collection) { if (collection) {
promises.push( promises.push(
populate({ populate({
id: node?.fields?.doc.value, id: node?.fields?.doc?.value?.id,
collection, collection,
currentDepth, currentDepth,
data: node?.fields?.doc, data: node?.fields?.doc,
@@ -51,6 +52,7 @@ export const linkAfterReadPromiseHOC = (
promises, promises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}) })
} }
return promises return promises

View File

@@ -35,9 +35,12 @@ export type LinkFields = {
// unknown, custom fields: // unknown, custom fields:
[key: string]: unknown [key: string]: unknown
doc: { doc: {
data?: any // Will be populated in afterRead hook
relationTo: string relationTo: string
value: string value: {
// Actual doc data, populated in afterRead hook
[key: string]: unknown
id: string
}
} | null } | null
linkType: 'custom' | 'internal' linkType: 'custom' | 'internal'
newTab: boolean newTab: boolean
@@ -124,23 +127,10 @@ export class LinkNode extends ElementNode {
element.target = '_blank' element.target = '_blank'
} }
element.rel = ''
if (this.__fields?.newTab === true && this.__fields?.linkType === 'custom') { if (this.__fields?.newTab === true && this.__fields?.linkType === 'custom') {
element.rel = manageRel(element.rel, 'add', 'noopener') element.rel = manageRel(element.rel, 'add', 'noopener')
} }
if (this.__fields?.sponsored ?? false) {
element.rel = manageRel(element.rel, 'add', 'sponsored')
}
if (this.__fields?.nofollow ?? false) {
element.rel = manageRel(element.rel, 'add', 'nofollow')
}
if (this.__fields?.rel !== null) {
element.rel += ` ${this.__rel}`
}
addClassNamesToElement(element, config.theme.link) addClassNamesToElement(element, config.theme.link)
return element return element
} }
@@ -212,9 +202,6 @@ export class LinkNode extends ElementNode {
updateDOM(prevNode: LinkNode, anchor: HTMLAnchorElement, config: EditorConfig): boolean { updateDOM(prevNode: LinkNode, anchor: HTMLAnchorElement, config: EditorConfig): boolean {
const url = this.__fields?.url const url = this.__fields?.url
const newTab = this.__fields?.newTab const newTab = this.__fields?.newTab
const sponsored = this.__fields?.sponsored
const nofollow = this.__fields?.nofollow
const rel = this.__fields?.rel
if (url != null && url !== prevNode.__fields?.url && this.__fields?.linkType === 'custom') { if (url != null && url !== prevNode.__fields?.url && this.__fields?.linkType === 'custom') {
anchor.href = url anchor.href = url
} }
@@ -240,33 +227,6 @@ export class LinkNode extends ElementNode {
} }
} }
if (nofollow !== prevNode.__fields.nofollow) {
if (nofollow ?? false) {
anchor.rel = manageRel(anchor.rel, 'add', 'nofollow')
} else {
anchor.rel = manageRel(anchor.rel, 'remove', 'nofollow')
}
}
if (sponsored !== prevNode.__fields.sponsored) {
if (sponsored ?? false) {
anchor.rel = manageRel(anchor.rel, 'add', 'sponsored')
} else {
anchor.rel = manageRel(anchor.rel, 'remove', 'sponsored')
}
}
// TODO - revisit - I don't think there can be any other rel
// values other than nofollow and noopener - so not
// sure why anchor.rel += rel below
if (rel !== prevNode.__fields.rel) {
if (rel != null) {
anchor.rel += rel
} else {
anchor.removeAttribute('rel')
}
}
return false return false
} }
} }
@@ -281,9 +241,6 @@ function convertAnchorElement(domNode: Node): DOMConversionOutput {
doc: null, doc: null,
linkType: 'custom', linkType: 'custom',
newTab: domNode.getAttribute('target') === '_blank', newTab: domNode.getAttribute('target') === '_blank',
nofollow: domNode.getAttribute('rel')?.includes('nofollow') ?? false,
rel: domNode.getAttribute('rel'),
sponsored: domNode.getAttribute('rel')?.includes('sponsored') ?? false,
url: domNode.getAttribute('href') ?? '', url: domNode.getAttribute('href') ?? '',
}, },
}) })

View File

@@ -109,8 +109,6 @@ export function LinkEditor({
doc: undefined, doc: undefined,
linkType: undefined, linkType: undefined,
newTab: undefined, newTab: undefined,
nofollow: undefined,
sponsored: undefined,
url: '', url: '',
...linkParent.getFields(), ...linkParent.getFields(),
}, },
@@ -124,7 +122,7 @@ export function LinkEditor({
// internal link // internal link
setLinkUrl( setLinkUrl(
`/admin/collections/${linkParent.getFields()?.doc?.relationTo}/${linkParent.getFields() `/admin/collections/${linkParent.getFields()?.doc?.relationTo}/${linkParent.getFields()
?.doc?.value}`, ?.doc?.value?.id}`,
) )
const relatedField = config.collections.find( const relatedField = config.collections.find(
@@ -321,6 +319,12 @@ export function LinkEditor({
const data = reduceFieldsToValues(fields, true) const data = reduceFieldsToValues(fields, true)
if (data?.fields?.doc?.value) {
data.fields.doc.value = {
id: data.fields.doc.value,
}
}
const newLinkPayload: LinkPayload = data as LinkPayload const newLinkPayload: LinkPayload = data as LinkPayload
editor.dispatchCommand(TOGGLE_LINK_COMMAND, newLinkPayload) editor.dispatchCommand(TOGGLE_LINK_COMMAND, newLinkPayload)

View File

@@ -14,19 +14,19 @@ export const relationshipAfterReadPromise: AfterReadPromise<SerializedRelationsh
}) => { }) => {
const promises: Promise<void>[] = [] const promises: Promise<void>[] = []
if (node?.fields?.id) { if (node?.value?.id) {
const collection = req.payload.collections[node?.fields?.relationTo] const collection = req.payload.collections[node?.relationTo]
if (collection) { if (collection) {
promises.push( promises.push(
populate({ populate({
id: node?.fields?.id, id: node.value.id,
collection, collection,
currentDepth, currentDepth,
data: node.fields, data: node,
depth, depth,
field, field,
key: 'data', key: 'value',
overrideAccess, overrideAccess,
req, req,
showHiddenFields, showHiddenFields,

View File

@@ -33,15 +33,16 @@ const insertRelationship = ({
}) => { }) => {
if (!replaceNodeKey) { if (!replaceNodeKey) {
editor.dispatchCommand(INSERT_RELATIONSHIP_COMMAND, { editor.dispatchCommand(INSERT_RELATIONSHIP_COMMAND, {
id,
data: null,
relationTo, relationTo,
value: {
id,
},
}) })
} else { } else {
editor.update(() => { editor.update(() => {
const node = $getNodeByKey(replaceNodeKey) const node = $getNodeByKey(replaceNodeKey)
if (node) { if (node) {
node.replace($createRelationshipNode({ id, data: null, relationTo })) node.replace($createRelationshipNode({ relationTo, value: { id } }))
} }
}) })
} }

View File

@@ -18,18 +18,16 @@ import * as React from 'react'
import { RelationshipComponent } from './components/RelationshipComponent' import { RelationshipComponent } from './components/RelationshipComponent'
export type RelationshipFields = { export type RelationshipData = {
data: null | unknown
id: string
relationTo: string relationTo: string
value: {
// Actual relationship, populated in afterRead hook
[key: string]: unknown
id: string
}
} }
export type SerializedRelationshipNode = Spread< export type SerializedRelationshipNode = Spread<RelationshipData, SerializedDecoratorBlockNode>
{
fields: RelationshipFields
},
SerializedDecoratorBlockNode
>
function relationshipElementToNode(domNode: HTMLDivElement): DOMConversionOutput | null { function relationshipElementToNode(domNode: HTMLDivElement): DOMConversionOutput | null {
const id = domNode.getAttribute('data-lexical-relationship-id') const id = domNode.getAttribute('data-lexical-relationship-id')
@@ -37,9 +35,10 @@ function relationshipElementToNode(domNode: HTMLDivElement): DOMConversionOutput
if (id != null && relationTo != null) { if (id != null && relationTo != null) {
const node = $createRelationshipNode({ const node = $createRelationshipNode({
id,
data: null,
relationTo, relationTo,
value: {
id,
},
}) })
return { node } return { node }
} }
@@ -47,24 +46,24 @@ function relationshipElementToNode(domNode: HTMLDivElement): DOMConversionOutput
} }
export class RelationshipNode extends DecoratorBlockNode { export class RelationshipNode extends DecoratorBlockNode {
__fields: RelationshipFields __data: RelationshipData
constructor({ constructor({
fields, data,
format, format,
key, key,
}: { }: {
fields: RelationshipFields data: RelationshipData
format?: ElementFormatType format?: ElementFormatType
key?: NodeKey key?: NodeKey
}) { }) {
super(format, key) super(format, key)
this.__fields = fields this.__data = data
} }
static clone(node: RelationshipNode): RelationshipNode { static clone(node: RelationshipNode): RelationshipNode {
return new RelationshipNode({ return new RelationshipNode({
fields: node.__fields, data: node.__data,
format: node.__format, format: node.__format,
key: node.__key, key: node.__key,
}) })
@@ -92,7 +91,11 @@ export class RelationshipNode extends DecoratorBlockNode {
} }
static importJSON(serializedNode: SerializedRelationshipNode): RelationshipNode { static importJSON(serializedNode: SerializedRelationshipNode): RelationshipNode {
const node = $createRelationshipNode(serializedNode.fields) const importedData: RelationshipData = {
relationTo: serializedNode.relationTo,
value: serializedNode.value,
}
const node = $createRelationshipNode(importedData)
node.setFormat(serializedNode.format) node.setFormat(serializedNode.format)
return node return node
} }
@@ -104,7 +107,7 @@ export class RelationshipNode extends DecoratorBlockNode {
return ( return (
<RelationshipComponent <RelationshipComponent
className={config.theme.relationship ?? 'LexicalEditorTheme__relationship'} className={config.theme.relationship ?? 'LexicalEditorTheme__relationship'}
fields={this.__fields} data={this.__data}
format={this.__format} format={this.__format}
nodeKey={this.getKey()} nodeKey={this.getKey()}
/> />
@@ -113,8 +116,8 @@ export class RelationshipNode extends DecoratorBlockNode {
exportDOM(): DOMExportOutput { exportDOM(): DOMExportOutput {
const element = document.createElement('div') const element = document.createElement('div')
element.setAttribute('data-lexical-relationship-id', this.__fields?.id) element.setAttribute('data-lexical-relationship-id', this.__data?.value?.id)
element.setAttribute('data-lexical-relationship-relationTo', this.__fields?.relationTo) element.setAttribute('data-lexical-relationship-relationTo', this.__data?.relationTo)
const text = document.createTextNode(this.getTextContent()) const text = document.createTextNode(this.getTextContent())
element.append(text) element.append(text)
@@ -124,14 +127,14 @@ export class RelationshipNode extends DecoratorBlockNode {
exportJSON(): SerializedRelationshipNode { exportJSON(): SerializedRelationshipNode {
return { return {
...super.exportJSON(), ...super.exportJSON(),
fields: this.getFields(), ...this.getData(),
type: this.getType(), type: this.getType(),
version: 1, version: 1,
} }
} }
getFields(): RelationshipFields { getData(): RelationshipData {
return this.getLatest().__fields return this.getLatest().__data
} }
getId(): string { getId(): string {
@@ -139,18 +142,18 @@ export class RelationshipNode extends DecoratorBlockNode {
} }
getTextContent(): string { getTextContent(): string {
return `${this?.__fields?.relationTo} relation to ${this.__fields?.id}` return `${this?.__data?.relationTo} relation to ${this.__data?.value?.id}`
} }
setFields(fields: RelationshipFields): void { setData(data: RelationshipData): void {
const writable = this.getWritable() const writable = this.getWritable()
writable.__fields = fields writable.__data = data
} }
} }
export function $createRelationshipNode(fields: RelationshipFields): RelationshipNode { export function $createRelationshipNode(data: RelationshipData): RelationshipNode {
return new RelationshipNode({ return new RelationshipNode({
fields, data,
}) })
} }

View File

@@ -9,7 +9,7 @@ import { getTranslation } from 'payload/utilities'
import React, { useCallback, useReducer, useState } from 'react' import React, { useCallback, useReducer, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { RelationshipFields } from '../RelationshipNode' import type { RelationshipData } from '../RelationshipNode'
import { useEditorConfigContext } from '../../../../lexical/config/EditorConfigProvider' import { useEditorConfigContext } from '../../../../lexical/config/EditorConfigProvider'
import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from '../../drawer' import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from '../../drawer'
@@ -25,7 +25,7 @@ const initialParams = {
type Props = { type Props = {
children?: React.ReactNode children?: React.ReactNode
className?: string className?: string
fields: RelationshipFields data: RelationshipData
format?: ElementFormatType format?: ElementFormatType
nodeKey?: string nodeKey?: string
} }
@@ -33,7 +33,10 @@ type Props = {
const Component: React.FC<Props> = (props) => { const Component: React.FC<Props> = (props) => {
const { const {
children, children,
fields: { id, relationTo }, data: {
relationTo,
value: { id },
},
nodeKey, nodeKey,
} = props } = props

View File

@@ -5,12 +5,12 @@ import { useConfig } from 'payload/components/utilities'
import { useEffect } from 'react' import { useEffect } from 'react'
import React from 'react' import React from 'react'
import type { RelationshipFields } from '../nodes/RelationshipNode' import type { RelationshipData } from '../nodes/RelationshipNode'
import { RelationshipDrawer } from '../drawer' import { RelationshipDrawer } from '../drawer'
import { $createRelationshipNode, RelationshipNode } from '../nodes/RelationshipNode' import { $createRelationshipNode, RelationshipNode } from '../nodes/RelationshipNode'
export const INSERT_RELATIONSHIP_COMMAND: LexicalCommand<RelationshipFields> = createCommand( export const INSERT_RELATIONSHIP_COMMAND: LexicalCommand<RelationshipData> = createCommand(
'INSERT_RELATIONSHIP_COMMAND', 'INSERT_RELATIONSHIP_COMMAND',
) )
@@ -23,7 +23,7 @@ export default function RelationshipPlugin(): JSX.Element | null {
throw new Error('RelationshipPlugin: RelationshipNode not registered on editor') throw new Error('RelationshipPlugin: RelationshipNode not registered on editor')
} }
return editor.registerCommand<RelationshipFields>( return editor.registerCommand<RelationshipData>(
INSERT_RELATIONSHIP_COMMAND, INSERT_RELATIONSHIP_COMMAND,
(payload) => { (payload) => {
const relationshipNode = $createRelationshipNode(payload) const relationshipNode = $createRelationshipNode(payload)

View File

@@ -17,19 +17,20 @@ export const uploadAfterReadPromiseHOC = (
overrideAccess, overrideAccess,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}) => { }) => {
const promises: Promise<void>[] = [] const promises: Promise<void>[] = []
if (node?.fields?.value?.id) { if (node?.value?.id) {
const collection = req.payload.collections[node?.fields?.relationTo] const collection = req.payload.collections[node?.relationTo]
if (collection) { if (collection) {
promises.push( promises.push(
populate({ populate({
id: node?.fields?.value?.id, id: node?.value?.id,
collection, collection,
currentDepth, currentDepth,
data: node.fields, data: node,
depth, depth,
field, field,
key: 'value', key: 'value',
@@ -39,17 +40,18 @@ export const uploadAfterReadPromiseHOC = (
}), }),
) )
} }
if (Array.isArray(props?.collections?.[node?.fields?.relationTo]?.fields)) { if (Array.isArray(props?.collections?.[node?.relationTo]?.fields)) {
recurseNestedFields({ recurseNestedFields({
afterReadPromises, afterReadPromises,
currentDepth, currentDepth,
data: node.fields || {}, data: node.fields || {},
depth, depth,
fields: props?.collections?.[node?.fields?.relationTo]?.fields, fields: props?.collections?.[node?.relationTo]?.fields,
overrideAccess, overrideAccess,
promises, promises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}) })
} }
} }

View File

@@ -19,7 +19,7 @@ import { useTranslation } from 'react-i18next'
import type { ElementProps } from '..' import type { ElementProps } from '..'
import type { UploadFeatureProps } from '../..' import type { UploadFeatureProps } from '../..'
import type { UploadNode } from '../../nodes/UploadNode' import type { UploadData, UploadNode } from '../../nodes/UploadNode'
import { useEditorConfigContext } from '../../../../lexical/config/EditorConfigProvider' import { useEditorConfigContext } from '../../../../lexical/config/EditorConfigProvider'
@@ -34,9 +34,8 @@ export const ExtraFieldsUploadDrawer: React.FC<
} }
> = (props) => { > = (props) => {
const { const {
data: { fields, relationTo, value },
drawerSlug, drawerSlug,
fields: { relationTo, value },
fields,
nodeKey, nodeKey,
relatedCollection, relatedCollection,
} = props } = props
@@ -69,11 +68,11 @@ export const ExtraFieldsUploadDrawer: React.FC<
editor.update(() => { editor.update(() => {
const uploadNode: UploadNode | null = $getNodeByKey(nodeKey) const uploadNode: UploadNode | null = $getNodeByKey(nodeKey)
if (uploadNode) { if (uploadNode) {
const newFields = { const newData: UploadData = {
...uploadNode.getFields(), ...uploadNode.getData(),
...data, fields: data,
} }
uploadNode.setFields(newFields) uploadNode.setData(newData)
} }
}) })

View File

@@ -13,7 +13,7 @@ import React, { useCallback, useReducer, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { UploadFeatureProps } from '..' import type { UploadFeatureProps } from '..'
import type { UploadFields } from '../nodes/UploadNode' import type { UploadData } from '../nodes/UploadNode'
import { useEditorConfigContext } from '../../../lexical/config/EditorConfigProvider' import { useEditorConfigContext } from '../../../lexical/config/EditorConfigProvider'
import { EnabledRelationshipsCondition } from '../../Relationship/utils/EnabledRelationshipsCondition' import { EnabledRelationshipsCondition } from '../../Relationship/utils/EnabledRelationshipsCondition'
@@ -28,14 +28,14 @@ const initialParams = {
} }
export type ElementProps = { export type ElementProps = {
fields: UploadFields data: UploadData
nodeKey: string nodeKey: string
//uploadProps: UploadFeatureProps //uploadProps: UploadFeatureProps
} }
const Component: React.FC<ElementProps> = (props) => { const Component: React.FC<ElementProps> = (props) => {
const { const {
fields: { relationTo, value }, data: { fields, relationTo, value },
nodeKey, nodeKey,
} = props } = props

View File

@@ -34,6 +34,7 @@ const insertUpload = ({
if (!replaceNodeKey) { if (!replaceNodeKey) {
editor.dispatchCommand(INSERT_UPLOAD_COMMAND, { editor.dispatchCommand(INSERT_UPLOAD_COMMAND, {
id, id,
fields: null,
relationTo, relationTo,
}) })
} else { } else {
@@ -42,7 +43,8 @@ const insertUpload = ({
if (node) { if (node) {
node.replace( node.replace(
$createUploadNode({ $createUploadNode({
fields: { data: {
fields: null,
relationTo, relationTo,
value: { value: {
id, id,

View File

@@ -16,13 +16,19 @@ import * as React from 'react'
const RawUploadComponent = React.lazy(async () => await import('../component')) const RawUploadComponent = React.lazy(async () => await import('../component'))
export interface RawUploadPayload { export interface RawUploadPayload {
fields: {
// unknown, custom fields:
[key: string]: unknown
}
id: string id: string
relationTo: string relationTo: string
} }
export type UploadFields = { export type UploadData = {
// unknown, custom fields: fields: {
[key: string]: unknown // unknown, custom fields:
[key: string]: unknown
}
relationTo: string relationTo: string
value: { value: {
// Actual upload data, populated in afterRead hook // Actual upload data, populated in afterRead hook
@@ -41,32 +47,27 @@ function convertUploadElement(domNode: Node): DOMConversionOutput | null {
return null return null
} }
export type SerializedUploadNode = Spread< export type SerializedUploadNode = Spread<UploadData, SerializedDecoratorBlockNode>
{
fields: UploadFields
},
SerializedDecoratorBlockNode
>
export class UploadNode extends DecoratorBlockNode { export class UploadNode extends DecoratorBlockNode {
__fields: UploadFields __data: UploadData
constructor({ constructor({
fields, data,
format, format,
key, key,
}: { }: {
fields: UploadFields data: UploadData
format?: ElementFormatType format?: ElementFormatType
key?: NodeKey key?: NodeKey
}) { }) {
super(format, key) super(format, key)
this.__fields = fields this.__data = data
} }
static clone(node: UploadNode): UploadNode { static clone(node: UploadNode): UploadNode {
return new UploadNode({ return new UploadNode({
fields: node.__fields, data: node.__data,
format: node.__format, format: node.__format,
key: node.__key, key: node.__key,
}) })
@@ -86,9 +87,13 @@ export class UploadNode extends DecoratorBlockNode {
} }
static importJSON(serializedNode: SerializedUploadNode): UploadNode { static importJSON(serializedNode: SerializedUploadNode): UploadNode {
const node = $createUploadNode({ const importedData: UploadData = {
fields: serializedNode.fields, fields: serializedNode.fields,
}) relationTo: serializedNode.relationTo,
value: serializedNode.value,
}
const node = $createUploadNode({ data: importedData })
node.setFormat(serializedNode.format) node.setFormat(serializedNode.format)
return node return node
@@ -99,9 +104,7 @@ export class UploadNode extends DecoratorBlockNode {
} }
decorate(): JSX.Element { decorate(): JSX.Element {
return ( return <RawUploadComponent data={this.__data} format={this.__format} nodeKey={this.getKey()} />
<RawUploadComponent fields={this.__fields} format={this.__format} nodeKey={this.getKey()} />
)
} }
exportDOM(): DOMExportOutput { exportDOM(): DOMExportOutput {
@@ -114,19 +117,19 @@ export class UploadNode extends DecoratorBlockNode {
exportJSON(): SerializedUploadNode { exportJSON(): SerializedUploadNode {
return { return {
...super.exportJSON(), ...super.exportJSON(),
fields: this.getFields(), ...this.getData(),
type: this.getType(), type: this.getType(),
version: 1, version: 1,
} }
} }
getFields(): UploadFields { getData(): UploadData {
return this.getLatest().__fields return this.getLatest().__data
} }
setFields(fields: UploadFields): void { setData(data: UploadData): void {
const writable = this.getWritable() const writable = this.getWritable()
writable.__fields = fields writable.__data = data
} }
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
@@ -135,8 +138,8 @@ export class UploadNode extends DecoratorBlockNode {
} }
} }
export function $createUploadNode({ fields }: { fields: UploadFields }): UploadNode { export function $createUploadNode({ data }: { data: UploadData }): UploadNode {
return $applyNodeReplacement(new UploadNode({ fields })) return $applyNodeReplacement(new UploadNode({ data }))
} }
export function $isUploadNode(node: LexicalNode | null | undefined): node is UploadNode { export function $isUploadNode(node: LexicalNode | null | undefined): node is UploadNode {

View File

@@ -29,7 +29,8 @@ export function UploadPlugin(): JSX.Element | null {
(payload: InsertUploadPayload) => { (payload: InsertUploadPayload) => {
editor.update(() => { editor.update(() => {
const uploadNode = $createUploadNode({ const uploadNode = $createUploadNode({
fields: { data: {
fields: payload.fields,
relationTo: payload.relationTo, relationTo: payload.relationTo,
value: { value: {
id: payload.id, id: payload.id,

View File

@@ -14,12 +14,12 @@ export const uploadValidation = (): NodeValidation<SerializedUploadNode> => {
}) => { }) => {
if (!CAN_USE_DOM) { if (!CAN_USE_DOM) {
const idField = payloadConfig.collections const idField = payloadConfig.collections
.find(({ slug }) => slug === node.fields.relationTo) .find(({ slug }) => slug === node.relationTo)
.fields.find((field) => fieldAffectsData(field) && field.name === 'id') .fields.find((field) => fieldAffectsData(field) && field.name === 'id')
const type = getIDType(idField, validation?.options?.payload?.db?.defaultIDType) const type = getIDType(idField, validation?.options?.payload?.db?.defaultIDType)
if (!isValidID(node.fields.value.id, type)) { if (!isValidID(node.value?.id, type)) {
return validation.options.t('validation:validUploadID') return validation.options.t('validation:validUploadID')
} }
} }

View File

@@ -19,6 +19,7 @@ export type AfterReadPromise<T extends SerializedLexicalNode = SerializedLexical
overrideAccess, overrideAccess,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}: { }: {
afterReadPromises: Map<string, Array<AfterReadPromise>> afterReadPromises: Map<string, Array<AfterReadPromise>>
currentDepth: number currentDepth: number
@@ -28,6 +29,7 @@ export type AfterReadPromise<T extends SerializedLexicalNode = SerializedLexical
overrideAccess: boolean overrideAccess: boolean
req: PayloadRequest req: PayloadRequest
showHiddenFields: boolean showHiddenFields: boolean
siblingDoc: Record<string, unknown>
}) => Promise<void>[] }) => Promise<void>[]
export type NodeValidation<T extends SerializedLexicalNode = SerializedLexicalNode> = ({ export type NodeValidation<T extends SerializedLexicalNode = SerializedLexicalNode> = ({

View File

@@ -1,6 +1,10 @@
export function cloneDeep<T>(object: T): T { export function cloneDeep<T>(object: T, cache: WeakMap<any, any> = new WeakMap()): T {
if (object === null) return null if (object === null) return null
if (cache.has(object)) {
return cache.get(object)
}
// Handle Date // Handle Date
if (object instanceof Date) { if (object instanceof Date) {
return new Date(object.getTime()) as unknown as T return new Date(object.getTime()) as unknown as T
@@ -11,19 +15,39 @@ export function cloneDeep<T>(object: T): T {
return new RegExp(object.source, object.flags) as unknown as T return new RegExp(object.source, object.flags) as unknown as T
} }
// Handle Array // Handle Map
if (Array.isArray(object)) { if (object instanceof Map) {
return object.map((item) => cloneDeep(item)) as unknown as T const clonedMap = new Map()
cache.set(object, clonedMap)
for (const [key, value] of object.entries()) {
clonedMap.set(key, cloneDeep(value, cache))
}
return clonedMap as unknown as T
} }
// Handle plain Object // Handle Set
if (object instanceof Set) {
const clonedSet = new Set()
cache.set(object, clonedSet)
for (const value of object.values()) {
clonedSet.add(cloneDeep(value, cache))
}
return clonedSet as unknown as T
}
// Handle Array and Object
if (typeof object === 'object' && object !== null) { if (typeof object === 'object' && object !== null) {
const clonedObject: any = {} const clonedObject: any = Array.isArray(object)
? []
: Object.create(Object.getPrototypeOf(object))
cache.set(object, clonedObject)
for (const key in object) { for (const key in object) {
if (object.hasOwnProperty(key)) { if (object.hasOwnProperty(key) || Object.getOwnPropertySymbols(object).includes(key as any)) {
clonedObject[key] = cloneDeep(object[key]) clonedObject[key] = cloneDeep(object[key], cache)
} }
} }
return clonedObject as T return clonedObject as T
} }

View File

@@ -1,13 +1,10 @@
import type { SerializedEditorState } from 'lexical' import type { Field, PayloadRequest, RichTextAdapter } from 'payload/types'
import type { Field, PayloadRequest, RichTextField } from 'payload/types'
import { fieldAffectsData, fieldHasSubFields, fieldIsArrayType } from 'payload/types' import { fieldAffectsData, fieldHasSubFields, fieldIsArrayType } from 'payload/types'
import type { AfterReadPromise } from '../field/features/types' import type { AfterReadPromise } from '../field/features/types'
import type { AdapterProps } from '../types'
import { populate } from './populate' import { populate } from './populate'
import { recurseRichText } from './richTextRelationshipPromise'
type NestedRichTextFieldsArgs = { type NestedRichTextFieldsArgs = {
afterReadPromises: Map<string, Array<AfterReadPromise>> afterReadPromises: Map<string, Array<AfterReadPromise>>
@@ -19,6 +16,7 @@ type NestedRichTextFieldsArgs = {
promises: Promise<void>[] promises: Promise<void>[]
req: PayloadRequest req: PayloadRequest
showHiddenFields: boolean showHiddenFields: boolean
siblingDoc: Record<string, unknown>
} }
export const recurseNestedFields = ({ export const recurseNestedFields = ({
@@ -31,6 +29,7 @@ export const recurseNestedFields = ({
promises, promises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}: NestedRichTextFieldsArgs): void => { }: NestedRichTextFieldsArgs): void => {
fields.forEach((field) => { fields.forEach((field) => {
if (field.type === 'relationship' || field.type === 'upload') { if (field.type === 'relationship' || field.type === 'upload') {
@@ -128,6 +127,7 @@ export const recurseNestedFields = ({
promises, promises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}) })
} else { } else {
recurseNestedFields({ recurseNestedFields({
@@ -140,6 +140,7 @@ export const recurseNestedFields = ({
promises, promises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}) })
} }
} else if (field.type === 'tabs') { } else if (field.type === 'tabs') {
@@ -154,6 +155,7 @@ export const recurseNestedFields = ({
promises, promises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}) })
}) })
} else if (Array.isArray(data[field.name])) { } else if (Array.isArray(data[field.name])) {
@@ -171,6 +173,7 @@ export const recurseNestedFields = ({
promises, promises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}) })
} }
}) })
@@ -188,29 +191,31 @@ export const recurseNestedFields = ({
promises, promises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}) })
}) })
} }
} }
if (field.type === 'richText' && Array.isArray(data[field.name])) { if (field.type === 'richText') {
;(data[field.name] as SerializedEditorState).root.children.forEach((node) => { // TODO: This does not properly work yet. E.g. it does not handle a relationship inside of lexical inside of block inside of lexical
if ('children' in node && Array.isArray(node.children)) { const editor: RichTextAdapter = field?.editor
// This assumes that the richText editor is using lexical and not slate.
// TODO: Throw an error if Slate is used. That would be cursed, who'd do that? if (editor?.afterReadPromise) {
recurseRichText({ const afterReadPromise = editor.afterReadPromise({
afterReadPromises, currentDepth,
children: node.children, depth,
currentDepth, field,
depth, overrideAccess,
field: field as RichTextField<AdapterProps>, req,
overrideAccess, showHiddenFields,
promises, siblingDoc,
req, })
showHiddenFields,
}) if (afterReadPromise) {
promises.push(afterReadPromise)
} }
}) }
} }
}) })
} }

View File

@@ -18,6 +18,7 @@ type RecurseRichTextArgs = {
promises: Promise<void>[] promises: Promise<void>[]
req: PayloadRequest req: PayloadRequest
showHiddenFields: boolean showHiddenFields: boolean
siblingDoc?: Record<string, unknown>
} }
export const recurseRichText = ({ export const recurseRichText = ({
@@ -30,6 +31,7 @@ export const recurseRichText = ({
promises, promises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}: RecurseRichTextArgs): void => { }: RecurseRichTextArgs): void => {
if (depth <= 0 || currentDepth > depth) { if (depth <= 0 || currentDepth > depth) {
return return
@@ -49,6 +51,7 @@ export const recurseRichText = ({
overrideAccess, overrideAccess,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}), }),
) )
} }
@@ -65,6 +68,7 @@ export const recurseRichText = ({
promises, promises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}) })
} }
}) })
@@ -93,6 +97,7 @@ export const richTextRelationshipPromise = async ({
promises, promises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}) })
await Promise.all(promises) await Promise.all(promises)

View File

@@ -90,7 +90,9 @@ export function generateLexicalRichText() {
fields: { fields: {
url: 'https://', url: 'https://',
doc: { doc: {
value: '{{ARRAY_DOC_ID}}', value: {
id: '{{ARRAY_DOC_ID}}',
},
relationTo: 'array-fields', relationTo: 'array-fields',
}, },
newTab: false, newTab: false,
@@ -117,13 +119,10 @@ export function generateLexicalRichText() {
format: '', format: '',
type: 'relationship', type: 'relationship',
version: 1, version: 1,
fields: { value: {
id: '{{TEXT_DOC_ID}}', id: '{{TEXT_DOC_ID}}',
data: {
id: '{{TEXT_DOC_ID}}',
},
relationTo: 'text-fields',
}, },
relationTo: 'text-fields',
}, },
{ {
children: [ children: [
@@ -234,11 +233,11 @@ export function generateLexicalRichText() {
format: '', format: '',
type: 'upload', type: 'upload',
version: 1, version: 1,
relationTo: 'uploads',
value: {
id: '{{UPLOAD_DOC_ID}}',
},
fields: { fields: {
relationTo: 'uploads',
value: {
id: '{{UPLOAD_DOC_ID}}',
},
caption: { caption: {
root: { root: {
type: 'root', type: 'root',