feat(richtext-lexical)!: rework how population works and saves data, improve node typing
This commit is contained in:
@@ -470,19 +470,24 @@ function buildObjectType({
|
|||||||
// is run here again, with the provided depth.
|
// is run here again, with the provided depth.
|
||||||
// In the graphql find.ts resolver, the depth is then hard-coded to 0.
|
// 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.
|
// 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) {
|
if (editor?.populationPromises) {
|
||||||
await editor?.populationPromise({
|
const fieldPromises = []
|
||||||
|
const populationPromises = []
|
||||||
|
editor?.populationPromises({
|
||||||
context,
|
context,
|
||||||
depth,
|
depth,
|
||||||
field,
|
field,
|
||||||
|
fieldPromises,
|
||||||
findMany: false,
|
findMany: false,
|
||||||
flattenLocales: false,
|
flattenLocales: false,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
populationPromises: [],
|
populationPromises,
|
||||||
req: context.req,
|
req: context.req,
|
||||||
showHiddenFields: false,
|
showHiddenFields: false,
|
||||||
siblingDoc: parent,
|
siblingDoc: parent,
|
||||||
})
|
})
|
||||||
|
await Promise.all(fieldPromises)
|
||||||
|
await Promise.all(populationPromises)
|
||||||
}
|
}
|
||||||
|
|
||||||
return parent[field.name]
|
return parent[field.name]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { I18n } from '@payloadcms/translations'
|
|||||||
import type { JSONSchema4 } from 'json-schema'
|
import type { JSONSchema4 } from 'json-schema'
|
||||||
|
|
||||||
import type { SanitizedConfig } from '../config/types.js'
|
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 { PayloadRequest, RequestContext } from '../types/index.js'
|
||||||
import type { WithServerSideProps } from './elements/WithServerSideProps.js'
|
import type { WithServerSideProps } from './elements/WithServerSideProps.js'
|
||||||
|
|
||||||
@@ -19,15 +19,6 @@ type RichTextAdapterBase<
|
|||||||
AdapterProps = any,
|
AdapterProps = any,
|
||||||
ExtraFieldProperties = {},
|
ExtraFieldProperties = {},
|
||||||
> = {
|
> = {
|
||||||
afterReadPromise?: ({
|
|
||||||
field,
|
|
||||||
incomingEditorState,
|
|
||||||
siblingDoc,
|
|
||||||
}: {
|
|
||||||
field: RichTextField<Value, AdapterProps, ExtraFieldProperties>
|
|
||||||
incomingEditorState: Value
|
|
||||||
siblingDoc: Record<string, unknown>
|
|
||||||
}) => Promise<void> | null
|
|
||||||
generateComponentMap: (args: {
|
generateComponentMap: (args: {
|
||||||
WithServerSideProps: WithServerSideProps
|
WithServerSideProps: WithServerSideProps
|
||||||
config: SanitizedConfig
|
config: SanitizedConfig
|
||||||
@@ -40,6 +31,7 @@ type RichTextAdapterBase<
|
|||||||
schemaMap: Map<string, Field[]>
|
schemaMap: Map<string, Field[]>
|
||||||
schemaPath: string
|
schemaPath: string
|
||||||
}) => Map<string, Field[]>
|
}) => Map<string, Field[]>
|
||||||
|
hooks?: FieldBase['hooks']
|
||||||
outputSchema?: ({
|
outputSchema?: ({
|
||||||
collectionIDFieldTypes,
|
collectionIDFieldTypes,
|
||||||
config,
|
config,
|
||||||
@@ -56,11 +48,18 @@ type RichTextAdapterBase<
|
|||||||
interfaceNameDefinitions: Map<string, JSONSchema4>
|
interfaceNameDefinitions: Map<string, JSONSchema4>
|
||||||
isRequired: boolean
|
isRequired: boolean
|
||||||
}) => JSONSchema4
|
}) => 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
|
context: RequestContext
|
||||||
currentDepth?: number
|
currentDepth?: number
|
||||||
depth: number
|
depth: number
|
||||||
field: RichTextField<Value, AdapterProps, ExtraFieldProperties>
|
field: RichTextField<Value, AdapterProps, ExtraFieldProperties>
|
||||||
|
fieldPromises: Promise<void>[]
|
||||||
findMany: boolean
|
findMany: boolean
|
||||||
flattenLocales: boolean
|
flattenLocales: boolean
|
||||||
overrideAccess?: boolean
|
overrideAccess?: boolean
|
||||||
@@ -68,7 +67,7 @@ type RichTextAdapterBase<
|
|||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
showHiddenFields: boolean
|
showHiddenFields: boolean
|
||||||
siblingDoc: Record<string, unknown>
|
siblingDoc: Record<string, unknown>
|
||||||
}) => Promise<void> | null
|
}) => void
|
||||||
validate: Validate<
|
validate: Validate<
|
||||||
Value,
|
Value,
|
||||||
Value,
|
Value,
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ type Args = {
|
|||||||
doc: Record<string, unknown>
|
doc: Record<string, unknown>
|
||||||
fallbackLocale: null | string
|
fallbackLocale: null | string
|
||||||
field: Field | TabAsField
|
field: Field | TabAsField
|
||||||
|
/**
|
||||||
|
* fieldPromises are used for things like field hooks. They should be awaited before awaiting populationPromises
|
||||||
|
*/
|
||||||
fieldPromises: Promise<void>[]
|
fieldPromises: Promise<void>[]
|
||||||
findMany: boolean
|
findMany: boolean
|
||||||
flattenLocales: boolean
|
flattenLocales: boolean
|
||||||
@@ -140,12 +143,13 @@ export const promise = async ({
|
|||||||
case 'richText': {
|
case 'richText': {
|
||||||
const editor: RichTextAdapter = field?.editor
|
const editor: RichTextAdapter = field?.editor
|
||||||
// This is run here AND in the GraphQL Resolver
|
// This is run here AND in the GraphQL Resolver
|
||||||
if (editor?.populationPromise) {
|
if (editor?.populationPromises) {
|
||||||
const populationPromise = editor.populationPromise({
|
editor.populationPromises({
|
||||||
context,
|
context,
|
||||||
currentDepth,
|
currentDepth,
|
||||||
depth,
|
depth,
|
||||||
field,
|
field,
|
||||||
|
fieldPromises,
|
||||||
findMany,
|
findMany,
|
||||||
flattenLocales,
|
flattenLocales,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
@@ -154,12 +158,8 @@ export const promise = async ({
|
|||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (populationPromise) {
|
|
||||||
populationPromises.push(populationPromise)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
// This is only run here, independent of depth
|
// This is only run here, independent of depth
|
||||||
if (editor?.afterReadPromise) {
|
if (editor?.afterReadPromise) {
|
||||||
const afterReadPromise = editor?.afterReadPromise({
|
const afterReadPromise = editor?.afterReadPromise({
|
||||||
@@ -171,7 +171,7 @@ export const promise = async ({
|
|||||||
if (afterReadPromise) {
|
if (afterReadPromise) {
|
||||||
populationPromises.push(afterReadPromise)
|
populationPromises.push(afterReadPromise)
|
||||||
}
|
}
|
||||||
}
|
}*/ //TODO: HOOKS!
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ type Args = {
|
|||||||
depth: number
|
depth: number
|
||||||
doc: Record<string, unknown>
|
doc: Record<string, unknown>
|
||||||
fallbackLocale: null | string
|
fallbackLocale: null | string
|
||||||
|
/**
|
||||||
|
* fieldPromises are used for things like field hooks. They should be awaited before awaiting populationPromises
|
||||||
|
*/
|
||||||
fieldPromises: Promise<void>[]
|
fieldPromises: Promise<void>[]
|
||||||
fields: (Field | TabAsField)[]
|
fields: (Field | TabAsField)[]
|
||||||
findMany: boolean
|
findMany: boolean
|
||||||
|
|||||||
@@ -109,3 +109,7 @@ export type AllOperations = AuthOperations | Operation | VersionOperations
|
|||||||
export function docHasTimestamps(doc: any): doc is TypeWithTimestamps {
|
export function docHasTimestamps(doc: any): doc is TypeWithTimestamps {
|
||||||
return doc?.createdAt && doc?.updatedAt
|
return doc?.createdAt && doc?.updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N // This is a commonly used trick to detect 'any'
|
||||||
|
export type IsAny<T> = IfAny<T, true, false>
|
||||||
|
export type ReplaceAny<T, DefaultType> = IsAny<T> extends true ? DefaultType : T
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import lexicalRichTextImport, { type SerializedQuoteNode } from '@lexical/rich-text'
|
import lexicalRichTextImport from '@lexical/rich-text'
|
||||||
const { QuoteNode } = lexicalRichTextImport
|
const { QuoteNode } = lexicalRichTextImport
|
||||||
|
|
||||||
import type { HTMLConverter } from '../converters/html/converter/types.js'
|
|
||||||
import type { FeatureProviderProviderServer } from '../types.js'
|
import type { FeatureProviderProviderServer } from '../types.js'
|
||||||
|
|
||||||
import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js'
|
import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js'
|
||||||
|
import { createNode } from '../typeUtilities.js'
|
||||||
import { BlockQuoteFeatureClientComponent } from './feature.client.js'
|
import { BlockQuoteFeatureClientComponent } from './feature.client.js'
|
||||||
import { MarkdownTransformer } from './markdownTransformer.js'
|
import { MarkdownTransformer } from './markdownTransformer.js'
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ export const BlockQuoteFeature: FeatureProviderProviderServer<undefined, undefin
|
|||||||
clientFeatureProps: null,
|
clientFeatureProps: null,
|
||||||
markdownTransformers: [MarkdownTransformer],
|
markdownTransformers: [MarkdownTransformer],
|
||||||
nodes: [
|
nodes: [
|
||||||
{
|
createNode({
|
||||||
converters: {
|
converters: {
|
||||||
html: {
|
html: {
|
||||||
converter: async ({ converters, node, parent, payload }) => {
|
converter: async ({ converters, node, parent, payload }) => {
|
||||||
@@ -33,10 +33,10 @@ export const BlockQuoteFeature: FeatureProviderProviderServer<undefined, undefin
|
|||||||
return `<blockquote>${childrenText}</blockquote>`
|
return `<blockquote>${childrenText}</blockquote>`
|
||||||
},
|
},
|
||||||
nodeTypes: [QuoteNode.getType()],
|
nodeTypes: [QuoteNode.getType()],
|
||||||
} as HTMLConverter<SerializedQuoteNode>,
|
},
|
||||||
},
|
},
|
||||||
node: QuoteNode,
|
node: QuoteNode,
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
serverFeatureProps: props,
|
serverFeatureProps: props,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
|||||||
reducedBlock &&
|
reducedBlock &&
|
||||||
initialState !== false && (
|
initialState !== false && (
|
||||||
<Form
|
<Form
|
||||||
|
beforeSubmit={[onChange]}
|
||||||
// @ts-expect-error TODO: Fix this
|
// @ts-expect-error TODO: Fix this
|
||||||
fields={fieldMap}
|
fields={fieldMap}
|
||||||
initialState={initialState}
|
initialState={initialState}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type { FeatureProviderProviderServer } from '../types.js'
|
|||||||
import type { BlocksFeatureClientProps } from './feature.client.js'
|
import type { BlocksFeatureClientProps } from './feature.client.js'
|
||||||
|
|
||||||
import { cloneDeep } from '../../lexical/utils/cloneDeep.js'
|
import { cloneDeep } from '../../lexical/utils/cloneDeep.js'
|
||||||
|
import { createNode } from '../typeUtilities.js'
|
||||||
import { BlocksFeatureClientComponent } from './feature.client.js'
|
import { BlocksFeatureClientComponent } from './feature.client.js'
|
||||||
import { BlockNode } from './nodes/BlocksNode.js'
|
import { BlockNode } from './nodes/BlocksNode.js'
|
||||||
import { blockPopulationPromiseHOC } from './populationPromise.js'
|
import { blockPopulationPromiseHOC } from './populationPromise.js'
|
||||||
@@ -131,11 +132,11 @@ export const BlocksFeature: FeatureProviderProviderServer<
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
nodes: [
|
nodes: [
|
||||||
{
|
createNode({
|
||||||
node: BlockNode,
|
node: BlockNode,
|
||||||
populationPromises: [blockPopulationPromiseHOC(props)],
|
populationPromises: [blockPopulationPromiseHOC(props)],
|
||||||
validations: [blockValidationHOC(props)],
|
validations: [blockValidationHOC(props)],
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
serverFeatureProps: props,
|
serverFeatureProps: props,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const blockPopulationPromiseHOC = (
|
|||||||
currentDepth,
|
currentDepth,
|
||||||
depth,
|
depth,
|
||||||
editorPopulationPromises,
|
editorPopulationPromises,
|
||||||
|
fieldPromises,
|
||||||
findMany,
|
findMany,
|
||||||
flattenLocales,
|
flattenLocales,
|
||||||
node,
|
node,
|
||||||
@@ -28,8 +29,6 @@ export const blockPopulationPromiseHOC = (
|
|||||||
const blocks: Block[] = props.blocks
|
const blocks: Block[] = props.blocks
|
||||||
const blockFieldData = node.fields
|
const blockFieldData = node.fields
|
||||||
|
|
||||||
const promises: Promise<void>[] = []
|
|
||||||
|
|
||||||
// Sanitize block's fields here. This is done here and not in the feature, because the payload config is available here
|
// 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 payloadConfig = req.payload.config
|
||||||
const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
|
const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
|
||||||
@@ -45,7 +44,7 @@ export const blockPopulationPromiseHOC = (
|
|||||||
// find block used in this node
|
// find block used in this node
|
||||||
const block = props.blocks.find((block) => block.slug === blockFieldData.blockType)
|
const block = props.blocks.find((block) => block.slug === blockFieldData.blockType)
|
||||||
if (!block || !block?.fields?.length || !blockFieldData) {
|
if (!block || !block?.fields?.length || !blockFieldData) {
|
||||||
return promises
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
recurseNestedFields({
|
recurseNestedFields({
|
||||||
@@ -54,19 +53,17 @@ export const blockPopulationPromiseHOC = (
|
|||||||
data: blockFieldData,
|
data: blockFieldData,
|
||||||
depth,
|
depth,
|
||||||
editorPopulationPromises,
|
editorPopulationPromises,
|
||||||
|
fieldPromises,
|
||||||
fields: block.fields,
|
fields: block.fields,
|
||||||
findMany,
|
findMany,
|
||||||
flattenLocales,
|
flattenLocales,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
populationPromises,
|
populationPromises,
|
||||||
promises,
|
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
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.
|
// 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,
|
siblingDoc: blockFieldData,
|
||||||
})
|
})
|
||||||
|
|
||||||
return promises
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return blockPopulationPromise
|
return blockPopulationPromise
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { HTMLConverter } from '../converters/html/converter/types.js'
|
|||||||
import type { FeatureProviderProviderServer } from '../types.js'
|
import type { FeatureProviderProviderServer } from '../types.js'
|
||||||
|
|
||||||
import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js'
|
import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js'
|
||||||
|
import { createNode } from '../typeUtilities.js'
|
||||||
import { HeadingFeatureClientComponent } from './feature.client.js'
|
import { HeadingFeatureClientComponent } from './feature.client.js'
|
||||||
import { MarkdownTransformer } from './markdownTransformer.js'
|
import { MarkdownTransformer } from './markdownTransformer.js'
|
||||||
|
|
||||||
@@ -30,8 +31,7 @@ export const HeadingFeature: FeatureProviderProviderServer<
|
|||||||
ClientComponent: HeadingFeatureClientComponent,
|
ClientComponent: HeadingFeatureClientComponent,
|
||||||
markdownTransformers: [MarkdownTransformer(enabledHeadingSizes)],
|
markdownTransformers: [MarkdownTransformer(enabledHeadingSizes)],
|
||||||
nodes: [
|
nodes: [
|
||||||
{
|
createNode({
|
||||||
type: HeadingNode.getType(),
|
|
||||||
converters: {
|
converters: {
|
||||||
html: {
|
html: {
|
||||||
converter: async ({ converters, node, parent, payload }) => {
|
converter: async ({ converters, node, parent, payload }) => {
|
||||||
@@ -48,10 +48,10 @@ export const HeadingFeature: FeatureProviderProviderServer<
|
|||||||
return '<' + node?.tag + '>' + childrenText + '</' + node?.tag + '>'
|
return '<' + node?.tag + '>' + childrenText + '</' + node?.tag + '>'
|
||||||
},
|
},
|
||||||
nodeTypes: [HeadingNode.getType()],
|
nodeTypes: [HeadingNode.getType()],
|
||||||
} as HTMLConverter<SerializedHeadingNode>,
|
},
|
||||||
},
|
},
|
||||||
node: HeadingNode,
|
node: HeadingNode,
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
serverFeatureProps: props,
|
serverFeatureProps: props,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { HTMLConverter } from '../converters/html/converter/types.js'
|
|
||||||
import type { FeatureProviderProviderServer } from '../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 { HorizontalRuleFeatureClientComponent } from './feature.client.js'
|
||||||
import { MarkdownTransformer } from './markdownTransformer.js'
|
import { MarkdownTransformer } from './markdownTransformer.js'
|
||||||
import { HorizontalRuleNode } from './nodes/HorizontalRuleNode.js'
|
import { HorizontalRuleNode } from './nodes/HorizontalRuleNode.js'
|
||||||
@@ -16,17 +15,17 @@ export const HorizontalRuleFeature: FeatureProviderProviderServer<undefined, und
|
|||||||
clientFeatureProps: null,
|
clientFeatureProps: null,
|
||||||
markdownTransformers: [MarkdownTransformer],
|
markdownTransformers: [MarkdownTransformer],
|
||||||
nodes: [
|
nodes: [
|
||||||
{
|
createNode({
|
||||||
converters: {
|
converters: {
|
||||||
html: {
|
html: {
|
||||||
converter: () => {
|
converter: () => {
|
||||||
return `<hr/>`
|
return `<hr/>`
|
||||||
},
|
},
|
||||||
nodeTypes: [HorizontalRuleNode.getType()],
|
nodeTypes: [HorizontalRuleNode.getType()],
|
||||||
} as HTMLConverter<SerializedHorizontalRuleNode>,
|
},
|
||||||
},
|
},
|
||||||
node: HorizontalRuleNode,
|
node: HorizontalRuleNode,
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
serverFeatureProps: props,
|
serverFeatureProps: props,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import type { SanitizedConfig } from 'payload/config'
|
|||||||
import type { Field, FieldWithRichTextRequiredEditor } from 'payload/types'
|
import type { Field, FieldWithRichTextRequiredEditor } from 'payload/types'
|
||||||
|
|
||||||
import { traverseFields } from '@payloadcms/next/utilities'
|
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 { FeatureProviderProviderServer } from '../types.js'
|
||||||
import type { ClientProps } from './feature.client.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 { convertLexicalNodesToHTML } from '../converters/html/converter/index.js'
|
||||||
|
import { createNode } from '../typeUtilities.js'
|
||||||
import { LinkFeatureClientComponent } from './feature.client.js'
|
import { LinkFeatureClientComponent } from './feature.client.js'
|
||||||
import { AutoLinkNode } from './nodes/AutoLinkNode.js'
|
import { AutoLinkNode } from './nodes/AutoLinkNode.js'
|
||||||
import { LinkNode } from './nodes/LinkNode.js'
|
import { LinkNode } from './nodes/LinkNode.js'
|
||||||
@@ -67,21 +67,26 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
|
|||||||
enabledCollections: props.enabledCollections,
|
enabledCollections: props.enabledCollections,
|
||||||
} as ExclusiveLinkCollectionsProps,
|
} as ExclusiveLinkCollectionsProps,
|
||||||
generateSchemaMap: ({ config, i18n, props }) => {
|
generateSchemaMap: ({ config, i18n, props }) => {
|
||||||
if (!props?.fields || !Array.isArray(props.fields) || props.fields.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const schemaMap = new Map<string, Field[]>()
|
|
||||||
|
|
||||||
const validRelationships = config.collections.map((c) => c.slug) || []
|
|
||||||
|
|
||||||
const transformedFields = transformExtraFields(
|
const transformedFields = transformExtraFields(
|
||||||
props.fields,
|
deepCopyObject(props.fields),
|
||||||
config,
|
config,
|
||||||
i18n,
|
i18n,
|
||||||
props.enabledCollections,
|
props.enabledCollections,
|
||||||
props.disabledCollections,
|
props.disabledCollections,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!transformedFields ||
|
||||||
|
!Array.isArray(transformedFields) ||
|
||||||
|
transformedFields.length === 0
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemaMap = new Map<string, Field[]>()
|
||||||
|
|
||||||
|
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||||
|
|
||||||
schemaMap.set('fields', transformedFields)
|
schemaMap.set('fields', transformedFields)
|
||||||
|
|
||||||
traverseFields({
|
traverseFields({
|
||||||
@@ -96,7 +101,7 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
|
|||||||
return schemaMap
|
return schemaMap
|
||||||
},
|
},
|
||||||
nodes: [
|
nodes: [
|
||||||
{
|
createNode({
|
||||||
converters: {
|
converters: {
|
||||||
html: {
|
html: {
|
||||||
converter: async ({ converters, node, parent, payload }) => {
|
converter: async ({ converters, node, parent, payload }) => {
|
||||||
@@ -123,12 +128,19 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
|
|||||||
return `<a href="${href}"${rel}>${childrenText}</a>`
|
return `<a href="${href}"${rel}>${childrenText}</a>`
|
||||||
},
|
},
|
||||||
nodeTypes: [AutoLinkNode.getType()],
|
nodeTypes: [AutoLinkNode.getType()],
|
||||||
} as HTMLConverter<SerializedAutoLinkNode>,
|
},
|
||||||
|
},
|
||||||
|
hooks: {
|
||||||
|
afterRead: [
|
||||||
|
({ node }) => {
|
||||||
|
return node
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
node: AutoLinkNode,
|
node: AutoLinkNode,
|
||||||
populationPromises: [linkPopulationPromiseHOC(props)],
|
populationPromises: [linkPopulationPromiseHOC(props)],
|
||||||
},
|
}),
|
||||||
{
|
createNode({
|
||||||
converters: {
|
converters: {
|
||||||
html: {
|
html: {
|
||||||
converter: async ({ converters, node, parent, payload }) => {
|
converter: async ({ converters, node, parent, payload }) => {
|
||||||
@@ -152,11 +164,11 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
|
|||||||
return `<a href="${href}"${rel}>${childrenText}</a>`
|
return `<a href="${href}"${rel}>${childrenText}</a>`
|
||||||
},
|
},
|
||||||
nodeTypes: [LinkNode.getType()],
|
nodeTypes: [LinkNode.getType()],
|
||||||
} as HTMLConverter<SerializedLinkNode>,
|
},
|
||||||
},
|
},
|
||||||
node: LinkNode,
|
node: LinkNode,
|
||||||
populationPromises: [linkPopulationPromiseHOC(props)],
|
populationPromises: [linkPopulationPromiseHOC(props)],
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
serverFeatureProps: props,
|
serverFeatureProps: props,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
|
import { sanitizeFields } from 'payload/config'
|
||||||
|
import { deepCopyObject } from 'payload/utilities'
|
||||||
|
|
||||||
import type { PopulationPromise } from '../types.js'
|
import type { PopulationPromise } from '../types.js'
|
||||||
import type { LinkFeatureServerProps } from './feature.server.js'
|
import type { LinkFeatureServerProps } from './feature.server.js'
|
||||||
import type { SerializedLinkNode } from './nodes/types.js'
|
import type { SerializedLinkNode } from './nodes/types.js'
|
||||||
|
|
||||||
import { populate } from '../../../populate/populate.js'
|
|
||||||
import { recurseNestedFields } from '../../../populate/recurseNestedFields.js'
|
import { recurseNestedFields } from '../../../populate/recurseNestedFields.js'
|
||||||
|
import { transformExtraFields } from './plugins/floatingLinkEditor/utilities.js'
|
||||||
|
|
||||||
export const linkPopulationPromiseHOC = (
|
export const linkPopulationPromiseHOC = (
|
||||||
props: LinkFeatureServerProps,
|
props: LinkFeatureServerProps,
|
||||||
): PopulationPromise<SerializedLinkNode> => {
|
): PopulationPromise<SerializedLinkNode> => {
|
||||||
const linkPopulationPromise: PopulationPromise<SerializedLinkNode> = ({
|
return ({
|
||||||
context,
|
context,
|
||||||
currentDepth,
|
currentDepth,
|
||||||
depth,
|
depth,
|
||||||
editorPopulationPromises,
|
editorPopulationPromises,
|
||||||
field,
|
fieldPromises,
|
||||||
findMany,
|
findMany,
|
||||||
flattenLocales,
|
flattenLocales,
|
||||||
node,
|
node,
|
||||||
@@ -21,53 +24,55 @@ export const linkPopulationPromiseHOC = (
|
|||||||
populationPromises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc,
|
|
||||||
}) => {
|
}) => {
|
||||||
const promises: Promise<void>[] = []
|
// Sanitize link's fields here. This is done here and not in the feature, because the payload config is available here
|
||||||
|
const payloadConfig = req.payload.config
|
||||||
|
const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
|
||||||
|
|
||||||
if (node?.fields?.doc?.value && node?.fields?.doc?.relationTo) {
|
const transformedFields = transformExtraFields(
|
||||||
const collection = req.payload.collections[node?.fields?.doc?.relationTo]
|
deepCopyObject(props.fields),
|
||||||
|
payloadConfig,
|
||||||
|
req.i18n,
|
||||||
|
props.enabledCollections,
|
||||||
|
props.disabledCollections,
|
||||||
|
)
|
||||||
|
|
||||||
if (collection) {
|
// TODO: Sanitize & transform ahead of time! On startup!
|
||||||
promises.push(
|
const sanitizedFields = sanitizeFields({
|
||||||
populate({
|
config: payloadConfig,
|
||||||
id:
|
fields: transformedFields,
|
||||||
typeof node?.fields?.doc?.value === 'object'
|
requireFieldLevelRichTextEditor: true,
|
||||||
? node?.fields?.doc?.value?.id
|
validRelationships,
|
||||||
: node?.fields?.doc?.value,
|
})
|
||||||
collection,
|
|
||||||
currentDepth,
|
if (!sanitizedFields?.length) {
|
||||||
data: node?.fields?.doc,
|
return
|
||||||
depth,
|
|
||||||
field,
|
|
||||||
key: 'value',
|
|
||||||
overrideAccess,
|
|
||||||
req,
|
|
||||||
showHiddenFields,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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({
|
recurseNestedFields({
|
||||||
context,
|
context,
|
||||||
currentDepth,
|
currentDepth,
|
||||||
data: node.fields || {},
|
data: {
|
||||||
|
fields: node.fields,
|
||||||
|
},
|
||||||
depth,
|
depth,
|
||||||
editorPopulationPromises,
|
editorPopulationPromises,
|
||||||
fields: props.fields,
|
fieldPromises,
|
||||||
|
fields: sanitizedFields,
|
||||||
findMany,
|
findMany,
|
||||||
flattenLocales,
|
flattenLocales,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
populationPromises,
|
populationPromises,
|
||||||
promises,
|
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc: node.fields || {},
|
siblingDoc: {
|
||||||
|
fields: node.fields,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return promises
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return linkPopulationPromise
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const { ListItemNode, ListNode } = lexicalListImport
|
|||||||
|
|
||||||
import type { FeatureProviderProviderServer } from '../../types.js'
|
import type { FeatureProviderProviderServer } from '../../types.js'
|
||||||
|
|
||||||
|
import { createNode } from '../../typeUtilities.js'
|
||||||
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter.js'
|
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter.js'
|
||||||
import { CheckListFeatureClientComponent } from './feature.client.js'
|
import { CheckListFeatureClientComponent } from './feature.client.js'
|
||||||
import { CHECK_LIST } from './markdownTransformers.js'
|
import { CHECK_LIST } from './markdownTransformers.js'
|
||||||
@@ -17,18 +18,18 @@ export const CheckListFeature: FeatureProviderProviderServer<undefined, undefine
|
|||||||
featureProviderMap.has('unorderedlist') || featureProviderMap.has('orderedlist')
|
featureProviderMap.has('unorderedlist') || featureProviderMap.has('orderedlist')
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
{
|
createNode({
|
||||||
converters: {
|
converters: {
|
||||||
html: ListHTMLConverter,
|
html: ListHTMLConverter,
|
||||||
},
|
},
|
||||||
node: ListNode,
|
node: ListNode,
|
||||||
},
|
}),
|
||||||
{
|
createNode({
|
||||||
converters: {
|
converters: {
|
||||||
html: ListItemHTMLConverter,
|
html: ListItemHTMLConverter,
|
||||||
},
|
},
|
||||||
node: ListItemNode,
|
node: ListItemNode,
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
serverFeatureProps: props,
|
serverFeatureProps: props,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const { ListItemNode, ListNode } = lexicalListImport
|
|||||||
|
|
||||||
import type { FeatureProviderProviderServer } from '../../types.js'
|
import type { FeatureProviderProviderServer } from '../../types.js'
|
||||||
|
|
||||||
|
import { createNode } from '../../typeUtilities.js'
|
||||||
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter.js'
|
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter.js'
|
||||||
import { OrderedListFeatureClientComponent } from './feature.client.js'
|
import { OrderedListFeatureClientComponent } from './feature.client.js'
|
||||||
import { ORDERED_LIST } from './markdownTransformer.js'
|
import { ORDERED_LIST } from './markdownTransformer.js'
|
||||||
@@ -16,18 +17,18 @@ export const OrderedListFeature: FeatureProviderProviderServer<undefined, undefi
|
|||||||
nodes: featureProviderMap.has('unorderedlist')
|
nodes: featureProviderMap.has('unorderedlist')
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
{
|
createNode({
|
||||||
converters: {
|
converters: {
|
||||||
html: ListHTMLConverter,
|
html: ListHTMLConverter,
|
||||||
},
|
},
|
||||||
node: ListNode,
|
node: ListNode,
|
||||||
},
|
}),
|
||||||
{
|
createNode({
|
||||||
converters: {
|
converters: {
|
||||||
html: ListItemHTMLConverter,
|
html: ListItemHTMLConverter,
|
||||||
},
|
},
|
||||||
node: ListItemNode,
|
node: ListItemNode,
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
serverFeatureProps: props,
|
serverFeatureProps: props,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const { ListItemNode, ListNode } = lexicalListImport
|
|||||||
|
|
||||||
import type { FeatureProviderProviderServer } from '../../types.js'
|
import type { FeatureProviderProviderServer } from '../../types.js'
|
||||||
|
|
||||||
|
import { createNode } from '../../typeUtilities.js'
|
||||||
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter.js'
|
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter.js'
|
||||||
import { UnorderedListFeatureClientComponent } from './feature.client.js'
|
import { UnorderedListFeatureClientComponent } from './feature.client.js'
|
||||||
import { UNORDERED_LIST } from './markdownTransformer.js'
|
import { UNORDERED_LIST } from './markdownTransformer.js'
|
||||||
@@ -16,18 +17,18 @@ export const UnorderedListFeature: FeatureProviderProviderServer<undefined, unde
|
|||||||
ClientComponent: UnorderedListFeatureClientComponent,
|
ClientComponent: UnorderedListFeatureClientComponent,
|
||||||
markdownTransformers: [UNORDERED_LIST],
|
markdownTransformers: [UNORDERED_LIST],
|
||||||
nodes: [
|
nodes: [
|
||||||
{
|
createNode({
|
||||||
converters: {
|
converters: {
|
||||||
html: ListHTMLConverter,
|
html: ListHTMLConverter,
|
||||||
},
|
},
|
||||||
node: ListNode,
|
node: ListNode,
|
||||||
},
|
}),
|
||||||
{
|
createNode({
|
||||||
converters: {
|
converters: {
|
||||||
html: ListItemHTMLConverter,
|
html: ListItemHTMLConverter,
|
||||||
},
|
},
|
||||||
node: ListItemNode,
|
node: ListItemNode,
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
serverFeatureProps: props,
|
serverFeatureProps: props,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,8 @@ export const _UploadConverter: LexicalPluginNodeConverter = {
|
|||||||
fields,
|
fields,
|
||||||
format: (lexicalPluginNode as any)?.format || '',
|
format: (lexicalPluginNode as any)?.format || '',
|
||||||
relationTo: (lexicalPluginNode as any)?.rawImagePayload?.relationTo,
|
relationTo: (lexicalPluginNode as any)?.rawImagePayload?.relationTo,
|
||||||
value: {
|
value: (lexicalPluginNode as any)?.rawImagePayload?.value?.id || '',
|
||||||
id: (lexicalPluginNode as any)?.rawImagePayload?.value?.id || '',
|
version: 2,
|
||||||
},
|
|
||||||
version: 1,
|
|
||||||
} as const as SerializedUploadNode
|
} as const as SerializedUploadNode
|
||||||
},
|
},
|
||||||
nodeTypes: ['upload'],
|
nodeTypes: ['upload'],
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ export const _SlateRelationshipConverter: SlateNodeConverter = {
|
|||||||
type: 'relationship',
|
type: 'relationship',
|
||||||
format: '',
|
format: '',
|
||||||
relationTo: slateNode.relationTo,
|
relationTo: slateNode.relationTo,
|
||||||
value: {
|
value: slateNode?.value?.id || '',
|
||||||
id: slateNode?.value?.id || '',
|
version: 2,
|
||||||
},
|
|
||||||
version: 1,
|
|
||||||
} as const as SerializedRelationshipNode
|
} as const as SerializedRelationshipNode
|
||||||
},
|
},
|
||||||
nodeTypes: ['relationship'],
|
nodeTypes: ['relationship'],
|
||||||
|
|||||||
@@ -10,10 +10,8 @@ export const _SlateUploadConverter: SlateNodeConverter = {
|
|||||||
},
|
},
|
||||||
format: '',
|
format: '',
|
||||||
relationTo: slateNode.relationTo,
|
relationTo: slateNode.relationTo,
|
||||||
value: {
|
value: slateNode.value?.id || '',
|
||||||
id: slateNode.value?.id || '',
|
version: 2,
|
||||||
},
|
|
||||||
version: 1,
|
|
||||||
} as const as SerializedUploadNode
|
} as const as SerializedUploadNode
|
||||||
},
|
},
|
||||||
nodeTypes: ['upload'],
|
nodeTypes: ['upload'],
|
||||||
|
|||||||
@@ -15,28 +15,26 @@ import { EnabledRelationshipsCondition } from '../utils/EnabledRelationshipsCond
|
|||||||
import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from './commands.js'
|
import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from './commands.js'
|
||||||
|
|
||||||
const insertRelationship = ({
|
const insertRelationship = ({
|
||||||
id,
|
|
||||||
editor,
|
editor,
|
||||||
relationTo,
|
relationTo,
|
||||||
replaceNodeKey,
|
replaceNodeKey,
|
||||||
|
value,
|
||||||
}: {
|
}: {
|
||||||
editor: LexicalEditor
|
editor: LexicalEditor
|
||||||
id: string
|
|
||||||
relationTo: string
|
relationTo: string
|
||||||
replaceNodeKey: null | string
|
replaceNodeKey: null | string
|
||||||
|
value: number | string
|
||||||
}) => {
|
}) => {
|
||||||
if (!replaceNodeKey) {
|
if (!replaceNodeKey) {
|
||||||
editor.dispatchCommand(INSERT_RELATIONSHIP_COMMAND, {
|
editor.dispatchCommand(INSERT_RELATIONSHIP_COMMAND, {
|
||||||
relationTo,
|
relationTo,
|
||||||
value: {
|
value,
|
||||||
id,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const node = $getNodeByKey(replaceNodeKey)
|
const node = $getNodeByKey(replaceNodeKey)
|
||||||
if (node) {
|
if (node) {
|
||||||
node.replace($createRelationshipNode({ relationTo, value: { id } }))
|
node.replace($createRelationshipNode({ relationTo, value }))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -75,10 +73,10 @@ const RelationshipDrawerComponent: React.FC<Props> = ({ enabledCollectionSlugs }
|
|||||||
const onSelect = useCallback(
|
const onSelect = useCallback(
|
||||||
({ collectionSlug, docID }) => {
|
({ collectionSlug, docID }) => {
|
||||||
insertRelationship({
|
insertRelationship({
|
||||||
id: docID,
|
|
||||||
editor,
|
editor,
|
||||||
relationTo: collectionSlug,
|
relationTo: collectionSlug,
|
||||||
replaceNodeKey,
|
replaceNodeKey,
|
||||||
|
value: docID,
|
||||||
})
|
})
|
||||||
closeDrawer()
|
closeDrawer()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { FeatureProviderProviderServer } from '../types.js'
|
import type { FeatureProviderProviderServer } from '../types.js'
|
||||||
|
|
||||||
|
import { createNode } from '../typeUtilities.js'
|
||||||
import { RelationshipFeatureClientComponent } from './feature.client.js'
|
import { RelationshipFeatureClientComponent } from './feature.client.js'
|
||||||
import { RelationshipNode } from './nodes/RelationshipNode.js'
|
import { RelationshipNode } from './nodes/RelationshipNode.js'
|
||||||
import { relationshipPopulationPromise } from './populationPromise.js'
|
import { relationshipPopulationPromise } from './populationPromise.js'
|
||||||
@@ -36,11 +37,11 @@ export const RelationshipFeature: FeatureProviderProviderServer<
|
|||||||
ClientComponent: RelationshipFeatureClientComponent,
|
ClientComponent: RelationshipFeatureClientComponent,
|
||||||
clientFeatureProps: props,
|
clientFeatureProps: props,
|
||||||
nodes: [
|
nodes: [
|
||||||
{
|
createNode({
|
||||||
node: RelationshipNode,
|
node: RelationshipNode,
|
||||||
populationPromises: [relationshipPopulationPromise],
|
populationPromises: [relationshipPopulationPromise],
|
||||||
// TODO: Add validation similar to upload
|
// TODO: Add validation similar to upload
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
serverFeatureProps: props,
|
serverFeatureProps: props,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,11 +25,7 @@ const RelationshipComponent = React.lazy(() =>
|
|||||||
|
|
||||||
export type RelationshipData = {
|
export type RelationshipData = {
|
||||||
relationTo: string
|
relationTo: string
|
||||||
value: {
|
value: number | string
|
||||||
// Actual relationship, populated in afterRead hook
|
|
||||||
[key: string]: unknown
|
|
||||||
id: string
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SerializedRelationshipNode = Spread<RelationshipData, SerializedDecoratorBlockNode>
|
export type SerializedRelationshipNode = Spread<RelationshipData, SerializedDecoratorBlockNode>
|
||||||
@@ -41,9 +37,7 @@ function relationshipElementToNode(domNode: HTMLDivElement): DOMConversionOutput
|
|||||||
if (id != null && relationTo != null) {
|
if (id != null && relationTo != null) {
|
||||||
const node = $createRelationshipNode({
|
const node = $createRelationshipNode({
|
||||||
relationTo,
|
relationTo,
|
||||||
value: {
|
value: id,
|
||||||
id,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
return { node }
|
return { node }
|
||||||
}
|
}
|
||||||
@@ -96,6 +90,10 @@ export class RelationshipNode extends DecoratorBlockNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static importJSON(serializedNode: SerializedRelationshipNode): RelationshipNode {
|
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 = {
|
const importedData: RelationshipData = {
|
||||||
relationTo: serializedNode.relationTo,
|
relationTo: serializedNode.relationTo,
|
||||||
value: serializedNode.value,
|
value: serializedNode.value,
|
||||||
@@ -108,6 +106,7 @@ export class RelationshipNode extends DecoratorBlockNode {
|
|||||||
static isInline(): false {
|
static isInline(): false {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element {
|
decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<RelationshipComponent
|
<RelationshipComponent
|
||||||
@@ -118,10 +117,9 @@ 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.__data?.value?.id)
|
element.setAttribute('data-lexical-relationship-id', String(this.__data?.value))
|
||||||
element.setAttribute('data-lexical-relationship-relationTo', this.__data?.relationTo)
|
element.setAttribute('data-lexical-relationship-relationTo', this.__data?.relationTo)
|
||||||
|
|
||||||
const text = document.createTextNode(this.getTextContent())
|
const text = document.createTextNode(this.getTextContent())
|
||||||
@@ -134,7 +132,7 @@ export class RelationshipNode extends DecoratorBlockNode {
|
|||||||
...super.exportJSON(),
|
...super.exportJSON(),
|
||||||
...this.getData(),
|
...this.getData(),
|
||||||
type: this.getType(),
|
type: this.getType(),
|
||||||
version: 1,
|
version: 2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +141,7 @@ export class RelationshipNode extends DecoratorBlockNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getTextContent(): string {
|
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 {
|
setData(data: RelationshipData): void {
|
||||||
|
|||||||
@@ -38,10 +38,7 @@ type Props = {
|
|||||||
const Component: React.FC<Props> = (props) => {
|
const Component: React.FC<Props> = (props) => {
|
||||||
const {
|
const {
|
||||||
children,
|
children,
|
||||||
data: {
|
data: { relationTo, value: id },
|
||||||
relationTo,
|
|
||||||
value: { id },
|
|
||||||
},
|
|
||||||
nodeKey,
|
nodeKey,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
|
|||||||
@@ -9,18 +9,20 @@ export const relationshipPopulationPromise: PopulationPromise<SerializedRelation
|
|||||||
field,
|
field,
|
||||||
node,
|
node,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
}) => {
|
}) => {
|
||||||
const promises: Promise<void>[] = []
|
if (node?.value) {
|
||||||
|
// @ts-expect-error
|
||||||
|
const id = node?.value?.id || node?.value // for backwards-compatibility
|
||||||
|
|
||||||
if (node?.value?.id) {
|
|
||||||
const collection = req.payload.collections[node?.relationTo]
|
const collection = req.payload.collections[node?.relationTo]
|
||||||
|
|
||||||
if (collection) {
|
if (collection) {
|
||||||
promises.push(
|
populationPromises.push(
|
||||||
populate({
|
populate({
|
||||||
id: node.value.id,
|
id,
|
||||||
collection,
|
collection,
|
||||||
currentDepth,
|
currentDepth,
|
||||||
data: node,
|
data: node,
|
||||||
@@ -34,6 +36,4 @@ export const relationshipPopulationPromise: PopulationPromise<SerializedRelation
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return promises
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import type { LexicalNode } from 'lexical'
|
||||||
|
|
||||||
|
import type { NodeWithHooks } from './types.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to create a node with hooks. You don't have to use this utility, but it improves type inference
|
||||||
|
* @param node the node
|
||||||
|
*/
|
||||||
|
export function createNode<Node extends LexicalNode>(
|
||||||
|
node: NodeWithHooks<Node>,
|
||||||
|
): NodeWithHooks<Node> {
|
||||||
|
return node
|
||||||
|
}
|
||||||
@@ -6,7 +6,13 @@ import type { SerializedLexicalNode } from 'lexical'
|
|||||||
import type { LexicalNodeReplacement } from 'lexical'
|
import type { LexicalNodeReplacement } from 'lexical'
|
||||||
import type { RequestContext } from 'payload'
|
import type { RequestContext } from 'payload'
|
||||||
import type { SanitizedConfig } from 'payload/config'
|
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 React from 'react'
|
||||||
|
|
||||||
import type { AdapterProps } from '../../types.js'
|
import type { AdapterProps } from '../../types.js'
|
||||||
@@ -21,6 +27,7 @@ export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexica
|
|||||||
depth,
|
depth,
|
||||||
editorPopulationPromises,
|
editorPopulationPromises,
|
||||||
field,
|
field,
|
||||||
|
fieldPromises,
|
||||||
findMany,
|
findMany,
|
||||||
flattenLocales,
|
flattenLocales,
|
||||||
node,
|
node,
|
||||||
@@ -38,6 +45,10 @@ export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexica
|
|||||||
*/
|
*/
|
||||||
editorPopulationPromises: Map<string, Array<PopulationPromise>>
|
editorPopulationPromises: Map<string, Array<PopulationPromise>>
|
||||||
field: RichTextField<SerializedEditorState, AdapterProps>
|
field: RichTextField<SerializedEditorState, AdapterProps>
|
||||||
|
/**
|
||||||
|
* fieldPromises are used for things like field hooks. They will be awaited before awaiting populationPromises
|
||||||
|
*/
|
||||||
|
fieldPromises: Promise<void>[]
|
||||||
findMany: boolean
|
findMany: boolean
|
||||||
flattenLocales: boolean
|
flattenLocales: boolean
|
||||||
node: T
|
node: T
|
||||||
@@ -46,7 +57,7 @@ export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexica
|
|||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
showHiddenFields: boolean
|
showHiddenFields: boolean
|
||||||
siblingDoc: Record<string, unknown>
|
siblingDoc: Record<string, unknown>
|
||||||
}) => Promise<void>[]
|
}) => void
|
||||||
|
|
||||||
export type NodeValidation<T extends SerializedLexicalNode = SerializedLexicalNode> = ({
|
export type NodeValidation<T extends SerializedLexicalNode = SerializedLexicalNode> = ({
|
||||||
node,
|
node,
|
||||||
@@ -171,6 +182,44 @@ export type ClientComponentProps<ClientFeatureProps> = ClientFeatureProps & {
|
|||||||
order: number
|
order: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FieldNodeHookArgs<T extends SerializedLexicalNode> = {
|
||||||
|
context: RequestContext
|
||||||
|
/** Boolean to denote if this hook is running against finding one, or finding many within the afterRead hook. */
|
||||||
|
findMany?: boolean
|
||||||
|
/** The value of the field. */
|
||||||
|
node?: T
|
||||||
|
/** A string relating to which operation the field type is currently executing within. Useful within beforeValidate, beforeChange, and afterChange hooks to differentiate between create and update operations. */
|
||||||
|
operation?: 'create' | 'delete' | 'read' | 'update'
|
||||||
|
/** The Express request object. It is mocked for Local API operations. */
|
||||||
|
req: PayloadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FieldNodeHook<T extends SerializedLexicalNode> = (
|
||||||
|
args: FieldNodeHookArgs<T>,
|
||||||
|
) => Promise<T> | T
|
||||||
|
|
||||||
|
// Define the node with hooks that use the node's exportJSON return type
|
||||||
|
export type NodeWithHooks<T extends LexicalNode = any> = {
|
||||||
|
converters?: {
|
||||||
|
html?: HTMLConverter<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>
|
||||||
|
}
|
||||||
|
hooks?: {
|
||||||
|
afterChange?: Array<FieldNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
|
||||||
|
afterRead?: Array<FieldNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
|
||||||
|
beforeChange?: Array<FieldNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
|
||||||
|
/**
|
||||||
|
* Runs before a document is duplicated to prevent errors in unique fields or return null to use defaultValue.
|
||||||
|
*/
|
||||||
|
beforeDuplicate?: Array<FieldNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
|
||||||
|
beforeValidate?: Array<FieldNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
|
||||||
|
}
|
||||||
|
node: Klass<T> | LexicalNodeReplacement
|
||||||
|
populationPromises?: Array<
|
||||||
|
PopulationPromise<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>
|
||||||
|
>
|
||||||
|
validations?: Array<NodeValidation<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
|
||||||
|
}
|
||||||
|
|
||||||
export type ServerFeature<ServerProps, ClientFeatureProps> = {
|
export type ServerFeature<ServerProps, ClientFeatureProps> = {
|
||||||
ClientComponent?: React.FC<ClientComponentProps<ClientFeatureProps>>
|
ClientComponent?: React.FC<ClientComponentProps<ClientFeatureProps>>
|
||||||
/**
|
/**
|
||||||
@@ -215,26 +264,8 @@ export type ServerFeature<ServerProps, ClientFeatureProps> = {
|
|||||||
isRequired: boolean
|
isRequired: boolean
|
||||||
}) => JSONSchema4
|
}) => JSONSchema4
|
||||||
}
|
}
|
||||||
hooks?: {
|
|
||||||
afterReadPromise?: ({
|
|
||||||
field,
|
|
||||||
incomingEditorState,
|
|
||||||
siblingDoc,
|
|
||||||
}: {
|
|
||||||
field: RichTextField<SerializedEditorState, AdapterProps>
|
|
||||||
incomingEditorState: SerializedEditorState
|
|
||||||
siblingDoc: Record<string, unknown>
|
|
||||||
}) => Promise<void> | null
|
|
||||||
}
|
|
||||||
markdownTransformers?: Transformer[]
|
markdownTransformers?: Transformer[]
|
||||||
nodes?: Array<{
|
nodes?: Array<NodeWithHooks>
|
||||||
converters?: {
|
|
||||||
html?: HTMLConverter
|
|
||||||
}
|
|
||||||
node: Klass<LexicalNode> | LexicalNodeReplacement
|
|
||||||
populationPromises?: Array<PopulationPromise>
|
|
||||||
validations?: Array<NodeValidation>
|
|
||||||
}>
|
|
||||||
|
|
||||||
/** 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 */
|
/** 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
|
serverFeatureProps: ServerProps
|
||||||
@@ -325,20 +356,18 @@ export type SanitizedServerFeatures = Required<
|
|||||||
}) => JSONSchema4
|
}) => JSONSchema4
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
hooks: {
|
/** The node types mapped to their hooks */
|
||||||
afterReadPromises: Array<
|
|
||||||
({
|
hooks?: {
|
||||||
field,
|
afterChange?: Map<string, Array<FieldNodeHook<SerializedLexicalNode>>>
|
||||||
incomingEditorState,
|
afterRead?: Map<string, Array<FieldNodeHook<SerializedLexicalNode>>>
|
||||||
siblingDoc,
|
beforeChange?: Map<string, Array<FieldNodeHook<SerializedLexicalNode>>>
|
||||||
}: {
|
/**
|
||||||
field: RichTextField<SerializedEditorState, AdapterProps>
|
* Runs before a document is duplicated to prevent errors in unique fields or return null to use defaultValue.
|
||||||
incomingEditorState: SerializedEditorState
|
*/
|
||||||
siblingDoc: Record<string, unknown>
|
beforeDuplicate?: Map<string, Array<FieldNodeHook<SerializedLexicalNode>>>
|
||||||
}) => Promise<void> | null
|
beforeValidate?: Map<string, Array<FieldNodeHook<SerializedLexicalNode>>>
|
||||||
>
|
} /** The node types mapped to their populationPromises */
|
||||||
}
|
|
||||||
/** The node types mapped to their populationPromises */
|
|
||||||
populationPromises: Map<string, Array<PopulationPromise>>
|
populationPromises: Map<string, Array<PopulationPromise>>
|
||||||
/** The node types mapped to their validations */
|
/** The node types mapped to their validations */
|
||||||
validations: Map<string, Array<NodeValidation>>
|
validations: Map<string, Array<NodeValidation>>
|
||||||
|
|||||||
@@ -65,13 +65,13 @@ const Component: React.FC<ElementProps> = (props) => {
|
|||||||
const drawerSlug = useDrawerSlug('upload-drawer')
|
const drawerSlug = useDrawerSlug('upload-drawer')
|
||||||
|
|
||||||
const [DocumentDrawer, DocumentDrawerToggler, { closeDrawer }] = useDocumentDrawer({
|
const [DocumentDrawer, DocumentDrawerToggler, { closeDrawer }] = useDocumentDrawer({
|
||||||
id: value?.id,
|
id: value,
|
||||||
collectionSlug: relatedCollection.slug,
|
collectionSlug: relatedCollection.slug,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Get the referenced document
|
// Get the referenced document
|
||||||
const [{ data }, { setParams }] = usePayloadAPI(
|
const [{ data }, { setParams }] = usePayloadAPI(
|
||||||
`${serverURL}${api}/${relatedCollection.slug}/${value?.id}`,
|
`${serverURL}${api}/${relatedCollection.slug}/${value}`,
|
||||||
{ initialParams },
|
{ initialParams },
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ const Component: React.FC<ElementProps> = (props) => {
|
|||||||
</DocumentDrawerToggler>
|
</DocumentDrawerToggler>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{value?.id && <DocumentDrawer onSave={updateUpload} />}
|
{value && <DocumentDrawer onSave={updateUpload} />}
|
||||||
{hasExtraFields ? (
|
{hasExtraFields ? (
|
||||||
<ExtraFieldsUploadDrawer
|
<ExtraFieldsUploadDrawer
|
||||||
drawerSlug={drawerSlug}
|
drawerSlug={drawerSlug}
|
||||||
|
|||||||
@@ -17,21 +17,21 @@ import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from './commands.js'
|
|||||||
const baseClass = 'lexical-upload-drawer'
|
const baseClass = 'lexical-upload-drawer'
|
||||||
|
|
||||||
const insertUpload = ({
|
const insertUpload = ({
|
||||||
id,
|
|
||||||
editor,
|
editor,
|
||||||
relationTo,
|
relationTo,
|
||||||
replaceNodeKey,
|
replaceNodeKey,
|
||||||
|
value,
|
||||||
}: {
|
}: {
|
||||||
editor: LexicalEditor
|
editor: LexicalEditor
|
||||||
id: string
|
|
||||||
relationTo: string
|
relationTo: string
|
||||||
replaceNodeKey: null | string
|
replaceNodeKey: null | string
|
||||||
|
value: number | string
|
||||||
}) => {
|
}) => {
|
||||||
if (!replaceNodeKey) {
|
if (!replaceNodeKey) {
|
||||||
editor.dispatchCommand(INSERT_UPLOAD_COMMAND, {
|
editor.dispatchCommand(INSERT_UPLOAD_COMMAND, {
|
||||||
id,
|
|
||||||
fields: null,
|
fields: null,
|
||||||
relationTo,
|
relationTo,
|
||||||
|
value,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
@@ -42,9 +42,7 @@ const insertUpload = ({
|
|||||||
data: {
|
data: {
|
||||||
fields: null,
|
fields: null,
|
||||||
relationTo,
|
relationTo,
|
||||||
value: {
|
value,
|
||||||
id,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -84,10 +82,10 @@ const UploadDrawerComponent: React.FC<Props> = ({ enabledCollectionSlugs }) => {
|
|||||||
const onSelect = useCallback(
|
const onSelect = useCallback(
|
||||||
({ collectionSlug, docID }) => {
|
({ collectionSlug, docID }) => {
|
||||||
insertUpload({
|
insertUpload({
|
||||||
id: docID,
|
|
||||||
editor,
|
editor,
|
||||||
relationTo: collectionSlug,
|
relationTo: collectionSlug,
|
||||||
replaceNodeKey,
|
replaceNodeKey,
|
||||||
|
value: docID,
|
||||||
})
|
})
|
||||||
closeDrawer()
|
closeDrawer()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 { traverseFields } from '@payloadcms/next/utilities'
|
||||||
|
|
||||||
import type { HTMLConverter } from '../converters/html/converter/types.js'
|
|
||||||
import type { FeatureProviderProviderServer } from '../types.js'
|
import type { FeatureProviderProviderServer } from '../types.js'
|
||||||
import type { UploadFeaturePropsClient } from './feature.client.js'
|
import type { UploadFeaturePropsClient } from './feature.client.js'
|
||||||
|
|
||||||
|
import { createNode } from '../typeUtilities.js'
|
||||||
import { UploadFeatureClientComponent } from './feature.client.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 { uploadPopulationPromiseHOC } from './populationPromise.js'
|
||||||
import { uploadValidation } from './validate.js'
|
import { uploadValidation } from './validate.js'
|
||||||
|
|
||||||
@@ -71,17 +78,21 @@ export const UploadFeature: FeatureProviderProviderServer<
|
|||||||
return schemaMap
|
return schemaMap
|
||||||
},
|
},
|
||||||
nodes: [
|
nodes: [
|
||||||
{
|
createNode({
|
||||||
converters: {
|
converters: {
|
||||||
html: {
|
html: {
|
||||||
converter: async ({ node, payload }) => {
|
converter: async ({ node, payload }) => {
|
||||||
|
// @ts-expect-error
|
||||||
|
const id = node?.value?.id || node?.value // for backwards-compatibility
|
||||||
|
|
||||||
if (payload) {
|
if (payload) {
|
||||||
let uploadDocument: any
|
let uploadDocument: TypeWithID & FileData
|
||||||
|
|
||||||
try {
|
try {
|
||||||
uploadDocument = await payload.findByID({
|
uploadDocument = (await payload.findByID({
|
||||||
id: node.value.id,
|
id,
|
||||||
collection: node.relationTo,
|
collection: node.relationTo,
|
||||||
})
|
})) as TypeWithID & FileData
|
||||||
} catch (ignored) {
|
} catch (ignored) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(
|
console.error(
|
||||||
@@ -93,12 +104,12 @@ export const UploadFeature: FeatureProviderProviderServer<
|
|||||||
return `<img />`
|
return `<img />`
|
||||||
}
|
}
|
||||||
|
|
||||||
const url: string = getAbsoluteURL(uploadDocument?.url as string, payload)
|
const url = getAbsoluteURL(uploadDocument?.url, payload)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the upload is not an image, return a link to the upload
|
* If the upload is not an image, return a link to the upload
|
||||||
*/
|
*/
|
||||||
if (!(uploadDocument?.mimeType as string)?.startsWith('image')) {
|
if (!uploadDocument?.mimeType?.startsWith('image')) {
|
||||||
return `<a href="${url}" rel="noopener noreferrer">${uploadDocument.filename}</a>`
|
return `<a href="${url}" rel="noopener noreferrer">${uploadDocument.filename}</a>`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +127,9 @@ export const UploadFeature: FeatureProviderProviderServer<
|
|||||||
|
|
||||||
// Iterate through each size in the data.sizes object
|
// Iterate through each size in the data.sizes object
|
||||||
for (const size in uploadDocument.sizes) {
|
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
|
// Skip if any property of the size object is null
|
||||||
if (
|
if (
|
||||||
@@ -129,7 +142,7 @@ export const UploadFeature: FeatureProviderProviderServer<
|
|||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const imageSizeURL: string = getAbsoluteURL(imageSize?.url as string, payload)
|
const imageSizeURL = getAbsoluteURL(imageSize?.url, payload)
|
||||||
|
|
||||||
pictureHTML += `<source srcset="${imageSizeURL}" media="(max-width: ${imageSize.width}px)" type="${imageSize.mimeType}">`
|
pictureHTML += `<source srcset="${imageSizeURL}" media="(max-width: ${imageSize.width}px)" type="${imageSize.mimeType}">`
|
||||||
}
|
}
|
||||||
@@ -139,16 +152,16 @@ export const UploadFeature: FeatureProviderProviderServer<
|
|||||||
pictureHTML += '</picture>'
|
pictureHTML += '</picture>'
|
||||||
return pictureHTML
|
return pictureHTML
|
||||||
} else {
|
} else {
|
||||||
return `<img src="${node.value.id}" />`
|
return `<img src="${id}" />`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
nodeTypes: [UploadNode.getType()],
|
nodeTypes: [UploadNode.getType()],
|
||||||
} as HTMLConverter<SerializedUploadNode>,
|
},
|
||||||
},
|
},
|
||||||
node: UploadNode,
|
node: UploadNode,
|
||||||
populationPromises: [uploadPopulationPromiseHOC(props)],
|
populationPromises: [uploadPopulationPromiseHOC(props)],
|
||||||
validations: [uploadValidation()],
|
validations: [uploadValidation()],
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
serverFeatureProps: props,
|
serverFeatureProps: props,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,26 +21,13 @@ const RawUploadComponent = React.lazy(() =>
|
|||||||
import('../component/index.js').then((module) => ({ default: module.UploadComponent })),
|
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 = {
|
export type UploadData = {
|
||||||
fields: {
|
fields: {
|
||||||
// unknown, custom fields:
|
// unknown, custom fields:
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
relationTo: string
|
relationTo: string
|
||||||
value: {
|
value: number | string
|
||||||
// Actual upload data, populated in afterRead hook
|
|
||||||
[key: string]: unknown
|
|
||||||
id: string
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertUploadElement(domNode: Node): DOMConversionOutput | null {
|
function convertUploadElement(domNode: Node): DOMConversionOutput | null {
|
||||||
@@ -93,6 +80,10 @@ export class UploadNode extends DecoratorBlockNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static importJSON(serializedNode: SerializedUploadNode): UploadNode {
|
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 = {
|
const importedData: UploadData = {
|
||||||
fields: serializedNode.fields,
|
fields: serializedNode.fields,
|
||||||
relationTo: serializedNode.relationTo,
|
relationTo: serializedNode.relationTo,
|
||||||
@@ -126,7 +117,7 @@ export class UploadNode extends DecoratorBlockNode {
|
|||||||
...super.exportJSON(),
|
...super.exportJSON(),
|
||||||
...this.getData(),
|
...this.getData(),
|
||||||
type: this.getType(),
|
type: this.getType(),
|
||||||
version: 1,
|
version: 2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ import type { LexicalCommand } from 'lexical'
|
|||||||
import { useConfig } from '@payloadcms/ui/providers/Config'
|
import { useConfig } from '@payloadcms/ui/providers/Config'
|
||||||
import React, { useEffect } from 'react'
|
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 { UploadDrawer } from '../drawer/index.js'
|
||||||
import { $createUploadNode, UploadNode } from '../nodes/UploadNode.js'
|
import { $createUploadNode, UploadNode } from '../nodes/UploadNode.js'
|
||||||
|
|
||||||
export type InsertUploadPayload = Readonly<RawUploadPayload>
|
export type InsertUploadPayload = Readonly<UploadData>
|
||||||
|
|
||||||
export const INSERT_UPLOAD_COMMAND: LexicalCommand<InsertUploadPayload> =
|
export const INSERT_UPLOAD_COMMAND: LexicalCommand<InsertUploadPayload> =
|
||||||
createCommand('INSERT_UPLOAD_COMMAND')
|
createCommand('INSERT_UPLOAD_COMMAND')
|
||||||
@@ -46,9 +46,7 @@ export function UploadPlugin(): JSX.Element | null {
|
|||||||
data: {
|
data: {
|
||||||
fields: payload.fields,
|
fields: payload.fields,
|
||||||
relationTo: payload.relationTo,
|
relationTo: payload.relationTo,
|
||||||
value: {
|
value: payload.value,
|
||||||
id: payload.id,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { sanitizeFields } from 'payload/config'
|
||||||
|
|
||||||
import type { PopulationPromise } from '../types.js'
|
import type { PopulationPromise } from '../types.js'
|
||||||
import type { UploadFeatureProps } from './feature.server.js'
|
import type { UploadFeatureProps } from './feature.server.js'
|
||||||
import type { SerializedUploadNode } from './nodes/UploadNode.js'
|
import type { SerializedUploadNode } from './nodes/UploadNode.js'
|
||||||
@@ -8,12 +10,13 @@ import { recurseNestedFields } from '../../../populate/recurseNestedFields.js'
|
|||||||
export const uploadPopulationPromiseHOC = (
|
export const uploadPopulationPromiseHOC = (
|
||||||
props?: UploadFeatureProps,
|
props?: UploadFeatureProps,
|
||||||
): PopulationPromise<SerializedUploadNode> => {
|
): PopulationPromise<SerializedUploadNode> => {
|
||||||
const uploadPopulationPromise: PopulationPromise<SerializedUploadNode> = ({
|
return ({
|
||||||
context,
|
context,
|
||||||
currentDepth,
|
currentDepth,
|
||||||
depth,
|
depth,
|
||||||
editorPopulationPromises,
|
editorPopulationPromises,
|
||||||
field,
|
field,
|
||||||
|
fieldPromises,
|
||||||
findMany,
|
findMany,
|
||||||
flattenLocales,
|
flattenLocales,
|
||||||
node,
|
node,
|
||||||
@@ -21,17 +24,19 @@ export const uploadPopulationPromiseHOC = (
|
|||||||
populationPromises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc,
|
|
||||||
}) => {
|
}) => {
|
||||||
const promises: Promise<void>[] = []
|
const payloadConfig = req.payload.config
|
||||||
|
|
||||||
if (node?.value?.id) {
|
if (node?.value) {
|
||||||
const collection = req.payload.collections[node?.relationTo]
|
const collection = req.payload.collections[node?.relationTo]
|
||||||
|
|
||||||
if (collection) {
|
if (collection) {
|
||||||
promises.push(
|
// @ts-expect-error
|
||||||
|
const id = node?.value?.id || node?.value // for backwards-compatibility
|
||||||
|
|
||||||
|
populationPromises.push(
|
||||||
populate({
|
populate({
|
||||||
id: node?.value?.id,
|
id,
|
||||||
collection,
|
collection,
|
||||||
currentDepth,
|
currentDepth,
|
||||||
data: node,
|
data: node,
|
||||||
@@ -45,27 +50,36 @@ export const uploadPopulationPromiseHOC = (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (Array.isArray(props?.collections?.[node?.relationTo]?.fields)) {
|
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({
|
recurseNestedFields({
|
||||||
context,
|
context,
|
||||||
currentDepth,
|
currentDepth,
|
||||||
data: node.fields || {},
|
data: node.fields || {},
|
||||||
depth,
|
depth,
|
||||||
editorPopulationPromises,
|
editorPopulationPromises,
|
||||||
fields: props?.collections?.[node?.relationTo]?.fields,
|
fieldPromises,
|
||||||
|
fields: sanitizedFields,
|
||||||
findMany,
|
findMany,
|
||||||
flattenLocales,
|
flattenLocales,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
populationPromises,
|
populationPromises,
|
||||||
promises,
|
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc: node.fields || {},
|
siblingDoc: node.fields || {},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return promises
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return uploadPopulationPromise
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { SerializedUploadNode } from './nodes/UploadNode.js'
|
|||||||
import { CAN_USE_DOM } from '../../lexical/utils/canUseDOM.js'
|
import { CAN_USE_DOM } from '../../lexical/utils/canUseDOM.js'
|
||||||
|
|
||||||
export const uploadValidation = (): NodeValidation<SerializedUploadNode> => {
|
export const uploadValidation = (): NodeValidation<SerializedUploadNode> => {
|
||||||
const uploadValidation: NodeValidation<SerializedUploadNode> = ({
|
return ({
|
||||||
node,
|
node,
|
||||||
validation: {
|
validation: {
|
||||||
options: {
|
options: {
|
||||||
@@ -16,8 +16,10 @@ export const uploadValidation = (): NodeValidation<SerializedUploadNode> => {
|
|||||||
}) => {
|
}) => {
|
||||||
if (!CAN_USE_DOM) {
|
if (!CAN_USE_DOM) {
|
||||||
const idType = payload.collections[node.relationTo].customIDType || payload.db.defaultIDType
|
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')
|
return t('validation:validUploadID')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,6 +28,4 @@ export const uploadValidation = (): NodeValidation<SerializedUploadNode> => {
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return uploadValidation
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,11 @@ export const sanitizeServerFeatures = (
|
|||||||
modifyOutputSchemas: [],
|
modifyOutputSchemas: [],
|
||||||
},
|
},
|
||||||
hooks: {
|
hooks: {
|
||||||
afterReadPromises: [],
|
afterChange: new Map(),
|
||||||
|
afterRead: new Map(),
|
||||||
|
beforeChange: new Map(),
|
||||||
|
beforeDuplicate: new Map(),
|
||||||
|
beforeValidate: new Map(),
|
||||||
},
|
},
|
||||||
markdownTransformers: [],
|
markdownTransformers: [],
|
||||||
nodes: [],
|
nodes: [],
|
||||||
@@ -33,13 +37,6 @@ export const sanitizeServerFeatures = (
|
|||||||
if (feature?.generatedTypes?.modifyOutputSchema) {
|
if (feature?.generatedTypes?.modifyOutputSchema) {
|
||||||
sanitized.generatedTypes.modifyOutputSchemas.push(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) {
|
if (feature.nodes?.length) {
|
||||||
sanitized.nodes = sanitized.nodes.concat(feature.nodes)
|
sanitized.nodes = sanitized.nodes.concat(feature.nodes)
|
||||||
@@ -54,6 +51,21 @@ export const sanitizeServerFeatures = (
|
|||||||
if (node?.converters?.html) {
|
if (node?.converters?.html) {
|
||||||
sanitized.converters.html.push(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)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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://.
|
// 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.
|
// Maybe show a dialog where they user can type the URL before inserting it.
|
||||||
|
|
||||||
|
if (!url) return false
|
||||||
|
|
||||||
if (url === 'https://') return true
|
if (url === 'https://') return true
|
||||||
|
|
||||||
// This makes sure URLs starting with www. instead of https are valid too
|
// This makes sure URLs starting with www. instead of https are valid too
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { sanitizeServerFeatures } from './field/lexical/config/server/sanitize.j
|
|||||||
import { cloneDeep } from './field/lexical/utils/cloneDeep.js'
|
import { cloneDeep } from './field/lexical/utils/cloneDeep.js'
|
||||||
import { getGenerateComponentMap } from './generateComponentMap.js'
|
import { getGenerateComponentMap } from './generateComponentMap.js'
|
||||||
import { getGenerateSchemaMap } from './generateSchemaMap.js'
|
import { getGenerateSchemaMap } from './generateSchemaMap.js'
|
||||||
import { richTextRelationshipPromise } from './populate/richTextRelationshipPromise.js'
|
import { populateLexicalPopulationPromises } from './populate/populateLexicalPopulationPromises.js'
|
||||||
import { richTextValidateHOC } from './validate/index.js'
|
import { richTextValidateHOC } from './validate/index.js'
|
||||||
|
|
||||||
export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapter {
|
export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapter {
|
||||||
@@ -64,29 +64,6 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
|||||||
Component: RichTextField,
|
Component: RichTextField,
|
||||||
toMergeIntoProps: { lexicalEditorConfig: finalSanitizedEditorConfig.lexical },
|
toMergeIntoProps: { lexicalEditorConfig: finalSanitizedEditorConfig.lexical },
|
||||||
}),
|
}),
|
||||||
afterReadPromise: ({ field, incomingEditorState, siblingDoc }) => {
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
const promises: Promise<void>[] = []
|
|
||||||
|
|
||||||
if (finalSanitizedEditorConfig?.features?.hooks?.afterReadPromises?.length) {
|
|
||||||
for (const afterReadPromise of finalSanitizedEditorConfig.features.hooks
|
|
||||||
.afterReadPromises) {
|
|
||||||
const promise = afterReadPromise({
|
|
||||||
field,
|
|
||||||
incomingEditorState,
|
|
||||||
siblingDoc,
|
|
||||||
})
|
|
||||||
if (promise) {
|
|
||||||
promises.push(promise)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.all(promises)
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch((error) => reject(error))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
editorConfig: finalSanitizedEditorConfig,
|
editorConfig: finalSanitizedEditorConfig,
|
||||||
generateComponentMap: getGenerateComponentMap({
|
generateComponentMap: getGenerateComponentMap({
|
||||||
resolvedFeatureMap,
|
resolvedFeatureMap,
|
||||||
@@ -94,6 +71,13 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
|||||||
generateSchemaMap: getGenerateSchemaMap({
|
generateSchemaMap: getGenerateSchemaMap({
|
||||||
resolvedFeatureMap,
|
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: ({
|
outputSchema: ({
|
||||||
collectionIDFieldTypes,
|
collectionIDFieldTypes,
|
||||||
config,
|
config,
|
||||||
@@ -171,11 +155,12 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
|||||||
|
|
||||||
return outputSchema
|
return outputSchema
|
||||||
},
|
},
|
||||||
populationPromise({
|
populationPromises({
|
||||||
context,
|
context,
|
||||||
currentDepth,
|
currentDepth,
|
||||||
depth,
|
depth,
|
||||||
field,
|
field,
|
||||||
|
fieldPromises,
|
||||||
findMany,
|
findMany,
|
||||||
flattenLocales,
|
flattenLocales,
|
||||||
overrideAccess,
|
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
|
// check if there are any features with nodes which have populationPromises for this field
|
||||||
if (finalSanitizedEditorConfig?.features?.populationPromises?.size) {
|
if (finalSanitizedEditorConfig?.features?.populationPromises?.size) {
|
||||||
return richTextRelationshipPromise({
|
populateLexicalPopulationPromises({
|
||||||
context,
|
context,
|
||||||
currentDepth,
|
currentDepth,
|
||||||
depth,
|
depth,
|
||||||
editorPopulationPromises: finalSanitizedEditorConfig.features.populationPromises,
|
editorPopulationPromises: finalSanitizedEditorConfig.features.populationPromises,
|
||||||
field,
|
field,
|
||||||
|
fieldPromises,
|
||||||
findMany,
|
findMany,
|
||||||
flattenLocales,
|
flattenLocales,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
@@ -201,8 +187,6 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
|||||||
siblingDoc,
|
siblingDoc,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
|
||||||
},
|
},
|
||||||
validate: richTextValidateHOC({
|
validate: richTextValidateHOC({
|
||||||
editorConfig: finalSanitizedEditorConfig,
|
editorConfig: finalSanitizedEditorConfig,
|
||||||
@@ -311,14 +295,19 @@ export {
|
|||||||
RelationshipNode,
|
RelationshipNode,
|
||||||
type SerializedRelationshipNode,
|
type SerializedRelationshipNode,
|
||||||
} from './field/features/relationship/nodes/RelationshipNode.js'
|
} from './field/features/relationship/nodes/RelationshipNode.js'
|
||||||
|
export { createNode } from './field/features/typeUtilities.js'
|
||||||
export type {
|
export type {
|
||||||
|
ClientComponentProps,
|
||||||
ClientFeature,
|
ClientFeature,
|
||||||
ClientFeatureProviderMap,
|
ClientFeatureProviderMap,
|
||||||
FeatureProviderClient,
|
FeatureProviderClient,
|
||||||
FeatureProviderProviderClient,
|
FeatureProviderProviderClient,
|
||||||
FeatureProviderProviderServer,
|
FeatureProviderProviderServer,
|
||||||
FeatureProviderServer,
|
FeatureProviderServer,
|
||||||
|
FieldNodeHook,
|
||||||
|
FieldNodeHookArgs,
|
||||||
NodeValidation,
|
NodeValidation,
|
||||||
|
NodeWithHooks,
|
||||||
PopulationPromise,
|
PopulationPromise,
|
||||||
ResolvedClientFeature,
|
ResolvedClientFeature,
|
||||||
ResolvedClientFeatureMap,
|
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 { UploadFeatureProps } from './field/features/upload/feature.server.js'
|
||||||
|
|
||||||
export type { RawUploadPayload } from './field/features/upload/nodes/UploadNode.js'
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
$createUploadNode,
|
$createUploadNode,
|
||||||
$isUploadNode,
|
$isUploadNode,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const populate = async ({
|
|||||||
}: Omit<Arguments, 'field'> & {
|
}: Omit<Arguments, 'field'> & {
|
||||||
collection: Collection
|
collection: Collection
|
||||||
field: Field
|
field: Field
|
||||||
id: string
|
id: number | string
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
const dataRef = data as Record<string, unknown>
|
const dataRef = data as Record<string, unknown>
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,17 @@
|
|||||||
import type { SerializedEditorState, SerializedLexicalNode } from 'lexical'
|
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 { PopulationPromise } from '../field/features/types.js'
|
||||||
import type { AdapterProps } from '../types.js'
|
import type { AdapterProps } from '../types.js'
|
||||||
|
|
||||||
export type Args = Parameters<
|
export type Args = Parameters<
|
||||||
RichTextAdapter<SerializedEditorState, AdapterProps>['populationPromise']
|
RichTextAdapter<SerializedEditorState, AdapterProps>['populationPromises']
|
||||||
>[0] & {
|
>[0] & {
|
||||||
editorPopulationPromises: Map<string, Array<PopulationPromise>>
|
editorPopulationPromises: Map<string, Array<PopulationPromise>>
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecurseRichTextArgs = {
|
type RecurseRichTextArgs = {
|
||||||
children: SerializedLexicalNode[]
|
children: SerializedLexicalNode[]
|
||||||
currentDepth: number
|
|
||||||
depth: number
|
|
||||||
editorPopulationPromises: Map<string, Array<PopulationPromise>>
|
|
||||||
field: RichTextField<SerializedEditorState, AdapterProps>
|
|
||||||
overrideAccess: boolean
|
|
||||||
promises: Promise<void>[]
|
|
||||||
req: PayloadRequest
|
|
||||||
showHiddenFields: boolean
|
|
||||||
siblingDoc?: Record<string, unknown>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const recurseRichText = ({
|
export const recurseRichText = ({
|
||||||
@@ -30,15 +21,15 @@ export const recurseRichText = ({
|
|||||||
depth,
|
depth,
|
||||||
editorPopulationPromises,
|
editorPopulationPromises,
|
||||||
field,
|
field,
|
||||||
|
fieldPromises,
|
||||||
findMany,
|
findMany,
|
||||||
flattenLocales,
|
flattenLocales,
|
||||||
overrideAccess = false,
|
overrideAccess = false,
|
||||||
populationPromises,
|
populationPromises,
|
||||||
promises,
|
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
}: RecurseRichTextArgs & Args): void => {
|
}: Args & RecurseRichTextArgs): void => {
|
||||||
if (depth <= 0 || currentDepth > depth) {
|
if (depth <= 0 || currentDepth > depth) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -47,23 +38,22 @@ export const recurseRichText = ({
|
|||||||
children.forEach((node) => {
|
children.forEach((node) => {
|
||||||
if (editorPopulationPromises?.has(node.type)) {
|
if (editorPopulationPromises?.has(node.type)) {
|
||||||
for (const promise of editorPopulationPromises.get(node.type)) {
|
for (const promise of editorPopulationPromises.get(node.type)) {
|
||||||
promises.push(
|
promise({
|
||||||
...promise({
|
context,
|
||||||
context,
|
currentDepth,
|
||||||
currentDepth,
|
depth,
|
||||||
depth,
|
editorPopulationPromises,
|
||||||
editorPopulationPromises,
|
field,
|
||||||
field,
|
fieldPromises,
|
||||||
findMany,
|
findMany,
|
||||||
flattenLocales,
|
flattenLocales,
|
||||||
node,
|
node,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
populationPromises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
}),
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,11 +65,11 @@ export const recurseRichText = ({
|
|||||||
depth,
|
depth,
|
||||||
editorPopulationPromises,
|
editorPopulationPromises,
|
||||||
field,
|
field,
|
||||||
|
fieldPromises,
|
||||||
findMany,
|
findMany,
|
||||||
flattenLocales,
|
flattenLocales,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
populationPromises,
|
populationPromises,
|
||||||
promises,
|
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
@@ -89,12 +79,16 @@ export const recurseRichText = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const richTextRelationshipPromise = async ({
|
/**
|
||||||
|
* Appends all new populationPromises to the populationPromises prop
|
||||||
|
*/
|
||||||
|
export const populateLexicalPopulationPromises = ({
|
||||||
context,
|
context,
|
||||||
currentDepth,
|
currentDepth,
|
||||||
depth,
|
depth,
|
||||||
editorPopulationPromises,
|
editorPopulationPromises,
|
||||||
field,
|
field,
|
||||||
|
fieldPromises,
|
||||||
findMany,
|
findMany,
|
||||||
flattenLocales,
|
flattenLocales,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
@@ -102,9 +96,7 @@ export const richTextRelationshipPromise = async ({
|
|||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
}: Args): Promise<void> => {
|
}: Args) => {
|
||||||
const promises = []
|
|
||||||
|
|
||||||
recurseRichText({
|
recurseRichText({
|
||||||
children: (siblingDoc[field?.name] as SerializedEditorState)?.root?.children ?? [],
|
children: (siblingDoc[field?.name] as SerializedEditorState)?.root?.children ?? [],
|
||||||
context,
|
context,
|
||||||
@@ -112,15 +104,13 @@ export const richTextRelationshipPromise = async ({
|
|||||||
depth,
|
depth,
|
||||||
editorPopulationPromises,
|
editorPopulationPromises,
|
||||||
field,
|
field,
|
||||||
|
fieldPromises,
|
||||||
findMany,
|
findMany,
|
||||||
flattenLocales,
|
flattenLocales,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
populationPromises,
|
populationPromises,
|
||||||
promises,
|
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
})
|
})
|
||||||
|
|
||||||
await Promise.all(promises)
|
|
||||||
}
|
}
|
||||||
@@ -14,12 +14,15 @@ type NestedRichTextFieldsArgs = {
|
|||||||
* This maps all the population promises to the node types
|
* This maps all the population promises to the node types
|
||||||
*/
|
*/
|
||||||
editorPopulationPromises: Map<string, Array<PopulationPromise>>
|
editorPopulationPromises: Map<string, Array<PopulationPromise>>
|
||||||
|
/**
|
||||||
|
* fieldPromises are used for things like field hooks. They should be awaited before awaiting populationPromises
|
||||||
|
*/
|
||||||
|
fieldPromises: Promise<void>[]
|
||||||
fields: Field[]
|
fields: Field[]
|
||||||
findMany: boolean
|
findMany: boolean
|
||||||
flattenLocales: boolean
|
flattenLocales: boolean
|
||||||
overrideAccess: boolean
|
overrideAccess: boolean
|
||||||
populationPromises: Promise<void>[]
|
populationPromises: Promise<void>[]
|
||||||
promises: Promise<void>[]
|
|
||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
showHiddenFields: boolean
|
showHiddenFields: boolean
|
||||||
siblingDoc: Record<string, unknown>
|
siblingDoc: Record<string, unknown>
|
||||||
@@ -30,12 +33,12 @@ export const recurseNestedFields = ({
|
|||||||
currentDepth = 0,
|
currentDepth = 0,
|
||||||
data,
|
data,
|
||||||
depth,
|
depth,
|
||||||
|
fieldPromises,
|
||||||
fields,
|
fields,
|
||||||
findMany,
|
findMany,
|
||||||
flattenLocales,
|
flattenLocales,
|
||||||
overrideAccess = false,
|
overrideAccess = false,
|
||||||
populationPromises,
|
populationPromises,
|
||||||
promises,
|
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
@@ -47,7 +50,7 @@ export const recurseNestedFields = ({
|
|||||||
depth,
|
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
|
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,
|
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,
|
fields,
|
||||||
findMany,
|
findMany,
|
||||||
flattenLocales,
|
flattenLocales,
|
||||||
@@ -58,7 +61,7 @@ export const recurseNestedFields = ({
|
|||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
triggerAccessControl: false, // TODO: Enable this to support access control
|
//triggerAccessControl: false, // TODO: Enable this to support access control
|
||||||
triggerHooks: false, // TODO: Enable this to support hooks
|
//triggerHooks: false, // TODO: Enable this to support hooks
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ type NestedRichTextFieldsArgs = {
|
|||||||
depth: number
|
depth: number
|
||||||
fields: Field[]
|
fields: Field[]
|
||||||
overrideAccess: boolean
|
overrideAccess: boolean
|
||||||
promises: Promise<void>[]
|
populationPromises: Promise<void>[]
|
||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
showHiddenFields: boolean
|
showHiddenFields: boolean
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@ export const recurseNestedFields = ({
|
|||||||
depth,
|
depth,
|
||||||
fields,
|
fields,
|
||||||
overrideAccess = false,
|
overrideAccess = false,
|
||||||
promises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
}: NestedRichTextFieldsArgs): void => {
|
}: NestedRichTextFieldsArgs): void => {
|
||||||
@@ -34,7 +34,7 @@ export const recurseNestedFields = ({
|
|||||||
data[field.name].forEach(({ relationTo, value }, i) => {
|
data[field.name].forEach(({ relationTo, value }, i) => {
|
||||||
const collection = req.payload.collections[relationTo]
|
const collection = req.payload.collections[relationTo]
|
||||||
if (collection) {
|
if (collection) {
|
||||||
promises.push(
|
populationPromises.push(
|
||||||
populate({
|
populate({
|
||||||
id: value,
|
id: value,
|
||||||
collection,
|
collection,
|
||||||
@@ -54,7 +54,7 @@ export const recurseNestedFields = ({
|
|||||||
data[field.name].forEach((id, i) => {
|
data[field.name].forEach((id, i) => {
|
||||||
const collection = req.payload.collections[field.relationTo as string]
|
const collection = req.payload.collections[field.relationTo as string]
|
||||||
if (collection) {
|
if (collection) {
|
||||||
promises.push(
|
populationPromises.push(
|
||||||
populate({
|
populate({
|
||||||
id,
|
id,
|
||||||
collection,
|
collection,
|
||||||
@@ -78,7 +78,7 @@ export const recurseNestedFields = ({
|
|||||||
) {
|
) {
|
||||||
if (!('hasMany' in field) || !field.hasMany) {
|
if (!('hasMany' in field) || !field.hasMany) {
|
||||||
const collection = req.payload.collections[data[field.name].relationTo]
|
const collection = req.payload.collections[data[field.name].relationTo]
|
||||||
promises.push(
|
populationPromises.push(
|
||||||
populate({
|
populate({
|
||||||
id: data[field.name].value,
|
id: data[field.name].value,
|
||||||
collection,
|
collection,
|
||||||
@@ -97,7 +97,7 @@ export const recurseNestedFields = ({
|
|||||||
}
|
}
|
||||||
if (typeof data[field.name] !== 'undefined' && typeof field.relationTo === 'string') {
|
if (typeof data[field.name] !== 'undefined' && typeof field.relationTo === 'string') {
|
||||||
const collection = req.payload.collections[field.relationTo]
|
const collection = req.payload.collections[field.relationTo]
|
||||||
promises.push(
|
populationPromises.push(
|
||||||
populate({
|
populate({
|
||||||
id: data[field.name],
|
id: data[field.name],
|
||||||
collection,
|
collection,
|
||||||
@@ -120,7 +120,7 @@ export const recurseNestedFields = ({
|
|||||||
depth,
|
depth,
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
promises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
})
|
})
|
||||||
@@ -131,7 +131,7 @@ export const recurseNestedFields = ({
|
|||||||
depth,
|
depth,
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
promises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
})
|
})
|
||||||
@@ -144,7 +144,7 @@ export const recurseNestedFields = ({
|
|||||||
depth,
|
depth,
|
||||||
fields: tab.fields,
|
fields: tab.fields,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
promises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
})
|
})
|
||||||
@@ -160,7 +160,7 @@ export const recurseNestedFields = ({
|
|||||||
depth,
|
depth,
|
||||||
fields: block.fields,
|
fields: block.fields,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
promises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
})
|
})
|
||||||
@@ -176,7 +176,7 @@ export const recurseNestedFields = ({
|
|||||||
depth,
|
depth,
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
promises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
})
|
})
|
||||||
@@ -193,7 +193,7 @@ export const recurseNestedFields = ({
|
|||||||
depth,
|
depth,
|
||||||
field,
|
field,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
promises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { AdapterArguments } from '../types.js'
|
|||||||
import { populate } from './populate.js'
|
import { populate } from './populate.js'
|
||||||
import { recurseNestedFields } from './recurseNestedFields.js'
|
import { recurseNestedFields } from './recurseNestedFields.js'
|
||||||
|
|
||||||
export type Args = Parameters<RichTextAdapter<any[], AdapterArguments>['populationPromise']>[0]
|
export type Args = Parameters<RichTextAdapter<any[], AdapterArguments>['populationPromises']>[0]
|
||||||
|
|
||||||
type RecurseRichTextArgs = {
|
type RecurseRichTextArgs = {
|
||||||
children: unknown[]
|
children: unknown[]
|
||||||
@@ -13,7 +13,7 @@ type RecurseRichTextArgs = {
|
|||||||
depth: number
|
depth: number
|
||||||
field: RichTextField<any[], AdapterArguments, AdapterArguments>
|
field: RichTextField<any[], AdapterArguments, AdapterArguments>
|
||||||
overrideAccess: boolean
|
overrideAccess: boolean
|
||||||
promises: Promise<void>[]
|
populationPromises: Promise<void>[]
|
||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
showHiddenFields: boolean
|
showHiddenFields: boolean
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export const recurseRichText = ({
|
|||||||
depth,
|
depth,
|
||||||
field,
|
field,
|
||||||
overrideAccess = false,
|
overrideAccess = false,
|
||||||
promises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
}: RecurseRichTextArgs): void => {
|
}: RecurseRichTextArgs): void => {
|
||||||
@@ -38,7 +38,7 @@ export const recurseRichText = ({
|
|||||||
const collection = req.payload.collections[element?.relationTo]
|
const collection = req.payload.collections[element?.relationTo]
|
||||||
|
|
||||||
if (collection) {
|
if (collection) {
|
||||||
promises.push(
|
populationPromises.push(
|
||||||
populate({
|
populate({
|
||||||
id: element.value.id,
|
id: element.value.id,
|
||||||
collection,
|
collection,
|
||||||
@@ -63,7 +63,7 @@ export const recurseRichText = ({
|
|||||||
depth,
|
depth,
|
||||||
fields: field.admin.upload.collections[element.relationTo].fields,
|
fields: field.admin.upload.collections[element.relationTo].fields,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
promises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
})
|
})
|
||||||
@@ -75,7 +75,7 @@ export const recurseRichText = ({
|
|||||||
const collection = req.payload.collections[element?.doc?.relationTo]
|
const collection = req.payload.collections[element?.doc?.relationTo]
|
||||||
|
|
||||||
if (collection) {
|
if (collection) {
|
||||||
promises.push(
|
populationPromises.push(
|
||||||
populate({
|
populate({
|
||||||
id: element.doc.value,
|
id: element.doc.value,
|
||||||
collection,
|
collection,
|
||||||
@@ -99,7 +99,7 @@ export const recurseRichText = ({
|
|||||||
depth,
|
depth,
|
||||||
fields: field.admin?.link?.fields,
|
fields: field.admin?.link?.fields,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
promises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
})
|
})
|
||||||
@@ -113,7 +113,7 @@ export const recurseRichText = ({
|
|||||||
depth,
|
depth,
|
||||||
field,
|
field,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
promises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
})
|
})
|
||||||
@@ -122,27 +122,24 @@ export const recurseRichText = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const richTextRelationshipPromise = async ({
|
export const richTextRelationshipPromise = ({
|
||||||
currentDepth,
|
currentDepth,
|
||||||
depth,
|
depth,
|
||||||
field,
|
field,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
}: Args): Promise<void> => {
|
}: Args) => {
|
||||||
const promises = []
|
|
||||||
|
|
||||||
recurseRichText({
|
recurseRichText({
|
||||||
children: siblingDoc[field.name] as unknown[],
|
children: siblingDoc[field.name] as unknown[],
|
||||||
currentDepth,
|
currentDepth,
|
||||||
depth,
|
depth,
|
||||||
field,
|
field,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
promises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
})
|
})
|
||||||
|
|
||||||
await Promise.all(promises)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,11 +25,12 @@ export function slateEditor(args: AdapterArguments): RichTextAdapter<any[], Adap
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
populationPromise({
|
populationPromises({
|
||||||
context,
|
context,
|
||||||
currentDepth,
|
currentDepth,
|
||||||
depth,
|
depth,
|
||||||
field,
|
field,
|
||||||
|
fieldPromises,
|
||||||
findMany,
|
findMany,
|
||||||
flattenLocales,
|
flattenLocales,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
@@ -44,11 +45,12 @@ export function slateEditor(args: AdapterArguments): RichTextAdapter<any[], Adap
|
|||||||
field.admin?.elements?.includes('link') ||
|
field.admin?.elements?.includes('link') ||
|
||||||
!field?.admin?.elements
|
!field?.admin?.elements
|
||||||
) {
|
) {
|
||||||
return richTextRelationshipPromise({
|
richTextRelationshipPromise({
|
||||||
context,
|
context,
|
||||||
currentDepth,
|
currentDepth,
|
||||||
depth,
|
depth,
|
||||||
field,
|
field,
|
||||||
|
fieldPromises,
|
||||||
findMany,
|
findMany,
|
||||||
flattenLocales,
|
flattenLocales,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
@@ -58,7 +60,6 @@ export function slateEditor(args: AdapterArguments): RichTextAdapter<any[], Adap
|
|||||||
siblingDoc,
|
siblingDoc,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
},
|
},
|
||||||
validate: richTextValidate,
|
validate: richTextValidate,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type Args<T> = {
|
|||||||
siblingData: Data
|
siblingData: Data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Make this works for rich text subfields
|
||||||
export const defaultValuePromise = async <T>({
|
export const defaultValuePromise = async <T>({
|
||||||
id,
|
id,
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -33,9 +33,15 @@ let serverURL: string
|
|||||||
/**
|
/**
|
||||||
* Client-side navigation to the lexical editor from list view
|
* 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) {
|
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)
|
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()
|
await navigateToLexicalFields()
|
||||||
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
|
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
|
||||||
await richTextField.scrollIntoViewIfNeeded()
|
await richTextField.scrollIntoViewIfNeeded()
|
||||||
@@ -1117,12 +1123,18 @@ describe('lexical', () => {
|
|||||||
await page.click('#action-save', { delay: 100 })
|
await page.click('#action-save', { delay: 100 })
|
||||||
await expect(page.locator('.Toastify')).toContainText('The following field is invalid')
|
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
|
// Check if error is shown next to field
|
||||||
await expect(
|
await expect(requiredTooltip).toBeInViewport() // toBeVisible() doesn't work for some reason
|
||||||
conditionalArrayBlock
|
})
|
||||||
.locator('.tooltip-content:has-text("This field is required.")')
|
})
|
||||||
.first(),
|
|
||||||
).toBeVisible()
|
describe('localization', () => {
|
||||||
|
test.skip('ensure simple localized lexical field works', async () => {
|
||||||
|
await navigateToLexicalFields(true, true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function generateLexicalRichText() {
|
|||||||
{
|
{
|
||||||
format: '',
|
format: '',
|
||||||
type: 'upload',
|
type: 'upload',
|
||||||
version: 1,
|
version: 2,
|
||||||
fields: {
|
fields: {
|
||||||
caption: {
|
caption: {
|
||||||
root: {
|
root: {
|
||||||
@@ -57,11 +57,9 @@ export function generateLexicalRichText() {
|
|||||||
{
|
{
|
||||||
format: '',
|
format: '',
|
||||||
type: 'relationship',
|
type: 'relationship',
|
||||||
version: 1,
|
version: 2,
|
||||||
relationTo: 'text-fields',
|
relationTo: 'text-fields',
|
||||||
value: {
|
value: '{{TEXT_DOC_ID}}',
|
||||||
id: '{{TEXT_DOC_ID}}',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
direction: 'ltr',
|
direction: 'ltr',
|
||||||
@@ -69,9 +67,7 @@ export function generateLexicalRichText() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationTo: 'uploads',
|
relationTo: 'uploads',
|
||||||
value: {
|
value: '{{UPLOAD_DOC_ID}}',
|
||||||
id: '{{UPLOAD_DOC_ID}}',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
format: '',
|
format: '',
|
||||||
@@ -120,11 +116,9 @@ export function generateLexicalRichText() {
|
|||||||
{
|
{
|
||||||
format: '',
|
format: '',
|
||||||
type: 'relationship',
|
type: 'relationship',
|
||||||
version: 1,
|
version: 2,
|
||||||
relationTo: 'rich-text-fields',
|
relationTo: 'rich-text-fields',
|
||||||
value: {
|
value: '{{RICH_TEXT_DOC_ID}}',
|
||||||
id: '{{RICH_TEXT_DOC_ID}}',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
children: [
|
children: [
|
||||||
@@ -173,11 +167,9 @@ export function generateLexicalRichText() {
|
|||||||
{
|
{
|
||||||
format: '',
|
format: '',
|
||||||
type: 'relationship',
|
type: 'relationship',
|
||||||
version: 1,
|
version: 2,
|
||||||
relationTo: 'text-fields',
|
relationTo: 'text-fields',
|
||||||
value: {
|
value: '{{TEXT_DOC_ID}}',
|
||||||
id: '{{TEXT_DOC_ID}}',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
95
test/fields/collections/LexicalLocalized/index.ts
Normal file
95
test/fields/collections/LexicalLocalized/index.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
import { BlocksFeature, lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||||
|
|
||||||
|
import { lexicalLocalizedFieldsSlug } from '../../slugs.js'
|
||||||
|
|
||||||
|
export const LexicalLocalizedFields: CollectionConfig = {
|
||||||
|
slug: lexicalLocalizedFieldsSlug,
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'title',
|
||||||
|
listSearchableFields: ['title'],
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
read: () => true,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lexicalSimple',
|
||||||
|
type: 'richText',
|
||||||
|
localized: true,
|
||||||
|
editor: lexicalEditor({
|
||||||
|
features: ({ defaultFeatures }) => [...defaultFeatures],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lexicalBlocksLocalized',
|
||||||
|
admin: {
|
||||||
|
description: 'Localized field with localized block subfields',
|
||||||
|
},
|
||||||
|
type: 'richText',
|
||||||
|
localized: true,
|
||||||
|
editor: lexicalEditor({
|
||||||
|
features: ({ defaultFeatures }) => [
|
||||||
|
...defaultFeatures,
|
||||||
|
BlocksFeature({
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
slug: 'block',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'textLocalized',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rel',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: lexicalLocalizedFieldsSlug,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lexicalBlocksSubLocalized',
|
||||||
|
type: 'richText',
|
||||||
|
admin: {
|
||||||
|
description: 'Non-localized field with localized block subfields',
|
||||||
|
},
|
||||||
|
editor: lexicalEditor({
|
||||||
|
features: ({ defaultFeatures }) => [
|
||||||
|
...defaultFeatures,
|
||||||
|
BlocksFeature({
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
slug: 'block',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'textLocalized',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rel',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: lexicalLocalizedFieldsSlug,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -116,10 +116,8 @@ export function generateLexicalRichText() {
|
|||||||
{
|
{
|
||||||
format: '',
|
format: '',
|
||||||
type: 'relationship',
|
type: 'relationship',
|
||||||
version: 1,
|
version: 2,
|
||||||
value: {
|
value: '{{TEXT_DOC_ID}}',
|
||||||
id: '{{TEXT_DOC_ID}}',
|
|
||||||
},
|
|
||||||
relationTo: 'text-fields',
|
relationTo: 'text-fields',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -230,11 +228,9 @@ export function generateLexicalRichText() {
|
|||||||
{
|
{
|
||||||
format: '',
|
format: '',
|
||||||
type: 'upload',
|
type: 'upload',
|
||||||
version: 1,
|
version: 2,
|
||||||
relationTo: 'uploads',
|
relationTo: 'uploads',
|
||||||
value: {
|
value: '{{UPLOAD_DOC_ID}}',
|
||||||
id: '{{UPLOAD_DOC_ID}}',
|
|
||||||
},
|
|
||||||
fields: {
|
fields: {
|
||||||
caption: {
|
caption: {
|
||||||
root: {
|
root: {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import GroupFields from './collections/Group/index.js'
|
|||||||
import IndexedFields from './collections/Indexed/index.js'
|
import IndexedFields from './collections/Indexed/index.js'
|
||||||
import JSONFields from './collections/JSON/index.js'
|
import JSONFields from './collections/JSON/index.js'
|
||||||
import { LexicalFields } from './collections/Lexical/index.js'
|
import { LexicalFields } from './collections/Lexical/index.js'
|
||||||
|
import { LexicalLocalizedFields } from './collections/LexicalLocalized/index.js'
|
||||||
import { LexicalMigrateFields } from './collections/LexicalMigrate/index.js'
|
import { LexicalMigrateFields } from './collections/LexicalMigrate/index.js'
|
||||||
import NumberFields from './collections/Number/index.js'
|
import NumberFields from './collections/Number/index.js'
|
||||||
import PointFields from './collections/Point/index.js'
|
import PointFields from './collections/Point/index.js'
|
||||||
@@ -31,6 +32,7 @@ import { clearAndSeedEverything } from './seed.js'
|
|||||||
export const collectionSlugs: CollectionConfig[] = [
|
export const collectionSlugs: CollectionConfig[] = [
|
||||||
LexicalFields,
|
LexicalFields,
|
||||||
LexicalMigrateFields,
|
LexicalMigrateFields,
|
||||||
|
LexicalLocalizedFields,
|
||||||
{
|
{
|
||||||
slug: 'users',
|
slug: 'users',
|
||||||
admin: {
|
admin: {
|
||||||
|
|||||||
@@ -416,9 +416,10 @@ describe('Lexical', () => {
|
|||||||
/**
|
/**
|
||||||
* Depth 1 population:
|
* 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:
|
// 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 () => {
|
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:
|
* 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
|
// 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 () => {
|
it('should populate relationship nodes inside of a sub-editor from a blocks node with depth 2', async () => {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { dateDoc } from './collections/Date/shared.js'
|
|||||||
import { groupDoc } from './collections/Group/shared.js'
|
import { groupDoc } from './collections/Group/shared.js'
|
||||||
import { jsonDoc } from './collections/JSON/shared.js'
|
import { jsonDoc } from './collections/JSON/shared.js'
|
||||||
import { lexicalDocData } from './collections/Lexical/data.js'
|
import { lexicalDocData } from './collections/Lexical/data.js'
|
||||||
|
import { textToLexicalJSON } from './collections/LexicalLocalized/textToLexicalJSON.js'
|
||||||
import { lexicalMigrateDocData } from './collections/LexicalMigrate/data.js'
|
import { lexicalMigrateDocData } from './collections/LexicalMigrate/data.js'
|
||||||
import { numberDoc } from './collections/Number/shared.js'
|
import { numberDoc } from './collections/Number/shared.js'
|
||||||
import { pointDoc } from './collections/Point/shared.js'
|
import { pointDoc } from './collections/Point/shared.js'
|
||||||
@@ -35,6 +36,7 @@ import {
|
|||||||
groupFieldsSlug,
|
groupFieldsSlug,
|
||||||
jsonFieldsSlug,
|
jsonFieldsSlug,
|
||||||
lexicalFieldsSlug,
|
lexicalFieldsSlug,
|
||||||
|
lexicalLocalizedFieldsSlug,
|
||||||
lexicalMigrateFieldsSlug,
|
lexicalMigrateFieldsSlug,
|
||||||
numberFieldsSlug,
|
numberFieldsSlug,
|
||||||
pointFieldsSlug,
|
pointFieldsSlug,
|
||||||
@@ -49,7 +51,7 @@ import {
|
|||||||
const filename = fileURLToPath(import.meta.url)
|
const filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
export const seed = async (_payload) => {
|
export const seed = async (_payload: Payload) => {
|
||||||
if (_payload.db.name === 'mongoose') {
|
if (_payload.db.name === 'mongoose') {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
_payload.config.collections.map(async (coll) => {
|
_payload.config.collections.map(async (coll) => {
|
||||||
@@ -274,6 +276,74 @@ export const seed = async (_payload) => {
|
|||||||
overrideAccess: true,
|
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({
|
await _payload.create({
|
||||||
collection: lexicalMigrateFieldsSlug,
|
collection: lexicalMigrateFieldsSlug,
|
||||||
data: lexicalMigrateDocWithRelId,
|
data: lexicalMigrateDocWithRelId,
|
||||||
|
|||||||
@@ -1,28 +1,29 @@
|
|||||||
export const usersSlug = 'users' as const
|
export const usersSlug = 'users'
|
||||||
export const arrayFieldsSlug = 'array-fields' as const
|
export const arrayFieldsSlug = 'array-fields'
|
||||||
export const blockFieldsSlug = 'block-fields' as const
|
export const blockFieldsSlug = 'block-fields'
|
||||||
export const checkboxFieldsSlug = 'checkbox-fields' as const
|
export const checkboxFieldsSlug = 'checkbox-fields'
|
||||||
export const codeFieldsSlug = 'code-fields' as const
|
export const codeFieldsSlug = 'code-fields'
|
||||||
export const collapsibleFieldsSlug = 'collapsible-fields' as const
|
export const collapsibleFieldsSlug = 'collapsible-fields'
|
||||||
export const conditionalLogicSlug = 'conditional-logic' as const
|
export const conditionalLogicSlug = 'conditional-logic'
|
||||||
export const dateFieldsSlug = 'date-fields' as const
|
export const dateFieldsSlug = 'date-fields'
|
||||||
export const groupFieldsSlug = 'group-fields' as const
|
export const groupFieldsSlug = 'group-fields'
|
||||||
export const indexedFieldsSlug = 'indexed-fields' as const
|
export const indexedFieldsSlug = 'indexed-fields'
|
||||||
export const jsonFieldsSlug = 'json-fields' as const
|
export const jsonFieldsSlug = 'json-fields'
|
||||||
export const lexicalFieldsSlug = 'lexical-fields' as const
|
export const lexicalFieldsSlug = 'lexical-fields'
|
||||||
export const lexicalMigrateFieldsSlug = 'lexical-migrate-fields' as const
|
export const lexicalLocalizedFieldsSlug = 'lexical-localized-fields'
|
||||||
export const numberFieldsSlug = 'number-fields' as const
|
export const lexicalMigrateFieldsSlug = 'lexical-migrate-fields'
|
||||||
export const pointFieldsSlug = 'point-fields' as const
|
export const numberFieldsSlug = 'number-fields'
|
||||||
export const radioFieldsSlug = 'radio-fields' as const
|
export const pointFieldsSlug = 'point-fields'
|
||||||
export const relationshipFieldsSlug = 'relationship-fields' as const
|
export const radioFieldsSlug = 'radio-fields'
|
||||||
export const richTextFieldsSlug = 'rich-text-fields' as const
|
export const relationshipFieldsSlug = 'relationship-fields'
|
||||||
export const rowFieldsSlug = 'row-fields' as const
|
export const richTextFieldsSlug = 'rich-text-fields'
|
||||||
export const selectFieldsSlug = 'select-fields' as const
|
export const rowFieldsSlug = 'row-fields'
|
||||||
export const tabsFieldsSlug = 'tabs-fields' as const
|
export const selectFieldsSlug = 'select-fields'
|
||||||
export const textFieldsSlug = 'text-fields' as const
|
export const tabsFieldsSlug = 'tabs-fields'
|
||||||
export const uploadsSlug = 'uploads' as const
|
export const textFieldsSlug = 'text-fields'
|
||||||
export const uploads2Slug = 'uploads2' as const
|
export const uploadsSlug = 'uploads'
|
||||||
export const uploads3Slug = 'uploads3' as const
|
export const uploads2Slug = 'uploads2'
|
||||||
|
export const uploads3Slug = 'uploads3'
|
||||||
export const collectionSlugs = [
|
export const collectionSlugs = [
|
||||||
usersSlug,
|
usersSlug,
|
||||||
arrayFieldsSlug,
|
arrayFieldsSlug,
|
||||||
|
|||||||
Reference in New Issue
Block a user