feat(richtext-lexical)!: sub-field hooks and localization support (#6591)
## BREAKING - Our internal field hook methods now have new required `schemaPath` and path `props`. This affects the following functions, if you are using those: `afterChangeTraverseFields`, `afterReadTraverseFields`, `beforeChangeTraverseFields`, `beforeValidateTraverseFields`, `afterReadPromise` - The afterChange field hook's `value` is now the value AFTER the previous hooks were run. Previously, this was the original value, which I believe is a bug - Only relevant if you have built your own richText adapter: the richText adapter `populationPromises` property has been renamed to `graphQLPopulationPromises` and is now only run for graphQL. Previously, it was run for graphQL AND the rest API. To migrate, use `hooks.afterRead` to run population for the rest API - Only relevant if you have built your own lexical features: The `populationPromises` server feature property has been renamed to `graphQLPopulationPromises` and is now only run for graphQL. Previously, it was run for graphQL AND the rest API. To migrate, use `hooks.afterRead` to run population for the rest API - Serialized lexical link and upload nodes now have a new `id` property. While not breaking, localization / hooks will not work for their fields until you have migrated to that. Re-saving the old document on the new version will automatically add the `id` property for you. You will also get a bunch of console logs for every lexical node which is not migrated
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
export { generateSchema } from '../bin/generateSchema.js'
|
export { generateSchema } from '../bin/generateSchema.js'
|
||||||
|
export { buildObjectType } from '../schema/buildObjectType.js'
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ type Args = {
|
|||||||
parentName: string
|
parentName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildObjectType({
|
export function buildObjectType({
|
||||||
name,
|
name,
|
||||||
baseFields = {},
|
baseFields = {},
|
||||||
config,
|
config,
|
||||||
@@ -492,13 +492,13 @@ 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?.populationPromises) {
|
if (editor?.graphQLPopulationPromises) {
|
||||||
const fieldPromises = []
|
const fieldPromises = []
|
||||||
const populationPromises = []
|
const populationPromises = []
|
||||||
const populateDepth =
|
const populateDepth =
|
||||||
field?.maxDepth !== undefined && field?.maxDepth < depth ? field?.maxDepth : depth
|
field?.maxDepth !== undefined && field?.maxDepth < depth ? field?.maxDepth : depth
|
||||||
|
|
||||||
editor?.populationPromises({
|
editor?.graphQLPopulationPromises({
|
||||||
context,
|
context,
|
||||||
depth: populateDepth,
|
depth: populateDepth,
|
||||||
draft: args.draft,
|
draft: args.draft,
|
||||||
@@ -698,5 +698,3 @@ function buildObjectType({
|
|||||||
|
|
||||||
return newlyCreatedBlockType
|
return newlyCreatedBlockType
|
||||||
}
|
}
|
||||||
|
|
||||||
export default buildObjectType
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import restoreVersionResolver from '../resolvers/collections/restoreVersion.js'
|
|||||||
import { updateResolver } from '../resolvers/collections/update.js'
|
import { updateResolver } from '../resolvers/collections/update.js'
|
||||||
import formatName from '../utilities/formatName.js'
|
import formatName from '../utilities/formatName.js'
|
||||||
import { buildMutationInputType, getCollectionIDType } from './buildMutationInputType.js'
|
import { buildMutationInputType, getCollectionIDType } from './buildMutationInputType.js'
|
||||||
import buildObjectType from './buildObjectType.js'
|
import { buildObjectType } from './buildObjectType.js'
|
||||||
import buildPaginatedListType from './buildPaginatedListType.js'
|
import buildPaginatedListType from './buildPaginatedListType.js'
|
||||||
import { buildPolicyType } from './buildPoliciesType.js'
|
import { buildPolicyType } from './buildPoliciesType.js'
|
||||||
import buildWhereInputType from './buildWhereInputType.js'
|
import buildWhereInputType from './buildWhereInputType.js'
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import restoreVersionResolver from '../resolvers/globals/restoreVersion.js'
|
|||||||
import updateResolver from '../resolvers/globals/update.js'
|
import updateResolver from '../resolvers/globals/update.js'
|
||||||
import formatName from '../utilities/formatName.js'
|
import formatName from '../utilities/formatName.js'
|
||||||
import { buildMutationInputType } from './buildMutationInputType.js'
|
import { buildMutationInputType } from './buildMutationInputType.js'
|
||||||
import buildObjectType from './buildObjectType.js'
|
import { buildObjectType } from './buildObjectType.js'
|
||||||
import buildPaginatedListType from './buildPaginatedListType.js'
|
import buildPaginatedListType from './buildPaginatedListType.js'
|
||||||
import { buildPolicyType } from './buildPoliciesType.js'
|
import { buildPolicyType } from './buildPoliciesType.js'
|
||||||
import buildWhereInputType from './buildWhereInputType.js'
|
import buildWhereInputType from './buildWhereInputType.js'
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import type { GenericLanguages, I18n, I18nClient } from '@payloadcms/translation
|
|||||||
import type { JSONSchema4 } from 'json-schema'
|
import type { JSONSchema4 } from 'json-schema'
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
|
|
||||||
|
import type { SanitizedCollectionConfig, TypeWithID } from '../collections/config/types.js'
|
||||||
import type { SanitizedConfig } from '../config/types.js'
|
import type { SanitizedConfig } from '../config/types.js'
|
||||||
import type { Field, FieldBase, RichTextField, Validate } from '../fields/config/types.js'
|
import type { Field, FieldAffectingData, RichTextField, Validate } from '../fields/config/types.js'
|
||||||
|
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
|
||||||
import type { PayloadRequestWithData, RequestContext } from '../types/index.js'
|
import type { PayloadRequestWithData, RequestContext } from '../types/index.js'
|
||||||
import type { WithServerSidePropsComponentProps } from './elements/WithServerSideProps.js'
|
import type { WithServerSidePropsComponentProps } from './elements/WithServerSideProps.js'
|
||||||
|
|
||||||
@@ -15,6 +17,173 @@ export type RichTextFieldProps<
|
|||||||
path?: string
|
path?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AfterReadRichTextHookArgs<
|
||||||
|
TData extends TypeWithID = any,
|
||||||
|
TValue = any,
|
||||||
|
TSiblingData = any,
|
||||||
|
> = {
|
||||||
|
currentDepth?: number
|
||||||
|
|
||||||
|
depth?: number
|
||||||
|
|
||||||
|
draft?: boolean
|
||||||
|
|
||||||
|
fallbackLocale?: string
|
||||||
|
|
||||||
|
fieldPromises?: Promise<void>[]
|
||||||
|
|
||||||
|
/** Boolean to denote if this hook is running against finding one, or finding many within the afterRead hook. */
|
||||||
|
findMany?: boolean
|
||||||
|
|
||||||
|
flattenLocales?: boolean
|
||||||
|
|
||||||
|
locale?: string
|
||||||
|
|
||||||
|
/** A string relating to which operation the field type is currently executing within. */
|
||||||
|
operation?: 'create' | 'delete' | 'read' | 'update'
|
||||||
|
|
||||||
|
overrideAccess?: boolean
|
||||||
|
|
||||||
|
populationPromises?: Promise<void>[]
|
||||||
|
showHiddenFields?: boolean
|
||||||
|
triggerAccessControl?: boolean
|
||||||
|
triggerHooks?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AfterChangeRichTextHookArgs<
|
||||||
|
TData extends TypeWithID = any,
|
||||||
|
TValue = any,
|
||||||
|
TSiblingData = any,
|
||||||
|
> = {
|
||||||
|
/** A string relating to which operation the field type is currently executing within. */
|
||||||
|
operation: 'create' | 'update'
|
||||||
|
/** The document before changes were applied. */
|
||||||
|
previousDoc?: TData
|
||||||
|
/** The sibling data of the document before changes being applied. */
|
||||||
|
previousSiblingDoc?: TData
|
||||||
|
/** The previous value of the field, before changes */
|
||||||
|
previousValue?: TValue
|
||||||
|
}
|
||||||
|
export type BeforeValidateRichTextHookArgs<
|
||||||
|
TData extends TypeWithID = any,
|
||||||
|
TValue = any,
|
||||||
|
TSiblingData = any,
|
||||||
|
> = {
|
||||||
|
/** A string relating to which operation the field type is currently executing within. */
|
||||||
|
operation: 'create' | 'update'
|
||||||
|
overrideAccess?: boolean
|
||||||
|
/** The sibling data of the document before changes being applied. */
|
||||||
|
previousSiblingDoc?: TData
|
||||||
|
/** The previous value of the field, before changes */
|
||||||
|
previousValue?: TValue
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BeforeChangeRichTextHookArgs<
|
||||||
|
TData extends TypeWithID = any,
|
||||||
|
TValue = any,
|
||||||
|
TSiblingData = any,
|
||||||
|
> = {
|
||||||
|
/**
|
||||||
|
* The original data with locales (not modified by any hooks). Only available in `beforeChange` and `beforeDuplicate` field hooks.
|
||||||
|
*/
|
||||||
|
docWithLocales?: Record<string, unknown>
|
||||||
|
|
||||||
|
duplicate?: boolean
|
||||||
|
|
||||||
|
errors?: { field: string; message: string }[]
|
||||||
|
/** Only available in `beforeChange` field hooks */
|
||||||
|
mergeLocaleActions?: (() => Promise<void>)[]
|
||||||
|
/** A string relating to which operation the field type is currently executing within. */
|
||||||
|
operation?: 'create' | 'delete' | 'read' | 'update'
|
||||||
|
/** The sibling data of the document before changes being applied. */
|
||||||
|
previousSiblingDoc?: TData
|
||||||
|
/** The previous value of the field, before changes */
|
||||||
|
previousValue?: TValue
|
||||||
|
/**
|
||||||
|
* The original siblingData with locales (not modified by any hooks).
|
||||||
|
*/
|
||||||
|
siblingDocWithLocales?: Record<string, unknown>
|
||||||
|
|
||||||
|
skipValidation?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BaseRichTextHookArgs<
|
||||||
|
TData extends TypeWithID = any,
|
||||||
|
TValue = any,
|
||||||
|
TSiblingData = any,
|
||||||
|
> = {
|
||||||
|
/** The collection which the field belongs to. If the field belongs to a global, this will be null. */
|
||||||
|
collection: SanitizedCollectionConfig | null
|
||||||
|
context: RequestContext
|
||||||
|
/** The data passed to update the document within create and update operations, and the full document itself in the afterRead hook. */
|
||||||
|
data?: Partial<TData>
|
||||||
|
/** The field which the hook is running against. */
|
||||||
|
field: FieldAffectingData
|
||||||
|
/** The global which the field belongs to. If the field belongs to a collection, this will be null. */
|
||||||
|
global: SanitizedGlobalConfig | null
|
||||||
|
|
||||||
|
/** The full original document in `update` operations. In the `afterChange` hook, this is the resulting document of the operation. */
|
||||||
|
originalDoc?: TData
|
||||||
|
/**
|
||||||
|
* The path of the field, e.g. ["group", "myArray", 1, "textField"]. The path is the schemaPath but with indexes and would be used in the context of field data, not field schemas.
|
||||||
|
*/
|
||||||
|
path: (number | string)[]
|
||||||
|
|
||||||
|
/** The Express request object. It is mocked for Local API operations. */
|
||||||
|
req: PayloadRequestWithData
|
||||||
|
/**
|
||||||
|
* The schemaPath of the field, e.g. ["group", "myArray", "textField"]. The schemaPath is the path but without indexes and would be used in the context of field schemas, not field data.
|
||||||
|
*/
|
||||||
|
schemaPath: string[]
|
||||||
|
/** The sibling data passed to a field that the hook is running against. */
|
||||||
|
siblingData: Partial<TSiblingData>
|
||||||
|
/** The value of the field. */
|
||||||
|
value?: TValue
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AfterReadRichTextHook<
|
||||||
|
TData extends TypeWithID = any,
|
||||||
|
TValue = any,
|
||||||
|
TSiblingData = any,
|
||||||
|
> = (
|
||||||
|
args: BaseRichTextHookArgs<TData, TValue, TSiblingData> &
|
||||||
|
AfterReadRichTextHookArgs<TData, TValue, TSiblingData>,
|
||||||
|
) => Promise<TValue> | TValue
|
||||||
|
|
||||||
|
export type AfterChangeRichTextHook<
|
||||||
|
TData extends TypeWithID = any,
|
||||||
|
TValue = any,
|
||||||
|
TSiblingData = any,
|
||||||
|
> = (
|
||||||
|
args: BaseRichTextHookArgs<TData, TValue, TSiblingData> &
|
||||||
|
AfterChangeRichTextHookArgs<TData, TValue, TSiblingData>,
|
||||||
|
) => Promise<TValue> | TValue
|
||||||
|
|
||||||
|
export type BeforeChangeRichTextHook<
|
||||||
|
TData extends TypeWithID = any,
|
||||||
|
TValue = any,
|
||||||
|
TSiblingData = any,
|
||||||
|
> = (
|
||||||
|
args: BaseRichTextHookArgs<TData, TValue, TSiblingData> &
|
||||||
|
BeforeChangeRichTextHookArgs<TData, TValue, TSiblingData>,
|
||||||
|
) => Promise<TValue> | TValue
|
||||||
|
|
||||||
|
export type BeforeValidateRichTextHook<
|
||||||
|
TData extends TypeWithID = any,
|
||||||
|
TValue = any,
|
||||||
|
TSiblingData = any,
|
||||||
|
> = (
|
||||||
|
args: BaseRichTextHookArgs<TData, TValue, TSiblingData> &
|
||||||
|
BeforeValidateRichTextHookArgs<TData, TValue, TSiblingData>,
|
||||||
|
) => Promise<TValue> | TValue
|
||||||
|
|
||||||
|
export type RichTextHooks = {
|
||||||
|
afterChange?: AfterChangeRichTextHook[]
|
||||||
|
afterRead?: AfterReadRichTextHook[]
|
||||||
|
beforeChange?: BeforeChangeRichTextHook[]
|
||||||
|
beforeValidate?: BeforeValidateRichTextHook[]
|
||||||
|
}
|
||||||
|
|
||||||
type RichTextAdapterBase<
|
type RichTextAdapterBase<
|
||||||
Value extends object = object,
|
Value extends object = object,
|
||||||
AdapterProps = any,
|
AdapterProps = any,
|
||||||
@@ -32,7 +201,28 @@ type RichTextAdapterBase<
|
|||||||
schemaMap: Map<string, Field[]>
|
schemaMap: Map<string, Field[]>
|
||||||
schemaPath: string
|
schemaPath: string
|
||||||
}) => Map<string, Field[]>
|
}) => Map<string, Field[]>
|
||||||
hooks?: FieldBase['hooks']
|
/**
|
||||||
|
* Like an afterRead hook, but runs only for the GraphQL resolver. For populating data, this should be used, as afterRead hooks do not have a depth in graphQL.
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
graphQLPopulationPromises?: (data: {
|
||||||
|
context: RequestContext
|
||||||
|
currentDepth?: number
|
||||||
|
depth: number
|
||||||
|
draft: boolean
|
||||||
|
field: RichTextField<Value, AdapterProps, ExtraFieldProperties>
|
||||||
|
fieldPromises: Promise<void>[]
|
||||||
|
findMany: boolean
|
||||||
|
flattenLocales: boolean
|
||||||
|
overrideAccess?: boolean
|
||||||
|
populationPromises: Promise<void>[]
|
||||||
|
req: PayloadRequestWithData
|
||||||
|
showHiddenFields: boolean
|
||||||
|
siblingDoc: Record<string, unknown>
|
||||||
|
}) => void
|
||||||
|
hooks?: RichTextHooks
|
||||||
i18n?: Partial<GenericLanguages>
|
i18n?: Partial<GenericLanguages>
|
||||||
outputSchema?: ({
|
outputSchema?: ({
|
||||||
collectionIDFieldTypes,
|
collectionIDFieldTypes,
|
||||||
@@ -50,27 +240,6 @@ type RichTextAdapterBase<
|
|||||||
interfaceNameDefinitions: Map<string, JSONSchema4>
|
interfaceNameDefinitions: Map<string, JSONSchema4>
|
||||||
isRequired: boolean
|
isRequired: boolean
|
||||||
}) => JSONSchema4
|
}) => JSONSchema4
|
||||||
/**
|
|
||||||
* Like an afterRead hook, but runs for both afterRead AND in the GraphQL resolver. For populating data, this should be used.
|
|
||||||
*
|
|
||||||
* To populate stuff / resolve field hooks, mutate the incoming populationPromises or fieldPromises array. They will then be awaited in the correct order within payload itself.
|
|
||||||
* @param data
|
|
||||||
*/
|
|
||||||
populationPromises?: (data: {
|
|
||||||
context: RequestContext
|
|
||||||
currentDepth?: number
|
|
||||||
depth: number
|
|
||||||
draft: boolean
|
|
||||||
field: RichTextField<Value, AdapterProps, ExtraFieldProperties>
|
|
||||||
fieldPromises: Promise<void>[]
|
|
||||||
findMany: boolean
|
|
||||||
flattenLocales: boolean
|
|
||||||
overrideAccess?: boolean
|
|
||||||
populationPromises: Promise<void>[]
|
|
||||||
req: PayloadRequestWithData
|
|
||||||
showHiddenFields: boolean
|
|
||||||
siblingDoc: Record<string, unknown>
|
|
||||||
}) => void
|
|
||||||
validate: Validate<
|
validate: Validate<
|
||||||
Value,
|
Value,
|
||||||
Value,
|
Value,
|
||||||
|
|||||||
@@ -172,25 +172,6 @@ export const sanitizeFields = async ({
|
|||||||
if (field.editor.i18n && Object.keys(field.editor.i18n).length >= 0) {
|
if (field.editor.i18n && Object.keys(field.editor.i18n).length >= 0) {
|
||||||
config.i18n.translations = deepMerge(config.i18n.translations, field.editor.i18n)
|
config.i18n.translations = deepMerge(config.i18n.translations, field.editor.i18n)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add editor adapter hooks to field hooks
|
|
||||||
if (!field.hooks) field.hooks = {}
|
|
||||||
|
|
||||||
const mergeHooks = (hookName: keyof typeof field.editor.hooks) => {
|
|
||||||
if (typeof field.editor === 'function') return
|
|
||||||
|
|
||||||
if (field.editor?.hooks?.[hookName]?.length) {
|
|
||||||
field.hooks[hookName] = field.hooks[hookName]
|
|
||||||
? field.hooks[hookName].concat(field.editor.hooks[hookName])
|
|
||||||
: [...field.editor.hooks[hookName]]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mergeHooks('afterRead')
|
|
||||||
mergeHooks('afterChange')
|
|
||||||
mergeHooks('beforeChange')
|
|
||||||
mergeHooks('beforeValidate')
|
|
||||||
mergeHooks('beforeDuplicate')
|
|
||||||
}
|
}
|
||||||
if (richTextSanitizationPromises) {
|
if (richTextSanitizationPromises) {
|
||||||
richTextSanitizationPromises.push(sanitizeRichText)
|
richTextSanitizationPromises.push(sanitizeRichText)
|
||||||
|
|||||||
@@ -499,8 +499,8 @@ export const richText = baseField.keys({
|
|||||||
CellComponent: componentSchema.optional(),
|
CellComponent: componentSchema.optional(),
|
||||||
FieldComponent: componentSchema.optional(),
|
FieldComponent: componentSchema.optional(),
|
||||||
afterReadPromise: joi.func().optional(),
|
afterReadPromise: joi.func().optional(),
|
||||||
|
graphQLPopulationPromises: joi.func().optional(),
|
||||||
outputSchema: joi.func().optional(),
|
outputSchema: joi.func().optional(),
|
||||||
populationPromise: joi.func().optional(),
|
|
||||||
validate: joi.func().required(),
|
validate: joi.func().required(),
|
||||||
})
|
})
|
||||||
.unknown(),
|
.unknown(),
|
||||||
|
|||||||
@@ -41,16 +41,28 @@ export type FieldHookArgs<TData extends TypeWithID = any, TValue = any, TSibling
|
|||||||
/** The full original document in `update` operations. In the `afterChange` hook, this is the resulting document of the operation. */
|
/** The full original document in `update` operations. In the `afterChange` hook, this is the resulting document of the operation. */
|
||||||
originalDoc?: TData
|
originalDoc?: TData
|
||||||
overrideAccess?: boolean
|
overrideAccess?: boolean
|
||||||
|
/**
|
||||||
|
* The path of the field, e.g. ["group", "myArray", 1, "textField"]. The path is the schemaPath but with indexes and would be used in the context of field data, not field schemas.
|
||||||
|
*/
|
||||||
|
path: (number | string)[]
|
||||||
/** The document before changes were applied, only in `afterChange` hooks. */
|
/** The document before changes were applied, only in `afterChange` hooks. */
|
||||||
previousDoc?: TData
|
previousDoc?: TData
|
||||||
/** The sibling data of the document before changes being applied, only in `beforeChange` and `afterChange` hook. */
|
/** The sibling data of the document before changes being applied, only in `beforeChange`, `beforeValidate`, `beforeDuplicate` and `afterChange` field hooks. */
|
||||||
previousSiblingDoc?: TData
|
previousSiblingDoc?: TData
|
||||||
/** The previous value of the field, before changes, only in `beforeChange`, `afterChange` and `beforeValidate` hooks. */
|
/** The previous value of the field, before changes, only in `beforeChange`, `afterChange`, `beforeDuplicate` and `beforeValidate` field hooks. */
|
||||||
previousValue?: TValue
|
previousValue?: TValue
|
||||||
/** The Express request object. It is mocked for Local API operations. */
|
/** The Express request object. It is mocked for Local API operations. */
|
||||||
req: PayloadRequestWithData
|
req: PayloadRequestWithData
|
||||||
|
/**
|
||||||
|
* The schemaPath of the field, e.g. ["group", "myArray", "textField"]. The schemaPath is the path but without indexes and would be used in the context of field schemas, not field data.
|
||||||
|
*/
|
||||||
|
schemaPath: string[]
|
||||||
/** The sibling data passed to a field that the hook is running against. */
|
/** The sibling data passed to a field that the hook is running against. */
|
||||||
siblingData: Partial<TSiblingData>
|
siblingData: Partial<TSiblingData>
|
||||||
|
/**
|
||||||
|
* The original siblingData with locales (not modified by any hooks). Only available in `beforeChange` and `beforeDuplicate` field hooks.
|
||||||
|
*/
|
||||||
|
siblingDocWithLocales?: Record<string, unknown>
|
||||||
/** The value of the field. */
|
/** The value of the field. */
|
||||||
value?: TValue
|
value?: TValue
|
||||||
}
|
}
|
||||||
|
|||||||
39
packages/payload/src/fields/getFieldPaths.ts
Normal file
39
packages/payload/src/fields/getFieldPaths.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import type { Field, TabAsField } from './config/types.js'
|
||||||
|
|
||||||
|
import { tabHasName } from './config/types.js'
|
||||||
|
|
||||||
|
export function getFieldPaths({
|
||||||
|
field,
|
||||||
|
parentPath,
|
||||||
|
parentSchemaPath,
|
||||||
|
}: {
|
||||||
|
field: Field | TabAsField
|
||||||
|
parentPath: (number | string)[]
|
||||||
|
parentSchemaPath: string[]
|
||||||
|
}): {
|
||||||
|
path: (number | string)[]
|
||||||
|
schemaPath: string[]
|
||||||
|
} {
|
||||||
|
if (field.type === 'tabs' || field.type === 'row' || field.type === 'collapsible') {
|
||||||
|
return {
|
||||||
|
path: parentPath,
|
||||||
|
schemaPath: parentSchemaPath,
|
||||||
|
}
|
||||||
|
} else if (field.type === 'tab') {
|
||||||
|
if (tabHasName(field)) {
|
||||||
|
return {
|
||||||
|
path: [...parentPath, field.name],
|
||||||
|
schemaPath: [...parentSchemaPath, field.name],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
path: parentPath,
|
||||||
|
schemaPath: parentSchemaPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const path = parentPath?.length ? [...parentPath, field.name] : [field.name]
|
||||||
|
const schemaPath = parentSchemaPath?.length ? [...parentSchemaPath, field.name] : [field.name]
|
||||||
|
|
||||||
|
return { path, schemaPath }
|
||||||
|
}
|
||||||
@@ -8,7 +8,13 @@ import { traverseFields } from './traverseFields.js'
|
|||||||
type Args<T> = {
|
type Args<T> = {
|
||||||
collection: SanitizedCollectionConfig | null
|
collection: SanitizedCollectionConfig | null
|
||||||
context: RequestContext
|
context: RequestContext
|
||||||
|
/**
|
||||||
|
* The data before hooks
|
||||||
|
*/
|
||||||
data: Record<string, unknown> | T
|
data: Record<string, unknown> | T
|
||||||
|
/**
|
||||||
|
* The data after hooks
|
||||||
|
*/
|
||||||
doc: Record<string, unknown> | T
|
doc: Record<string, unknown> | T
|
||||||
global: SanitizedGlobalConfig | null
|
global: SanitizedGlobalConfig | null
|
||||||
operation: 'create' | 'update'
|
operation: 'create' | 'update'
|
||||||
@@ -24,7 +30,6 @@ export const afterChange = async <T extends Record<string, unknown>>({
|
|||||||
collection,
|
collection,
|
||||||
context,
|
context,
|
||||||
data,
|
data,
|
||||||
|
|
||||||
doc: incomingDoc,
|
doc: incomingDoc,
|
||||||
global,
|
global,
|
||||||
operation,
|
operation,
|
||||||
@@ -41,9 +46,11 @@ export const afterChange = async <T extends Record<string, unknown>>({
|
|||||||
fields: collection?.fields || global?.fields,
|
fields: collection?.fields || global?.fields,
|
||||||
global,
|
global,
|
||||||
operation,
|
operation,
|
||||||
|
path: [],
|
||||||
previousDoc,
|
previousDoc,
|
||||||
previousSiblingDoc: previousDoc,
|
previousSiblingDoc: previousDoc,
|
||||||
req,
|
req,
|
||||||
|
schemaPath: [],
|
||||||
siblingData: data,
|
siblingData: data,
|
||||||
siblingDoc: doc,
|
siblingDoc: doc,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
|
import type { RichTextAdapter } from '../../../admin/RichText.js'
|
||||||
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
||||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||||
import type { PayloadRequestWithData, RequestContext } from '../../../types/index.js'
|
import type { PayloadRequestWithData, RequestContext } from '../../../types/index.js'
|
||||||
import type { Field, TabAsField } from '../../config/types.js'
|
import type { Field, TabAsField } from '../../config/types.js'
|
||||||
|
|
||||||
|
import { MissingEditorProp } from '../../../errors/index.js'
|
||||||
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
||||||
|
import { getFieldPaths } from '../../getFieldPaths.js'
|
||||||
import { traverseFields } from './traverseFields.js'
|
import { traverseFields } from './traverseFields.js'
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
@@ -15,6 +18,14 @@ type Args = {
|
|||||||
field: Field | TabAsField
|
field: Field | TabAsField
|
||||||
global: SanitizedGlobalConfig | null
|
global: SanitizedGlobalConfig | null
|
||||||
operation: 'create' | 'update'
|
operation: 'create' | 'update'
|
||||||
|
/**
|
||||||
|
* The parent's path
|
||||||
|
*/
|
||||||
|
parentPath: (number | string)[]
|
||||||
|
/**
|
||||||
|
* The parent's schemaPath (path without indexes).
|
||||||
|
*/
|
||||||
|
parentSchemaPath: string[]
|
||||||
previousDoc: Record<string, unknown>
|
previousDoc: Record<string, unknown>
|
||||||
previousSiblingDoc: Record<string, unknown>
|
previousSiblingDoc: Record<string, unknown>
|
||||||
req: PayloadRequestWithData
|
req: PayloadRequestWithData
|
||||||
@@ -33,12 +44,20 @@ export const promise = async ({
|
|||||||
field,
|
field,
|
||||||
global,
|
global,
|
||||||
operation,
|
operation,
|
||||||
|
parentPath,
|
||||||
|
parentSchemaPath,
|
||||||
previousDoc,
|
previousDoc,
|
||||||
previousSiblingDoc,
|
previousSiblingDoc,
|
||||||
req,
|
req,
|
||||||
siblingData,
|
siblingData,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
}: Args): Promise<void> => {
|
}: Args): Promise<void> => {
|
||||||
|
const { path: fieldPath, schemaPath: fieldSchemaPath } = getFieldPaths({
|
||||||
|
field,
|
||||||
|
parentPath,
|
||||||
|
parentSchemaPath,
|
||||||
|
})
|
||||||
|
|
||||||
if (fieldAffectsData(field)) {
|
if (fieldAffectsData(field)) {
|
||||||
// Execute hooks
|
// Execute hooks
|
||||||
if (field.hooks?.afterChange) {
|
if (field.hooks?.afterChange) {
|
||||||
@@ -53,12 +72,14 @@ export const promise = async ({
|
|||||||
global,
|
global,
|
||||||
operation,
|
operation,
|
||||||
originalDoc: doc,
|
originalDoc: doc,
|
||||||
|
path: fieldPath,
|
||||||
previousDoc,
|
previousDoc,
|
||||||
previousSiblingDoc,
|
previousSiblingDoc,
|
||||||
previousValue: previousDoc[field.name],
|
previousValue: previousDoc[field.name],
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
siblingData,
|
siblingData,
|
||||||
value: siblingData[field.name],
|
value: siblingDoc[field.name],
|
||||||
})
|
})
|
||||||
|
|
||||||
if (hookedValue !== undefined) {
|
if (hookedValue !== undefined) {
|
||||||
@@ -79,9 +100,11 @@ export const promise = async ({
|
|||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
global,
|
global,
|
||||||
operation,
|
operation,
|
||||||
|
path: fieldPath,
|
||||||
previousDoc,
|
previousDoc,
|
||||||
previousSiblingDoc: previousDoc[field.name] as Record<string, unknown>,
|
previousSiblingDoc: previousDoc[field.name] as Record<string, unknown>,
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
siblingData: (siblingData?.[field.name] as Record<string, unknown>) || {},
|
siblingData: (siblingData?.[field.name] as Record<string, unknown>) || {},
|
||||||
siblingDoc: siblingDoc[field.name] as Record<string, unknown>,
|
siblingDoc: siblingDoc[field.name] as Record<string, unknown>,
|
||||||
})
|
})
|
||||||
@@ -104,9 +127,11 @@ export const promise = async ({
|
|||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
global,
|
global,
|
||||||
operation,
|
operation,
|
||||||
|
path: [...fieldPath, i],
|
||||||
previousDoc,
|
previousDoc,
|
||||||
previousSiblingDoc: previousDoc?.[field.name]?.[i] || ({} as Record<string, unknown>),
|
previousSiblingDoc: previousDoc?.[field.name]?.[i] || ({} as Record<string, unknown>),
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
siblingData: siblingData?.[field.name]?.[i] || {},
|
siblingData: siblingData?.[field.name]?.[i] || {},
|
||||||
siblingDoc: { ...row } || {},
|
siblingDoc: { ...row } || {},
|
||||||
}),
|
}),
|
||||||
@@ -135,10 +160,12 @@ export const promise = async ({
|
|||||||
fields: block.fields,
|
fields: block.fields,
|
||||||
global,
|
global,
|
||||||
operation,
|
operation,
|
||||||
|
path: [...fieldPath, i],
|
||||||
previousDoc,
|
previousDoc,
|
||||||
previousSiblingDoc:
|
previousSiblingDoc:
|
||||||
previousDoc?.[field.name]?.[i] || ({} as Record<string, unknown>),
|
previousDoc?.[field.name]?.[i] || ({} as Record<string, unknown>),
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
siblingData: siblingData?.[field.name]?.[i] || {},
|
siblingData: siblingData?.[field.name]?.[i] || {},
|
||||||
siblingDoc: { ...row } || {},
|
siblingDoc: { ...row } || {},
|
||||||
}),
|
}),
|
||||||
@@ -161,9 +188,11 @@ export const promise = async ({
|
|||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
global,
|
global,
|
||||||
operation,
|
operation,
|
||||||
|
path: fieldPath,
|
||||||
previousDoc,
|
previousDoc,
|
||||||
previousSiblingDoc: { ...previousSiblingDoc },
|
previousSiblingDoc: { ...previousSiblingDoc },
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
siblingData: siblingData || {},
|
siblingData: siblingData || {},
|
||||||
siblingDoc: { ...siblingDoc },
|
siblingDoc: { ...siblingDoc },
|
||||||
})
|
})
|
||||||
@@ -190,9 +219,11 @@ export const promise = async ({
|
|||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
global,
|
global,
|
||||||
operation,
|
operation,
|
||||||
|
path: fieldPath,
|
||||||
previousDoc,
|
previousDoc,
|
||||||
previousSiblingDoc: tabPreviousSiblingDoc,
|
previousSiblingDoc: tabPreviousSiblingDoc,
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
siblingData: tabSiblingData,
|
siblingData: tabSiblingData,
|
||||||
siblingDoc: tabSiblingDoc,
|
siblingDoc: tabSiblingDoc,
|
||||||
})
|
})
|
||||||
@@ -209,15 +240,57 @@ export const promise = async ({
|
|||||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||||
global,
|
global,
|
||||||
operation,
|
operation,
|
||||||
|
path: fieldPath,
|
||||||
previousDoc,
|
previousDoc,
|
||||||
previousSiblingDoc: { ...previousSiblingDoc },
|
previousSiblingDoc: { ...previousSiblingDoc },
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
siblingData: siblingData || {},
|
siblingData: siblingData || {},
|
||||||
siblingDoc: { ...siblingDoc },
|
siblingDoc: { ...siblingDoc },
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'richText': {
|
||||||
|
if (!field?.editor) {
|
||||||
|
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||||
|
}
|
||||||
|
if (typeof field?.editor === 'function') {
|
||||||
|
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor: RichTextAdapter = field?.editor
|
||||||
|
|
||||||
|
if (editor?.hooks?.afterChange?.length) {
|
||||||
|
await editor.hooks.afterChange.reduce(async (priorHook, currentHook) => {
|
||||||
|
await priorHook
|
||||||
|
|
||||||
|
const hookedValue = await currentHook({
|
||||||
|
collection,
|
||||||
|
context,
|
||||||
|
data,
|
||||||
|
field,
|
||||||
|
global,
|
||||||
|
operation,
|
||||||
|
originalDoc: doc,
|
||||||
|
path: fieldPath,
|
||||||
|
previousDoc,
|
||||||
|
previousSiblingDoc,
|
||||||
|
previousValue: previousDoc[field.name],
|
||||||
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
|
siblingData,
|
||||||
|
value: siblingDoc[field.name],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hookedValue !== undefined) {
|
||||||
|
siblingDoc[field.name] = hookedValue
|
||||||
|
}
|
||||||
|
}, Promise.resolve())
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ type Args = {
|
|||||||
fields: (Field | TabAsField)[]
|
fields: (Field | TabAsField)[]
|
||||||
global: SanitizedGlobalConfig | null
|
global: SanitizedGlobalConfig | null
|
||||||
operation: 'create' | 'update'
|
operation: 'create' | 'update'
|
||||||
|
path: (number | string)[]
|
||||||
previousDoc: Record<string, unknown>
|
previousDoc: Record<string, unknown>
|
||||||
previousSiblingDoc: Record<string, unknown>
|
previousSiblingDoc: Record<string, unknown>
|
||||||
req: PayloadRequestWithData
|
req: PayloadRequestWithData
|
||||||
|
schemaPath: string[]
|
||||||
siblingData: Record<string, unknown>
|
siblingData: Record<string, unknown>
|
||||||
siblingDoc: Record<string, unknown>
|
siblingDoc: Record<string, unknown>
|
||||||
}
|
}
|
||||||
@@ -28,9 +30,11 @@ export const traverseFields = async ({
|
|||||||
fields,
|
fields,
|
||||||
global,
|
global,
|
||||||
operation,
|
operation,
|
||||||
|
path,
|
||||||
previousDoc,
|
previousDoc,
|
||||||
previousSiblingDoc,
|
previousSiblingDoc,
|
||||||
req,
|
req,
|
||||||
|
schemaPath,
|
||||||
siblingData,
|
siblingData,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
}: Args): Promise<void> => {
|
}: Args): Promise<void> => {
|
||||||
@@ -46,6 +50,8 @@ export const traverseFields = async ({
|
|||||||
field,
|
field,
|
||||||
global,
|
global,
|
||||||
operation,
|
operation,
|
||||||
|
parentPath: path,
|
||||||
|
parentSchemaPath: schemaPath,
|
||||||
previousDoc,
|
previousDoc,
|
||||||
previousSiblingDoc,
|
previousSiblingDoc,
|
||||||
req,
|
req,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ type Args = {
|
|||||||
/**
|
/**
|
||||||
* This function is responsible for the following actions, in order:
|
* This function is responsible for the following actions, in order:
|
||||||
* - Remove hidden fields from response
|
* - Remove hidden fields from response
|
||||||
* - Flatten locales into requested locale
|
* - Flatten locales into requested locale. If the input doc contains all locales, the output doc after this function will only contain the requested locale.
|
||||||
* - Sanitize outgoing data (point field, etc.)
|
* - Sanitize outgoing data (point field, etc.)
|
||||||
* - Execute field hooks
|
* - Execute field hooks
|
||||||
* - Execute read access control
|
* - Execute read access control
|
||||||
@@ -77,8 +77,10 @@ export async function afterRead<T = any>(args: Args): Promise<T> {
|
|||||||
global,
|
global,
|
||||||
locale,
|
locale,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
path: [],
|
||||||
populationPromises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
|
schemaPath: [],
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc: doc,
|
siblingDoc: doc,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
import type { RichTextAdapter } from '../../../admin/types.js'
|
import type { RichTextAdapter } from '../../../admin/RichText.js'
|
||||||
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
||||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||||
import type { PayloadRequestWithData, RequestContext } from '../../../types/index.js'
|
import type { PayloadRequestWithData, RequestContext } from '../../../types/index.js'
|
||||||
@@ -8,6 +8,7 @@ import type { Field, TabAsField } from '../../config/types.js'
|
|||||||
import { MissingEditorProp } from '../../../errors/index.js'
|
import { MissingEditorProp } from '../../../errors/index.js'
|
||||||
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
||||||
import getValueWithDefault from '../../getDefaultValue.js'
|
import getValueWithDefault from '../../getDefaultValue.js'
|
||||||
|
import { getFieldPaths } from '../../getFieldPaths.js'
|
||||||
import { relationshipPopulationPromise } from './relationshipPopulationPromise.js'
|
import { relationshipPopulationPromise } from './relationshipPopulationPromise.js'
|
||||||
import { traverseFields } from './traverseFields.js'
|
import { traverseFields } from './traverseFields.js'
|
||||||
|
|
||||||
@@ -29,6 +30,14 @@ type Args = {
|
|||||||
global: SanitizedGlobalConfig | null
|
global: SanitizedGlobalConfig | null
|
||||||
locale: null | string
|
locale: null | string
|
||||||
overrideAccess: boolean
|
overrideAccess: boolean
|
||||||
|
/**
|
||||||
|
* The parent's path.
|
||||||
|
*/
|
||||||
|
parentPath: (number | string)[]
|
||||||
|
/**
|
||||||
|
* The parent's schemaPath (path without indexes).
|
||||||
|
*/
|
||||||
|
parentSchemaPath: string[]
|
||||||
populationPromises: Promise<void>[]
|
populationPromises: Promise<void>[]
|
||||||
req: PayloadRequestWithData
|
req: PayloadRequestWithData
|
||||||
showHiddenFields: boolean
|
showHiddenFields: boolean
|
||||||
@@ -60,6 +69,8 @@ export const promise = async ({
|
|||||||
global,
|
global,
|
||||||
locale,
|
locale,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
parentPath,
|
||||||
|
parentSchemaPath,
|
||||||
populationPromises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
@@ -67,6 +78,12 @@ export const promise = async ({
|
|||||||
triggerAccessControl = true,
|
triggerAccessControl = true,
|
||||||
triggerHooks = true,
|
triggerHooks = true,
|
||||||
}: Args): Promise<void> => {
|
}: Args): Promise<void> => {
|
||||||
|
const { path: fieldPath, schemaPath: fieldSchemaPath } = getFieldPaths({
|
||||||
|
field,
|
||||||
|
parentPath,
|
||||||
|
parentSchemaPath,
|
||||||
|
})
|
||||||
|
|
||||||
if (
|
if (
|
||||||
fieldAffectsData(field) &&
|
fieldAffectsData(field) &&
|
||||||
field.hidden &&
|
field.hidden &&
|
||||||
@@ -151,29 +168,7 @@ export const promise = async ({
|
|||||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||||
}
|
}
|
||||||
|
|
||||||
const editor: RichTextAdapter = field?.editor
|
// Rich Text fields should use afterRead hooks to do population. The previous editor.populationPromises have been renamed to editor.graphQLPopulationPromises
|
||||||
// This is run here AND in the GraphQL Resolver
|
|
||||||
if (editor?.populationPromises) {
|
|
||||||
const populateDepth =
|
|
||||||
field?.maxDepth !== undefined && field?.maxDepth < depth ? field?.maxDepth : depth
|
|
||||||
|
|
||||||
editor.populationPromises({
|
|
||||||
context,
|
|
||||||
currentDepth,
|
|
||||||
depth: populateDepth,
|
|
||||||
draft,
|
|
||||||
field,
|
|
||||||
fieldPromises,
|
|
||||||
findMany,
|
|
||||||
flattenLocales,
|
|
||||||
overrideAccess,
|
|
||||||
populationPromises,
|
|
||||||
req,
|
|
||||||
showHiddenFields,
|
|
||||||
siblingDoc,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,10 +207,14 @@ export const promise = async ({
|
|||||||
context,
|
context,
|
||||||
data: doc,
|
data: doc,
|
||||||
field,
|
field,
|
||||||
|
findMany,
|
||||||
global,
|
global,
|
||||||
operation: 'read',
|
operation: 'read',
|
||||||
originalDoc: doc,
|
originalDoc: doc,
|
||||||
|
overrideAccess,
|
||||||
|
path: fieldPath,
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
siblingData: siblingDoc,
|
siblingData: siblingDoc,
|
||||||
value,
|
value,
|
||||||
})
|
})
|
||||||
@@ -238,7 +237,9 @@ export const promise = async ({
|
|||||||
operation: 'read',
|
operation: 'read',
|
||||||
originalDoc: doc,
|
originalDoc: doc,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
path: fieldPath,
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
siblingData: siblingDoc,
|
siblingData: siblingDoc,
|
||||||
value: siblingDoc[field.name],
|
value: siblingDoc[field.name],
|
||||||
})
|
})
|
||||||
@@ -322,8 +323,10 @@ export const promise = async ({
|
|||||||
global,
|
global,
|
||||||
locale,
|
locale,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
path: fieldPath,
|
||||||
populationPromises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc: groupDoc,
|
siblingDoc: groupDoc,
|
||||||
triggerAccessControl,
|
triggerAccessControl,
|
||||||
@@ -337,7 +340,7 @@ export const promise = async ({
|
|||||||
const rows = siblingDoc[field.name]
|
const rows = siblingDoc[field.name]
|
||||||
|
|
||||||
if (Array.isArray(rows)) {
|
if (Array.isArray(rows)) {
|
||||||
rows.forEach((row) => {
|
rows.forEach((row, i) => {
|
||||||
traverseFields({
|
traverseFields({
|
||||||
collection,
|
collection,
|
||||||
context,
|
context,
|
||||||
@@ -353,8 +356,10 @@ export const promise = async ({
|
|||||||
global,
|
global,
|
||||||
locale,
|
locale,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
path: [...fieldPath, i],
|
||||||
populationPromises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc: row || {},
|
siblingDoc: row || {},
|
||||||
triggerAccessControl,
|
triggerAccessControl,
|
||||||
@@ -364,7 +369,7 @@ export const promise = async ({
|
|||||||
} else if (!shouldHoistLocalizedValue && typeof rows === 'object' && rows !== null) {
|
} else if (!shouldHoistLocalizedValue && typeof rows === 'object' && rows !== null) {
|
||||||
Object.values(rows).forEach((localeRows) => {
|
Object.values(rows).forEach((localeRows) => {
|
||||||
if (Array.isArray(localeRows)) {
|
if (Array.isArray(localeRows)) {
|
||||||
localeRows.forEach((row) => {
|
localeRows.forEach((row, i) => {
|
||||||
traverseFields({
|
traverseFields({
|
||||||
collection,
|
collection,
|
||||||
context,
|
context,
|
||||||
@@ -380,8 +385,10 @@ export const promise = async ({
|
|||||||
global,
|
global,
|
||||||
locale,
|
locale,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
path: [...fieldPath, i],
|
||||||
populationPromises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc: row || {},
|
siblingDoc: row || {},
|
||||||
triggerAccessControl,
|
triggerAccessControl,
|
||||||
@@ -400,7 +407,7 @@ export const promise = async ({
|
|||||||
const rows = siblingDoc[field.name]
|
const rows = siblingDoc[field.name]
|
||||||
|
|
||||||
if (Array.isArray(rows)) {
|
if (Array.isArray(rows)) {
|
||||||
rows.forEach((row) => {
|
rows.forEach((row, i) => {
|
||||||
const block = field.blocks.find((blockType) => blockType.slug === row.blockType)
|
const block = field.blocks.find((blockType) => blockType.slug === row.blockType)
|
||||||
|
|
||||||
if (block) {
|
if (block) {
|
||||||
@@ -419,8 +426,10 @@ export const promise = async ({
|
|||||||
global,
|
global,
|
||||||
locale,
|
locale,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
path: [...fieldPath, i],
|
||||||
populationPromises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc: row || {},
|
siblingDoc: row || {},
|
||||||
triggerAccessControl,
|
triggerAccessControl,
|
||||||
@@ -431,7 +440,7 @@ export const promise = async ({
|
|||||||
} else if (!shouldHoistLocalizedValue && typeof rows === 'object' && rows !== null) {
|
} else if (!shouldHoistLocalizedValue && typeof rows === 'object' && rows !== null) {
|
||||||
Object.values(rows).forEach((localeRows) => {
|
Object.values(rows).forEach((localeRows) => {
|
||||||
if (Array.isArray(localeRows)) {
|
if (Array.isArray(localeRows)) {
|
||||||
localeRows.forEach((row) => {
|
localeRows.forEach((row, i) => {
|
||||||
const block = field.blocks.find((blockType) => blockType.slug === row.blockType)
|
const block = field.blocks.find((blockType) => blockType.slug === row.blockType)
|
||||||
|
|
||||||
if (block) {
|
if (block) {
|
||||||
@@ -450,8 +459,10 @@ export const promise = async ({
|
|||||||
global,
|
global,
|
||||||
locale,
|
locale,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
path: [...fieldPath, i],
|
||||||
populationPromises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc: row || {},
|
siblingDoc: row || {},
|
||||||
triggerAccessControl,
|
triggerAccessControl,
|
||||||
@@ -485,8 +496,10 @@ export const promise = async ({
|
|||||||
global,
|
global,
|
||||||
locale,
|
locale,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
path: fieldPath,
|
||||||
populationPromises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
triggerAccessControl,
|
triggerAccessControl,
|
||||||
@@ -518,8 +531,10 @@ export const promise = async ({
|
|||||||
global,
|
global,
|
||||||
locale,
|
locale,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
path: fieldPath,
|
||||||
populationPromises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc: tabDoc,
|
siblingDoc: tabDoc,
|
||||||
triggerAccessControl,
|
triggerAccessControl,
|
||||||
@@ -545,8 +560,10 @@ export const promise = async ({
|
|||||||
global,
|
global,
|
||||||
locale,
|
locale,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
path: fieldPath,
|
||||||
populationPromises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
triggerAccessControl,
|
triggerAccessControl,
|
||||||
@@ -555,6 +572,101 @@ export const promise = async ({
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'richText': {
|
||||||
|
if (!field?.editor) {
|
||||||
|
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||||
|
}
|
||||||
|
if (typeof field?.editor === 'function') {
|
||||||
|
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor: RichTextAdapter = field?.editor
|
||||||
|
|
||||||
|
if (editor?.hooks?.afterRead?.length) {
|
||||||
|
await editor.hooks.afterRead.reduce(async (priorHook, currentHook) => {
|
||||||
|
await priorHook
|
||||||
|
|
||||||
|
const shouldRunHookOnAllLocales =
|
||||||
|
field.localized &&
|
||||||
|
(locale === 'all' || !flattenLocales) &&
|
||||||
|
typeof siblingDoc[field.name] === 'object'
|
||||||
|
|
||||||
|
if (shouldRunHookOnAllLocales) {
|
||||||
|
const hookPromises = Object.entries(siblingDoc[field.name]).map(([locale, value]) =>
|
||||||
|
(async () => {
|
||||||
|
const hookedValue = await currentHook({
|
||||||
|
collection,
|
||||||
|
context,
|
||||||
|
currentDepth,
|
||||||
|
data: doc,
|
||||||
|
depth,
|
||||||
|
draft,
|
||||||
|
fallbackLocale,
|
||||||
|
field,
|
||||||
|
fieldPromises,
|
||||||
|
findMany,
|
||||||
|
flattenLocales,
|
||||||
|
global,
|
||||||
|
locale,
|
||||||
|
operation: 'read',
|
||||||
|
originalDoc: doc,
|
||||||
|
overrideAccess,
|
||||||
|
path: fieldPath,
|
||||||
|
populationPromises,
|
||||||
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
|
showHiddenFields,
|
||||||
|
siblingData: siblingDoc,
|
||||||
|
triggerAccessControl,
|
||||||
|
triggerHooks,
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hookedValue !== undefined) {
|
||||||
|
siblingDoc[field.name][locale] = hookedValue
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
)
|
||||||
|
|
||||||
|
await Promise.all(hookPromises)
|
||||||
|
} else {
|
||||||
|
const hookedValue = await currentHook({
|
||||||
|
collection,
|
||||||
|
context,
|
||||||
|
currentDepth,
|
||||||
|
data: doc,
|
||||||
|
depth,
|
||||||
|
draft,
|
||||||
|
fallbackLocale,
|
||||||
|
field,
|
||||||
|
fieldPromises,
|
||||||
|
findMany,
|
||||||
|
flattenLocales,
|
||||||
|
global,
|
||||||
|
locale,
|
||||||
|
operation: 'read',
|
||||||
|
originalDoc: doc,
|
||||||
|
overrideAccess,
|
||||||
|
path: fieldPath,
|
||||||
|
populationPromises,
|
||||||
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
|
showHiddenFields,
|
||||||
|
siblingData: siblingDoc,
|
||||||
|
triggerAccessControl,
|
||||||
|
triggerHooks,
|
||||||
|
value: siblingDoc[field.name],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hookedValue !== undefined) {
|
||||||
|
siblingDoc[field.name] = hookedValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, Promise.resolve())
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,10 @@ type Args = {
|
|||||||
global: SanitizedGlobalConfig | null
|
global: SanitizedGlobalConfig | null
|
||||||
locale: null | string
|
locale: null | string
|
||||||
overrideAccess: boolean
|
overrideAccess: boolean
|
||||||
|
path: (number | string)[]
|
||||||
populationPromises: Promise<void>[]
|
populationPromises: Promise<void>[]
|
||||||
req: PayloadRequestWithData
|
req: PayloadRequestWithData
|
||||||
|
schemaPath: string[]
|
||||||
showHiddenFields: boolean
|
showHiddenFields: boolean
|
||||||
siblingDoc: Record<string, unknown>
|
siblingDoc: Record<string, unknown>
|
||||||
triggerAccessControl?: boolean
|
triggerAccessControl?: boolean
|
||||||
@@ -46,8 +48,10 @@ export const traverseFields = ({
|
|||||||
global,
|
global,
|
||||||
locale,
|
locale,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
path,
|
||||||
populationPromises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
|
schemaPath,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
triggerAccessControl = true,
|
triggerAccessControl = true,
|
||||||
@@ -70,6 +74,8 @@ export const traverseFields = ({
|
|||||||
global,
|
global,
|
||||||
locale,
|
locale,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
parentPath: path,
|
||||||
|
parentSchemaPath: schemaPath,
|
||||||
populationPromises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ type Args<T> = {
|
|||||||
* - Validate data
|
* - Validate data
|
||||||
* - Transform data for storage
|
* - Transform data for storage
|
||||||
* - beforeDuplicate hooks (if duplicate)
|
* - beforeDuplicate hooks (if duplicate)
|
||||||
* - Unflatten locales
|
* - Unflatten locales. The input `data` is the normal document for one locale. The output result will become the document with locales.
|
||||||
*/
|
*/
|
||||||
export const beforeChange = async <T extends Record<string, unknown>>({
|
export const beforeChange = async <T extends Record<string, unknown>>({
|
||||||
id,
|
id,
|
||||||
@@ -59,8 +59,9 @@ export const beforeChange = async <T extends Record<string, unknown>>({
|
|||||||
global,
|
global,
|
||||||
mergeLocaleActions,
|
mergeLocaleActions,
|
||||||
operation,
|
operation,
|
||||||
path: '',
|
path: [],
|
||||||
req,
|
req,
|
||||||
|
schemaPath: [],
|
||||||
siblingData: data,
|
siblingData: data,
|
||||||
siblingDoc: doc,
|
siblingDoc: doc,
|
||||||
siblingDocWithLocales: docWithLocales,
|
siblingDocWithLocales: docWithLocales,
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import merge from 'deepmerge'
|
import merge from 'deepmerge'
|
||||||
|
|
||||||
|
import type { RichTextAdapter } from '../../../admin/RichText.js'
|
||||||
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
||||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||||
import type { Operation, PayloadRequestWithData, RequestContext } from '../../../types/index.js'
|
import type { Operation, PayloadRequestWithData, RequestContext } from '../../../types/index.js'
|
||||||
import type { Field, FieldHookArgs, TabAsField, ValidateOptions } from '../../config/types.js'
|
import type { Field, FieldHookArgs, TabAsField, ValidateOptions } from '../../config/types.js'
|
||||||
|
|
||||||
|
import { MissingEditorProp } from '../../../errors/index.js'
|
||||||
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
||||||
|
import { getFieldPaths } from '../../getFieldPaths.js'
|
||||||
import { beforeDuplicate } from './beforeDuplicate.js'
|
import { beforeDuplicate } from './beforeDuplicate.js'
|
||||||
import { getExistingRowDoc } from './getExistingRowDoc.js'
|
import { getExistingRowDoc } from './getExistingRowDoc.js'
|
||||||
import { traverseFields } from './traverseFields.js'
|
import { traverseFields } from './traverseFields.js'
|
||||||
@@ -23,7 +26,14 @@ type Args = {
|
|||||||
id?: number | string
|
id?: number | string
|
||||||
mergeLocaleActions: (() => Promise<void>)[]
|
mergeLocaleActions: (() => Promise<void>)[]
|
||||||
operation: Operation
|
operation: Operation
|
||||||
path: string
|
/**
|
||||||
|
* The parent's path.
|
||||||
|
*/
|
||||||
|
parentPath: (number | string)[]
|
||||||
|
/**
|
||||||
|
* The parent's schemaPath (path without indexes).
|
||||||
|
*/
|
||||||
|
parentSchemaPath: string[]
|
||||||
req: PayloadRequestWithData
|
req: PayloadRequestWithData
|
||||||
siblingData: Record<string, unknown>
|
siblingData: Record<string, unknown>
|
||||||
siblingDoc: Record<string, unknown>
|
siblingDoc: Record<string, unknown>
|
||||||
@@ -52,7 +62,8 @@ export const promise = async ({
|
|||||||
global,
|
global,
|
||||||
mergeLocaleActions,
|
mergeLocaleActions,
|
||||||
operation,
|
operation,
|
||||||
path,
|
parentPath,
|
||||||
|
parentSchemaPath,
|
||||||
req,
|
req,
|
||||||
siblingData,
|
siblingData,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
@@ -67,6 +78,12 @@ export const promise = async ({
|
|||||||
const defaultLocale = localization ? localization?.defaultLocale : 'en'
|
const defaultLocale = localization ? localization?.defaultLocale : 'en'
|
||||||
const operationLocale = req.locale || defaultLocale
|
const operationLocale = req.locale || defaultLocale
|
||||||
|
|
||||||
|
const { path: fieldPath, schemaPath: fieldSchemaPath } = getFieldPaths({
|
||||||
|
field,
|
||||||
|
parentPath,
|
||||||
|
parentSchemaPath,
|
||||||
|
})
|
||||||
|
|
||||||
if (fieldAffectsData(field)) {
|
if (fieldAffectsData(field)) {
|
||||||
// skip validation if the field is localized and the incoming data is null
|
// skip validation if the field is localized and the incoming data is null
|
||||||
if (field.localized && operationLocale !== defaultLocale) {
|
if (field.localized && operationLocale !== defaultLocale) {
|
||||||
@@ -88,10 +105,13 @@ export const promise = async ({
|
|||||||
global,
|
global,
|
||||||
operation,
|
operation,
|
||||||
originalDoc: doc,
|
originalDoc: doc,
|
||||||
|
path: fieldPath,
|
||||||
previousSiblingDoc: siblingDoc,
|
previousSiblingDoc: siblingDoc,
|
||||||
previousValue: siblingDoc[field.name],
|
previousValue: siblingDoc[field.name],
|
||||||
req,
|
req,
|
||||||
|
schemaPath: parentSchemaPath,
|
||||||
siblingData,
|
siblingData,
|
||||||
|
siblingDocWithLocales,
|
||||||
value: siblingData[field.name],
|
value: siblingData[field.name],
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -127,7 +147,7 @@ export const promise = async ({
|
|||||||
|
|
||||||
if (typeof validationResult === 'string') {
|
if (typeof validationResult === 'string') {
|
||||||
errors.push({
|
errors.push({
|
||||||
field: `${path}${field.name}`,
|
field: fieldPath.join('.'),
|
||||||
message: validationResult,
|
message: validationResult,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -139,8 +159,13 @@ export const promise = async ({
|
|||||||
data,
|
data,
|
||||||
field,
|
field,
|
||||||
global: undefined,
|
global: undefined,
|
||||||
|
path: fieldPath,
|
||||||
|
previousSiblingDoc: siblingDoc,
|
||||||
|
previousValue: siblingDoc[field.name],
|
||||||
req,
|
req,
|
||||||
|
schemaPath: parentSchemaPath,
|
||||||
siblingData,
|
siblingData,
|
||||||
|
siblingDocWithLocales,
|
||||||
value: siblingData[field.name],
|
value: siblingData[field.name],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,8 +250,9 @@ export const promise = async ({
|
|||||||
global,
|
global,
|
||||||
mergeLocaleActions,
|
mergeLocaleActions,
|
||||||
operation,
|
operation,
|
||||||
path: `${path}${field.name}.`,
|
path: fieldPath,
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
siblingData: siblingData[field.name] as Record<string, unknown>,
|
siblingData: siblingData[field.name] as Record<string, unknown>,
|
||||||
siblingDoc: siblingDoc[field.name] as Record<string, unknown>,
|
siblingDoc: siblingDoc[field.name] as Record<string, unknown>,
|
||||||
siblingDocWithLocales: siblingDocWithLocales[field.name] as Record<string, unknown>,
|
siblingDocWithLocales: siblingDocWithLocales[field.name] as Record<string, unknown>,
|
||||||
@@ -256,8 +282,9 @@ export const promise = async ({
|
|||||||
global,
|
global,
|
||||||
mergeLocaleActions,
|
mergeLocaleActions,
|
||||||
operation,
|
operation,
|
||||||
path: `${path}${field.name}.${i}.`,
|
path: [...fieldPath, i],
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
siblingData: row,
|
siblingData: row,
|
||||||
siblingDoc: getExistingRowDoc(row, siblingDoc[field.name]),
|
siblingDoc: getExistingRowDoc(row, siblingDoc[field.name]),
|
||||||
siblingDocWithLocales: getExistingRowDoc(row, siblingDocWithLocales[field.name]),
|
siblingDocWithLocales: getExistingRowDoc(row, siblingDocWithLocales[field.name]),
|
||||||
@@ -299,8 +326,9 @@ export const promise = async ({
|
|||||||
global,
|
global,
|
||||||
mergeLocaleActions,
|
mergeLocaleActions,
|
||||||
operation,
|
operation,
|
||||||
path: `${path}${field.name}.${i}.`,
|
path: [...fieldPath, i],
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
siblingData: row,
|
siblingData: row,
|
||||||
siblingDoc: rowSiblingDoc,
|
siblingDoc: rowSiblingDoc,
|
||||||
siblingDocWithLocales: rowSiblingDocWithLocales,
|
siblingDocWithLocales: rowSiblingDocWithLocales,
|
||||||
@@ -331,8 +359,9 @@ export const promise = async ({
|
|||||||
global,
|
global,
|
||||||
mergeLocaleActions,
|
mergeLocaleActions,
|
||||||
operation,
|
operation,
|
||||||
path,
|
path: fieldPath,
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
siblingData,
|
siblingData,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
siblingDocWithLocales,
|
siblingDocWithLocales,
|
||||||
@@ -343,13 +372,11 @@ export const promise = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'tab': {
|
case 'tab': {
|
||||||
let tabPath = path
|
|
||||||
let tabSiblingData = siblingData
|
let tabSiblingData = siblingData
|
||||||
let tabSiblingDoc = siblingDoc
|
let tabSiblingDoc = siblingDoc
|
||||||
let tabSiblingDocWithLocales = siblingDocWithLocales
|
let tabSiblingDocWithLocales = siblingDocWithLocales
|
||||||
|
|
||||||
if (tabHasName(field)) {
|
if (tabHasName(field)) {
|
||||||
tabPath = `${path}${field.name}.`
|
|
||||||
if (typeof siblingData[field.name] !== 'object') siblingData[field.name] = {}
|
if (typeof siblingData[field.name] !== 'object') siblingData[field.name] = {}
|
||||||
if (typeof siblingDoc[field.name] !== 'object') siblingDoc[field.name] = {}
|
if (typeof siblingDoc[field.name] !== 'object') siblingDoc[field.name] = {}
|
||||||
if (typeof siblingDocWithLocales[field.name] !== 'object')
|
if (typeof siblingDocWithLocales[field.name] !== 'object')
|
||||||
@@ -373,8 +400,9 @@ export const promise = async ({
|
|||||||
global,
|
global,
|
||||||
mergeLocaleActions,
|
mergeLocaleActions,
|
||||||
operation,
|
operation,
|
||||||
path: tabPath,
|
path: fieldPath,
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
siblingData: tabSiblingData,
|
siblingData: tabSiblingData,
|
||||||
siblingDoc: tabSiblingDoc,
|
siblingDoc: tabSiblingDoc,
|
||||||
siblingDocWithLocales: tabSiblingDocWithLocales,
|
siblingDocWithLocales: tabSiblingDocWithLocales,
|
||||||
@@ -398,8 +426,9 @@ export const promise = async ({
|
|||||||
global,
|
global,
|
||||||
mergeLocaleActions,
|
mergeLocaleActions,
|
||||||
operation,
|
operation,
|
||||||
path,
|
path: fieldPath,
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
siblingData,
|
siblingData,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
siblingDocWithLocales,
|
siblingDocWithLocales,
|
||||||
@@ -409,6 +438,52 @@ export const promise = async ({
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'richText': {
|
||||||
|
if (!field?.editor) {
|
||||||
|
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||||
|
}
|
||||||
|
if (typeof field?.editor === 'function') {
|
||||||
|
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor: RichTextAdapter = field?.editor
|
||||||
|
|
||||||
|
if (editor?.hooks?.beforeChange?.length) {
|
||||||
|
await editor.hooks.beforeChange.reduce(async (priorHook, currentHook) => {
|
||||||
|
await priorHook
|
||||||
|
|
||||||
|
const hookedValue = await currentHook({
|
||||||
|
collection,
|
||||||
|
context,
|
||||||
|
data,
|
||||||
|
docWithLocales,
|
||||||
|
duplicate,
|
||||||
|
errors,
|
||||||
|
field,
|
||||||
|
global,
|
||||||
|
mergeLocaleActions,
|
||||||
|
operation,
|
||||||
|
originalDoc: doc,
|
||||||
|
path: fieldPath,
|
||||||
|
previousSiblingDoc: siblingDoc,
|
||||||
|
previousValue: siblingDoc[field.name],
|
||||||
|
req,
|
||||||
|
schemaPath: parentSchemaPath,
|
||||||
|
siblingData,
|
||||||
|
siblingDocWithLocales,
|
||||||
|
skipValidation,
|
||||||
|
value: siblingData[field.name],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hookedValue !== undefined) {
|
||||||
|
siblingData[field.name] = hookedValue
|
||||||
|
}
|
||||||
|
}, Promise.resolve())
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ type Args = {
|
|||||||
id?: number | string
|
id?: number | string
|
||||||
mergeLocaleActions: (() => Promise<void>)[]
|
mergeLocaleActions: (() => Promise<void>)[]
|
||||||
operation: Operation
|
operation: Operation
|
||||||
path: string
|
path: (number | string)[]
|
||||||
req: PayloadRequestWithData
|
req: PayloadRequestWithData
|
||||||
|
schemaPath: string[]
|
||||||
siblingData: Record<string, unknown>
|
siblingData: Record<string, unknown>
|
||||||
/**
|
/**
|
||||||
* The original siblingData (not modified by any hooks)
|
* The original siblingData (not modified by any hooks)
|
||||||
@@ -44,7 +45,7 @@ type Args = {
|
|||||||
* - Execute field hooks
|
* - Execute field hooks
|
||||||
* - Validate data
|
* - Validate data
|
||||||
* - Transform data for storage
|
* - Transform data for storage
|
||||||
* - Unflatten locales
|
* - Unflatten locales. The input `data` is the normal document for one locale. The output result will become the document with locales.
|
||||||
*/
|
*/
|
||||||
export const traverseFields = async ({
|
export const traverseFields = async ({
|
||||||
id,
|
id,
|
||||||
@@ -61,6 +62,7 @@ export const traverseFields = async ({
|
|||||||
operation,
|
operation,
|
||||||
path,
|
path,
|
||||||
req,
|
req,
|
||||||
|
schemaPath,
|
||||||
siblingData,
|
siblingData,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
siblingDocWithLocales,
|
siblingDocWithLocales,
|
||||||
@@ -83,7 +85,8 @@ export const traverseFields = async ({
|
|||||||
global,
|
global,
|
||||||
mergeLocaleActions,
|
mergeLocaleActions,
|
||||||
operation,
|
operation,
|
||||||
path,
|
parentPath: path,
|
||||||
|
parentSchemaPath: schemaPath,
|
||||||
req,
|
req,
|
||||||
siblingData,
|
siblingData,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ export const beforeValidate = async <T extends Record<string, unknown>>({
|
|||||||
global,
|
global,
|
||||||
operation,
|
operation,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
path: [],
|
||||||
req,
|
req,
|
||||||
|
schemaPath: [],
|
||||||
siblingData: data,
|
siblingData: data,
|
||||||
siblingDoc: doc,
|
siblingDoc: doc,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
|
import type { RichTextAdapter } from '../../../admin/RichText.js'
|
||||||
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
||||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||||
import type { PayloadRequestWithData, RequestContext } from '../../../types/index.js'
|
import type { PayloadRequestWithData, RequestContext } from '../../../types/index.js'
|
||||||
import type { Field, TabAsField } from '../../config/types.js'
|
import type { Field, TabAsField } from '../../config/types.js'
|
||||||
|
|
||||||
|
import { MissingEditorProp } from '../../../errors/index.js'
|
||||||
import { fieldAffectsData, tabHasName, valueIsValueWithRelation } from '../../config/types.js'
|
import { fieldAffectsData, tabHasName, valueIsValueWithRelation } from '../../config/types.js'
|
||||||
import getValueWithDefault from '../../getDefaultValue.js'
|
import getValueWithDefault from '../../getDefaultValue.js'
|
||||||
|
import { getFieldPaths } from '../../getFieldPaths.js'
|
||||||
import { cloneDataFromOriginalDoc } from '../beforeChange/cloneDataFromOriginalDoc.js'
|
import { cloneDataFromOriginalDoc } from '../beforeChange/cloneDataFromOriginalDoc.js'
|
||||||
import { getExistingRowDoc } from '../beforeChange/getExistingRowDoc.js'
|
import { getExistingRowDoc } from '../beforeChange/getExistingRowDoc.js'
|
||||||
import { traverseFields } from './traverseFields.js'
|
import { traverseFields } from './traverseFields.js'
|
||||||
@@ -23,6 +26,8 @@ type Args<T> = {
|
|||||||
id?: number | string
|
id?: number | string
|
||||||
operation: 'create' | 'update'
|
operation: 'create' | 'update'
|
||||||
overrideAccess: boolean
|
overrideAccess: boolean
|
||||||
|
parentPath: (number | string)[]
|
||||||
|
parentSchemaPath: string[]
|
||||||
req: PayloadRequestWithData
|
req: PayloadRequestWithData
|
||||||
siblingData: Record<string, unknown>
|
siblingData: Record<string, unknown>
|
||||||
/**
|
/**
|
||||||
@@ -48,10 +53,18 @@ export const promise = async <T>({
|
|||||||
global,
|
global,
|
||||||
operation,
|
operation,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
parentPath,
|
||||||
|
parentSchemaPath,
|
||||||
req,
|
req,
|
||||||
siblingData,
|
siblingData,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
}: Args<T>): Promise<void> => {
|
}: Args<T>): Promise<void> => {
|
||||||
|
const { path: fieldPath, schemaPath: fieldSchemaPath } = getFieldPaths({
|
||||||
|
field,
|
||||||
|
parentPath,
|
||||||
|
parentSchemaPath,
|
||||||
|
})
|
||||||
|
|
||||||
if (fieldAffectsData(field)) {
|
if (fieldAffectsData(field)) {
|
||||||
if (field.name === 'id') {
|
if (field.name === 'id') {
|
||||||
if (field.type === 'number' && typeof siblingData[field.name] === 'string') {
|
if (field.type === 'number' && typeof siblingData[field.name] === 'string') {
|
||||||
@@ -229,8 +242,11 @@ export const promise = async <T>({
|
|||||||
operation,
|
operation,
|
||||||
originalDoc: doc,
|
originalDoc: doc,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
path: fieldPath,
|
||||||
previousSiblingDoc: siblingDoc,
|
previousSiblingDoc: siblingDoc,
|
||||||
|
previousValue: siblingData[field.name],
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
siblingData,
|
siblingData,
|
||||||
value: siblingData[field.name],
|
value: siblingData[field.name],
|
||||||
})
|
})
|
||||||
@@ -288,7 +304,9 @@ export const promise = async <T>({
|
|||||||
global,
|
global,
|
||||||
operation,
|
operation,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
path: fieldPath,
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
siblingData: groupData,
|
siblingData: groupData,
|
||||||
siblingDoc: groupDoc,
|
siblingDoc: groupDoc,
|
||||||
})
|
})
|
||||||
@@ -301,7 +319,7 @@ export const promise = async <T>({
|
|||||||
|
|
||||||
if (Array.isArray(rows)) {
|
if (Array.isArray(rows)) {
|
||||||
const promises = []
|
const promises = []
|
||||||
rows.forEach((row) => {
|
rows.forEach((row, i) => {
|
||||||
promises.push(
|
promises.push(
|
||||||
traverseFields({
|
traverseFields({
|
||||||
id,
|
id,
|
||||||
@@ -313,7 +331,9 @@ export const promise = async <T>({
|
|||||||
global,
|
global,
|
||||||
operation,
|
operation,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
path: [...fieldPath, i],
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
siblingData: row,
|
siblingData: row,
|
||||||
siblingDoc: getExistingRowDoc(row, siblingDoc[field.name]),
|
siblingDoc: getExistingRowDoc(row, siblingDoc[field.name]),
|
||||||
}),
|
}),
|
||||||
@@ -329,7 +349,7 @@ export const promise = async <T>({
|
|||||||
|
|
||||||
if (Array.isArray(rows)) {
|
if (Array.isArray(rows)) {
|
||||||
const promises = []
|
const promises = []
|
||||||
rows.forEach((row) => {
|
rows.forEach((row, i) => {
|
||||||
const rowSiblingDoc = getExistingRowDoc(row, siblingDoc[field.name])
|
const rowSiblingDoc = getExistingRowDoc(row, siblingDoc[field.name])
|
||||||
const blockTypeToMatch = row.blockType || rowSiblingDoc.blockType
|
const blockTypeToMatch = row.blockType || rowSiblingDoc.blockType
|
||||||
const block = field.blocks.find((blockType) => blockType.slug === blockTypeToMatch)
|
const block = field.blocks.find((blockType) => blockType.slug === blockTypeToMatch)
|
||||||
@@ -348,7 +368,9 @@ export const promise = async <T>({
|
|||||||
global,
|
global,
|
||||||
operation,
|
operation,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
path: [...fieldPath, i],
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
siblingData: row,
|
siblingData: row,
|
||||||
siblingDoc: rowSiblingDoc,
|
siblingDoc: rowSiblingDoc,
|
||||||
}),
|
}),
|
||||||
@@ -373,7 +395,9 @@ export const promise = async <T>({
|
|||||||
global,
|
global,
|
||||||
operation,
|
operation,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
path: fieldPath,
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
siblingData,
|
siblingData,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
})
|
})
|
||||||
@@ -405,7 +429,9 @@ export const promise = async <T>({
|
|||||||
global,
|
global,
|
||||||
operation,
|
operation,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
path: fieldPath,
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
siblingData: tabSiblingData,
|
siblingData: tabSiblingData,
|
||||||
siblingDoc: tabSiblingDoc,
|
siblingDoc: tabSiblingDoc,
|
||||||
})
|
})
|
||||||
@@ -424,7 +450,9 @@ export const promise = async <T>({
|
|||||||
global,
|
global,
|
||||||
operation,
|
operation,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
path: fieldPath,
|
||||||
req,
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
siblingData,
|
siblingData,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
})
|
})
|
||||||
@@ -432,6 +460,46 @@ export const promise = async <T>({
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'richText': {
|
||||||
|
if (!field?.editor) {
|
||||||
|
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||||
|
}
|
||||||
|
if (typeof field?.editor === 'function') {
|
||||||
|
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor: RichTextAdapter = field?.editor
|
||||||
|
|
||||||
|
if (editor?.hooks?.beforeValidate?.length) {
|
||||||
|
await editor.hooks.beforeValidate.reduce(async (priorHook, currentHook) => {
|
||||||
|
await priorHook
|
||||||
|
|
||||||
|
const hookedValue = await currentHook({
|
||||||
|
collection,
|
||||||
|
context,
|
||||||
|
data,
|
||||||
|
field,
|
||||||
|
global,
|
||||||
|
operation,
|
||||||
|
originalDoc: doc,
|
||||||
|
overrideAccess,
|
||||||
|
path: fieldPath,
|
||||||
|
previousSiblingDoc: siblingDoc,
|
||||||
|
previousValue: siblingData[field.name],
|
||||||
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
|
siblingData,
|
||||||
|
value: siblingData[field.name],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hookedValue !== undefined) {
|
||||||
|
siblingData[field.name] = hookedValue
|
||||||
|
}
|
||||||
|
}, Promise.resolve())
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ type Args<T> = {
|
|||||||
id?: number | string
|
id?: number | string
|
||||||
operation: 'create' | 'update'
|
operation: 'create' | 'update'
|
||||||
overrideAccess: boolean
|
overrideAccess: boolean
|
||||||
|
path: (number | string)[]
|
||||||
req: PayloadRequestWithData
|
req: PayloadRequestWithData
|
||||||
|
schemaPath: string[]
|
||||||
siblingData: Record<string, unknown>
|
siblingData: Record<string, unknown>
|
||||||
/**
|
/**
|
||||||
* The original siblingData (not modified by any hooks)
|
* The original siblingData (not modified by any hooks)
|
||||||
@@ -36,7 +38,9 @@ export const traverseFields = async <T>({
|
|||||||
global,
|
global,
|
||||||
operation,
|
operation,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
path,
|
||||||
req,
|
req,
|
||||||
|
schemaPath,
|
||||||
siblingData,
|
siblingData,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
}: Args<T>): Promise<void> => {
|
}: Args<T>): Promise<void> => {
|
||||||
@@ -53,6 +57,8 @@ export const traverseFields = async <T>({
|
|||||||
global,
|
global,
|
||||||
operation,
|
operation,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
parentPath: path,
|
||||||
|
parentSchemaPath: schemaPath,
|
||||||
req,
|
req,
|
||||||
siblingData,
|
siblingData,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ export const BlockContent: React.FC<Props> = (props) => {
|
|||||||
formData,
|
formData,
|
||||||
formSchema,
|
formSchema,
|
||||||
nodeKey,
|
nodeKey,
|
||||||
path,
|
|
||||||
reducedBlock: { labels },
|
reducedBlock: { labels },
|
||||||
schemaPath,
|
schemaPath,
|
||||||
} = props
|
} = props
|
||||||
@@ -111,17 +110,21 @@ export const BlockContent: React.FC<Props> = (props) => {
|
|||||||
// does not have, even if it's undefined.
|
// does not have, even if it's undefined.
|
||||||
// Currently, this happens if a block has another sub-blocks field. Inside formData, that sub-blocks field has an undefined blockName property.
|
// Currently, this happens if a block has another sub-blocks field. Inside formData, that sub-blocks field has an undefined blockName property.
|
||||||
// Inside of fields.data however, that sub-blocks blockName property does not exist at all.
|
// Inside of fields.data however, that sub-blocks blockName property does not exist at all.
|
||||||
function removeUndefinedAndNullRecursively(obj: object) {
|
function removeUndefinedAndNullAndEmptyArraysRecursively(obj: object) {
|
||||||
Object.keys(obj).forEach((key) => {
|
for (const key in obj) {
|
||||||
if (obj[key] && typeof obj[key] === 'object') {
|
const value = obj[key]
|
||||||
removeUndefinedAndNullRecursively(obj[key])
|
if (Array.isArray(value) && !value?.length) {
|
||||||
} else if (obj[key] === undefined || obj[key] === null) {
|
delete obj[key]
|
||||||
|
} else if (value && typeof value === 'object') {
|
||||||
|
removeUndefinedAndNullAndEmptyArraysRecursively(value)
|
||||||
|
} else if (value === undefined || value === null) {
|
||||||
delete obj[key]
|
delete obj[key]
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
removeUndefinedAndNullRecursively(newFormData)
|
removeUndefinedAndNullAndEmptyArraysRecursively(newFormData)
|
||||||
removeUndefinedAndNullRecursively(formData)
|
|
||||||
|
removeUndefinedAndNullAndEmptyArraysRecursively(formData)
|
||||||
|
|
||||||
// Only update if the data has actually changed. Otherwise, we may be triggering an unnecessary value change,
|
// Only update if the data has actually changed. Otherwise, we may be triggering an unnecessary value change,
|
||||||
// which would trigger the "Leave without saving" dialog unnecessarily
|
// which would trigger the "Leave without saving" dialog unnecessarily
|
||||||
|
|||||||
@@ -29,10 +29,8 @@ import type { BlocksFeatureClientProps } from '../feature.client.js'
|
|||||||
import { useEditorConfigContext } from '../../../lexical/config/client/EditorConfigProvider.js'
|
import { useEditorConfigContext } from '../../../lexical/config/client/EditorConfigProvider.js'
|
||||||
import { BlockContent } from './BlockContent.js'
|
import { BlockContent } from './BlockContent.js'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
import { removeEmptyArrayValues } from './removeEmptyArrayValues.js'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
blockFieldWrapperName: string
|
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
|
|
||||||
formData: BlockFields
|
formData: BlockFields
|
||||||
@@ -44,7 +42,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const BlockComponent: React.FC<Props> = (props) => {
|
export const BlockComponent: React.FC<Props> = (props) => {
|
||||||
const { blockFieldWrapperName, formData, nodeKey } = props
|
const { formData, nodeKey } = props
|
||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
const submitted = useFormSubmitted()
|
const submitted = useFormSubmitted()
|
||||||
const { id } = useDocumentInfo()
|
const { id } = useDocumentInfo()
|
||||||
@@ -81,7 +79,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
if (state) {
|
if (state) {
|
||||||
setInitialState({
|
setInitialState({
|
||||||
...removeEmptyArrayValues({ fields: state }),
|
...state,
|
||||||
blockName: {
|
blockName: {
|
||||||
initialValue: '',
|
initialValue: '',
|
||||||
passesCondition: true,
|
passesCondition: true,
|
||||||
@@ -175,6 +173,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
|||||||
</Collapsible>
|
</Collapsible>
|
||||||
)
|
)
|
||||||
}, [
|
}, [
|
||||||
|
classNames,
|
||||||
fieldMap,
|
fieldMap,
|
||||||
parentLexicalRichTextField,
|
parentLexicalRichTextField,
|
||||||
nodeKey,
|
nodeKey,
|
||||||
@@ -182,7 +181,6 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
|||||||
submitted,
|
submitted,
|
||||||
initialState,
|
initialState,
|
||||||
reducedBlock,
|
reducedBlock,
|
||||||
blockFieldWrapperName,
|
|
||||||
onChange,
|
onChange,
|
||||||
schemaFieldsPath,
|
schemaFieldsPath,
|
||||||
path,
|
path,
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import type { BlocksFeatureClientProps } from './feature.client.js'
|
|||||||
|
|
||||||
import { createNode } from '../typeUtilities.js'
|
import { createNode } from '../typeUtilities.js'
|
||||||
import { BlocksFeatureClientComponent } from './feature.client.js'
|
import { BlocksFeatureClientComponent } from './feature.client.js'
|
||||||
|
import { blockPopulationPromiseHOC } from './graphQLPopulationPromise.js'
|
||||||
import { i18n } from './i18n.js'
|
import { i18n } from './i18n.js'
|
||||||
import { BlockNode } from './nodes/BlocksNode.js'
|
import { BlockNode } from './nodes/BlocksNode.js'
|
||||||
import { blockPopulationPromiseHOC } from './populationPromise.js'
|
|
||||||
import { blockValidationHOC } from './validate.js'
|
import { blockValidationHOC } from './validate.js'
|
||||||
|
|
||||||
export type BlocksFeatureProps = {
|
export type BlocksFeatureProps = {
|
||||||
@@ -114,71 +114,17 @@ export const BlocksFeature: FeatureProviderProviderServer<
|
|||||||
i18n,
|
i18n,
|
||||||
nodes: [
|
nodes: [
|
||||||
createNode({
|
createNode({
|
||||||
/* // TODO: Implement these hooks once docWithLocales / originalSiblingDoc => node matching has been figured out
|
getSubFields: ({ node, req }) => {
|
||||||
hooks: {
|
const blockType = node.fields.blockType
|
||||||
beforeChange: [
|
|
||||||
async ({ context, findMany, node, operation, overrideAccess, req }) => {
|
|
||||||
const blockType = node.fields.blockType
|
|
||||||
|
|
||||||
const block = deepCopyObject(
|
const block = props.blocks.find((block) => block.slug === blockType)
|
||||||
props.blocks.find((block) => block.slug === blockType),
|
return block?.fields
|
||||||
)
|
},
|
||||||
|
getSubFieldsData: ({ node }) => {
|
||||||
|
return node?.fields
|
||||||
await beforeChangeTraverseFields({
|
},
|
||||||
id: null,
|
graphQLPopulationPromises: [blockPopulationPromiseHOC(props)],
|
||||||
collection: null,
|
|
||||||
context,
|
|
||||||
data: node.fields,
|
|
||||||
doc: node.fields,
|
|
||||||
fields: sanitizedBlock.fields,
|
|
||||||
global: null,
|
|
||||||
mergeLocaleActions: [],
|
|
||||||
operation:
|
|
||||||
operation === 'create' || operation === 'update' ? operation : 'update',
|
|
||||||
overrideAccess,
|
|
||||||
path: '',
|
|
||||||
req,
|
|
||||||
siblingData: node.fields,
|
|
||||||
siblingDoc: node.fields,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
return node
|
|
||||||
},
|
|
||||||
],
|
|
||||||
beforeValidate: [
|
|
||||||
async ({ context, findMany, node, operation, overrideAccess, req }) => {
|
|
||||||
const blockType = node.fields.blockType
|
|
||||||
|
|
||||||
const block = deepCopyObject(
|
|
||||||
props.blocks.find((block) => block.slug === blockType),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
await beforeValidateTraverseFields({
|
|
||||||
id: null,
|
|
||||||
collection: null,
|
|
||||||
context,
|
|
||||||
data: node.fields,
|
|
||||||
doc: node.fields,
|
|
||||||
fields: sanitizedBlock.fields,
|
|
||||||
global: null,
|
|
||||||
operation:
|
|
||||||
operation === 'create' || operation === 'update' ? operation : 'update',
|
|
||||||
overrideAccess,
|
|
||||||
req,
|
|
||||||
siblingData: node.fields,
|
|
||||||
siblingDoc: node.fields,
|
|
||||||
})
|
|
||||||
|
|
||||||
return node
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},*/
|
|
||||||
node: BlockNode,
|
node: BlockNode,
|
||||||
populationPromises: [blockPopulationPromiseHOC(props)],
|
|
||||||
validations: [blockValidationHOC(props)],
|
validations: [blockValidationHOC(props)],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { PopulationPromise } from '../types.js'
|
|||||||
import type { BlocksFeatureProps } from './feature.server.js'
|
import type { BlocksFeatureProps } from './feature.server.js'
|
||||||
import type { SerializedBlockNode } from './nodes/BlocksNode.js'
|
import type { SerializedBlockNode } from './nodes/BlocksNode.js'
|
||||||
|
|
||||||
import { recurseNestedFields } from '../../../populate/recurseNestedFields.js'
|
import { recursivelyPopulateFieldsForGraphQL } from '../../../populateGraphQL/recursivelyPopulateFieldsForGraphQL.js'
|
||||||
|
|
||||||
export const blockPopulationPromiseHOC = (
|
export const blockPopulationPromiseHOC = (
|
||||||
props: BlocksFeatureProps,
|
props: BlocksFeatureProps,
|
||||||
@@ -21,7 +21,6 @@ export const blockPopulationPromiseHOC = (
|
|||||||
populationPromises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc,
|
|
||||||
}) => {
|
}) => {
|
||||||
const blockFieldData = node.fields
|
const blockFieldData = node.fields
|
||||||
|
|
||||||
@@ -31,22 +30,21 @@ export const blockPopulationPromiseHOC = (
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
recurseNestedFields({
|
recursivelyPopulateFieldsForGraphQL({
|
||||||
context,
|
context,
|
||||||
currentDepth,
|
currentDepth,
|
||||||
data: blockFieldData,
|
data: blockFieldData,
|
||||||
depth,
|
depth,
|
||||||
|
draft,
|
||||||
editorPopulationPromises,
|
editorPopulationPromises,
|
||||||
fieldPromises,
|
fieldPromises,
|
||||||
fields: block.fields,
|
fields: block.fields,
|
||||||
findMany,
|
findMany,
|
||||||
flattenLocales: false, // Disable localization handling which does not work properly yet. Once we fully support hooks, this can be enabled (pass through flattenLocales again)
|
flattenLocales,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
populationPromises,
|
populationPromises,
|
||||||
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.
|
|
||||||
draft,
|
|
||||||
siblingDoc: blockFieldData,
|
siblingDoc: blockFieldData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { User } from 'payload/auth'
|
import type { User } from 'payload/auth'
|
||||||
import type { SanitizedConfig } from 'payload/config'
|
import type { SanitizedConfig } from 'payload/config'
|
||||||
import type { Field, RadioField, TextField } from 'payload/types'
|
import type { FieldAffectingData, RadioField, TextField } from 'payload/types'
|
||||||
|
|
||||||
import { validateUrl } from '../../../lexical/utils/url.js'
|
import { validateUrl } from '../../../lexical/utils/url.js'
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ export const getBaseFields = (
|
|||||||
enabledCollections: false | string[],
|
enabledCollections: false | string[],
|
||||||
disabledCollections: false | string[],
|
disabledCollections: false | string[],
|
||||||
maxDepth?: number,
|
maxDepth?: number,
|
||||||
): Field[] => {
|
): FieldAffectingData[] => {
|
||||||
let enabledRelations: string[]
|
let enabledRelations: string[]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,7 +33,7 @@ export const getBaseFields = (
|
|||||||
.map(({ slug }) => slug)
|
.map(({ slug }) => slug)
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseFields: Field[] = [
|
const baseFields: FieldAffectingData[] = [
|
||||||
{
|
{
|
||||||
name: 'text',
|
name: 'text',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Config, SanitizedConfig } from 'payload/config'
|
import type { Config, SanitizedConfig } from 'payload/config'
|
||||||
import type { Field } from 'payload/types'
|
import type { Field, FieldAffectingData } from 'payload/types'
|
||||||
|
|
||||||
import { traverseFields } from '@payloadcms/ui/utilities/buildFieldSchemaMap/traverseFields'
|
import { traverseFields } from '@payloadcms/ui/utilities/buildFieldSchemaMap/traverseFields'
|
||||||
import { sanitizeFields } from 'payload/config'
|
import { sanitizeFields } from 'payload/config'
|
||||||
@@ -11,12 +11,12 @@ import type { ClientProps } from './feature.client.js'
|
|||||||
import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js'
|
import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js'
|
||||||
import { createNode } from '../typeUtilities.js'
|
import { createNode } from '../typeUtilities.js'
|
||||||
import { LinkFeatureClientComponent } from './feature.client.js'
|
import { LinkFeatureClientComponent } from './feature.client.js'
|
||||||
|
import { linkPopulationPromiseHOC } from './graphQLPopulationPromise.js'
|
||||||
import { i18n } from './i18n.js'
|
import { i18n } from './i18n.js'
|
||||||
import { LinkMarkdownTransformer } from './markdownTransformer.js'
|
import { LinkMarkdownTransformer } from './markdownTransformer.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'
|
||||||
import { transformExtraFields } from './plugins/floatingLinkEditor/utilities.js'
|
import { transformExtraFields } from './plugins/floatingLinkEditor/utilities.js'
|
||||||
import { linkPopulationPromiseHOC } from './populationPromise.js'
|
|
||||||
import { linkValidation } from './validate.js'
|
import { linkValidation } from './validate.js'
|
||||||
|
|
||||||
export type ExclusiveLinkCollectionsProps =
|
export type ExclusiveLinkCollectionsProps =
|
||||||
@@ -46,7 +46,12 @@ export type LinkFeatureServerProps = ExclusiveLinkCollectionsProps & {
|
|||||||
* A function or array defining additional fields for the link feature. These will be
|
* A function or array defining additional fields for the link feature. These will be
|
||||||
* displayed in the link editor drawer.
|
* displayed in the link editor drawer.
|
||||||
*/
|
*/
|
||||||
fields?: ((args: { config: SanitizedConfig; defaultFields: Field[] }) => Field[]) | Field[]
|
fields?:
|
||||||
|
| ((args: {
|
||||||
|
config: SanitizedConfig
|
||||||
|
defaultFields: FieldAffectingData[]
|
||||||
|
}) => (Field | FieldAffectingData)[])
|
||||||
|
| Field[]
|
||||||
/**
|
/**
|
||||||
* Sets a maximum population depth for the internal doc default field of link, regardless of the remaining depth when the field is reached.
|
* Sets a maximum population depth for the internal doc default field of link, regardless of the remaining depth when the field is reached.
|
||||||
* This behaves exactly like the maxDepth properties of relationship and upload fields.
|
* This behaves exactly like the maxDepth properties of relationship and upload fields.
|
||||||
@@ -82,6 +87,13 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
|
|||||||
})
|
})
|
||||||
props.fields = sanitizedFields
|
props.fields = sanitizedFields
|
||||||
|
|
||||||
|
// the text field is not included in the node data.
|
||||||
|
// Thus, for tasks like validation, we do not want to pass it a text field in the schema which will never have data.
|
||||||
|
// Otherwise, it will cause a validation error (field is required).
|
||||||
|
const sanitizedFieldsWithoutText = deepCopyObject(sanitizedFields).filter(
|
||||||
|
(field) => field.name !== 'text',
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ClientComponent: LinkFeatureClientComponent,
|
ClientComponent: LinkFeatureClientComponent,
|
||||||
clientFeatureProps: {
|
clientFeatureProps: {
|
||||||
@@ -143,16 +155,9 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
|
|||||||
nodeTypes: [AutoLinkNode.getType()],
|
nodeTypes: [AutoLinkNode.getType()],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
hooks: {
|
|
||||||
afterRead: [
|
|
||||||
({ node }) => {
|
|
||||||
return node
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
node: AutoLinkNode,
|
node: AutoLinkNode,
|
||||||
populationPromises: [linkPopulationPromiseHOC(props)],
|
// Since AutoLinkNodes are just internal links, they need no hooks or graphQL population promises
|
||||||
validations: [linkValidation(props)],
|
validations: [linkValidation(props, sanitizedFieldsWithoutText)],
|
||||||
}),
|
}),
|
||||||
createNode({
|
createNode({
|
||||||
converters: {
|
converters: {
|
||||||
@@ -181,9 +186,15 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
|
|||||||
nodeTypes: [LinkNode.getType()],
|
nodeTypes: [LinkNode.getType()],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
getSubFields: ({ node, req }) => {
|
||||||
|
return sanitizedFieldsWithoutText
|
||||||
|
},
|
||||||
|
getSubFieldsData: ({ node }) => {
|
||||||
|
return node?.fields
|
||||||
|
},
|
||||||
|
graphQLPopulationPromises: [linkPopulationPromiseHOC(props)],
|
||||||
node: LinkNode,
|
node: LinkNode,
|
||||||
populationPromises: [linkPopulationPromiseHOC(props)],
|
validations: [linkValidation(props, sanitizedFieldsWithoutText)],
|
||||||
validations: [linkValidation(props)],
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
serverFeatureProps: props,
|
serverFeatureProps: props,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ 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 { recurseNestedFields } from '../../../populate/recurseNestedFields.js'
|
import { recursivelyPopulateFieldsForGraphQL } from '../../../populateGraphQL/recursivelyPopulateFieldsForGraphQL.js'
|
||||||
|
|
||||||
export const linkPopulationPromiseHOC = (
|
export const linkPopulationPromiseHOC = (
|
||||||
props: LinkFeatureServerProps,
|
props: LinkFeatureServerProps,
|
||||||
@@ -30,7 +30,7 @@ export const linkPopulationPromiseHOC = (
|
|||||||
* Should populate all fields, including the doc field (for internal links), as it's treated like a normal field
|
* Should populate all fields, including the doc field (for internal links), as it's treated like a normal field
|
||||||
*/
|
*/
|
||||||
if (Array.isArray(props.fields)) {
|
if (Array.isArray(props.fields)) {
|
||||||
recurseNestedFields({
|
recursivelyPopulateFieldsForGraphQL({
|
||||||
context,
|
context,
|
||||||
currentDepth,
|
currentDepth,
|
||||||
data: node.fields,
|
data: node.fields,
|
||||||
@@ -40,7 +40,7 @@ export const linkPopulationPromiseHOC = (
|
|||||||
fieldPromises,
|
fieldPromises,
|
||||||
fields: props.fields,
|
fields: props.fields,
|
||||||
findMany,
|
findMany,
|
||||||
flattenLocales: false, // Disable localization handling which does not work properly yet. Once we fully support hooks, this can be enabled (pass through flattenLocales again)
|
flattenLocales,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
populationPromises,
|
populationPromises,
|
||||||
req,
|
req,
|
||||||
@@ -11,7 +11,7 @@ import { LinkNode } from './LinkNode.js'
|
|||||||
|
|
||||||
export class AutoLinkNode extends LinkNode {
|
export class AutoLinkNode extends LinkNode {
|
||||||
static clone(node: AutoLinkNode): AutoLinkNode {
|
static clone(node: AutoLinkNode): AutoLinkNode {
|
||||||
return new AutoLinkNode({ fields: node.__fields, key: node.__key })
|
return new AutoLinkNode({ id: undefined, fields: node.__fields, key: node.__key })
|
||||||
}
|
}
|
||||||
|
|
||||||
static getType(): string {
|
static getType(): string {
|
||||||
@@ -61,7 +61,7 @@ export class AutoLinkNode extends LinkNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function $createAutoLinkNode({ fields }: { fields: LinkFields }): AutoLinkNode {
|
export function $createAutoLinkNode({ fields }: { fields: LinkFields }): AutoLinkNode {
|
||||||
return $applyNodeReplacement(new AutoLinkNode({ fields }))
|
return $applyNodeReplacement(new AutoLinkNode({ id: undefined, fields }))
|
||||||
}
|
}
|
||||||
export function $isAutoLinkNode(node: LexicalNode | null | undefined): node is AutoLinkNode {
|
export function $isAutoLinkNode(node: LexicalNode | null | undefined): node is AutoLinkNode {
|
||||||
return node instanceof AutoLinkNode
|
return node instanceof AutoLinkNode
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type {
|
|||||||
} from 'lexical'
|
} from 'lexical'
|
||||||
|
|
||||||
import { addClassNamesToElement, isHTMLAnchorElement } from '@lexical/utils'
|
import { addClassNamesToElement, isHTMLAnchorElement } from '@lexical/utils'
|
||||||
|
import ObjectID from 'bson-objectid'
|
||||||
import {
|
import {
|
||||||
$applyNodeReplacement,
|
$applyNodeReplacement,
|
||||||
$createTextNode,
|
$createTextNode,
|
||||||
@@ -29,8 +30,10 @@ const SUPPORTED_URL_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'sms:', '
|
|||||||
/** @noInheritDoc */
|
/** @noInheritDoc */
|
||||||
export class LinkNode extends ElementNode {
|
export class LinkNode extends ElementNode {
|
||||||
__fields: LinkFields
|
__fields: LinkFields
|
||||||
|
__id: string
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
|
id,
|
||||||
fields = {
|
fields = {
|
||||||
doc: null,
|
doc: null,
|
||||||
linkType: 'custom',
|
linkType: 'custom',
|
||||||
@@ -40,14 +43,17 @@ export class LinkNode extends ElementNode {
|
|||||||
key,
|
key,
|
||||||
}: {
|
}: {
|
||||||
fields: LinkFields
|
fields: LinkFields
|
||||||
|
id: string
|
||||||
key?: NodeKey
|
key?: NodeKey
|
||||||
}) {
|
}) {
|
||||||
super(key)
|
super(key)
|
||||||
this.__fields = fields
|
this.__fields = fields
|
||||||
|
this.__id = id
|
||||||
}
|
}
|
||||||
|
|
||||||
static clone(node: LinkNode): LinkNode {
|
static clone(node: LinkNode): LinkNode {
|
||||||
return new LinkNode({
|
return new LinkNode({
|
||||||
|
id: node.__id,
|
||||||
fields: node.__fields,
|
fields: node.__fields,
|
||||||
key: node.__key,
|
key: node.__key,
|
||||||
})
|
})
|
||||||
@@ -76,7 +82,13 @@ export class LinkNode extends ElementNode {
|
|||||||
serializedNode.version = 2
|
serializedNode.version = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (serializedNode.version === 2 && !serializedNode.id) {
|
||||||
|
serializedNode.id = new ObjectID.default().toHexString()
|
||||||
|
serializedNode.version = 3
|
||||||
|
}
|
||||||
|
|
||||||
const node = $createLinkNode({
|
const node = $createLinkNode({
|
||||||
|
id: serializedNode.id,
|
||||||
fields: serializedNode.fields,
|
fields: serializedNode.fields,
|
||||||
})
|
})
|
||||||
node.setFormat(serializedNode.format)
|
node.setFormat(serializedNode.format)
|
||||||
@@ -115,12 +127,17 @@ export class LinkNode extends ElementNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exportJSON(): SerializedLinkNode {
|
exportJSON(): SerializedLinkNode {
|
||||||
return {
|
const returnObject: SerializedLinkNode = {
|
||||||
...super.exportJSON(),
|
...super.exportJSON(),
|
||||||
type: this.getType(),
|
type: this.getType(),
|
||||||
fields: this.getFields(),
|
fields: this.getFields(),
|
||||||
version: 2,
|
version: 3,
|
||||||
}
|
}
|
||||||
|
const id = this.getID()
|
||||||
|
if (id) {
|
||||||
|
returnObject.id = id
|
||||||
|
}
|
||||||
|
return returnObject
|
||||||
}
|
}
|
||||||
|
|
||||||
extractWithChild(
|
extractWithChild(
|
||||||
@@ -146,6 +163,10 @@ export class LinkNode extends ElementNode {
|
|||||||
return this.getLatest().__fields
|
return this.getLatest().__fields
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getID(): string {
|
||||||
|
return this.getLatest().__id
|
||||||
|
}
|
||||||
|
|
||||||
insertNewAfter(selection: RangeSelection, restoreSelection = true): ElementNodeType | null {
|
insertNewAfter(selection: RangeSelection, restoreSelection = true): ElementNodeType | null {
|
||||||
const element = this.getParentOrThrow().insertNewAfter(selection, restoreSelection)
|
const element = this.getParentOrThrow().insertNewAfter(selection, restoreSelection)
|
||||||
if ($isElementNode(element)) {
|
if ($isElementNode(element)) {
|
||||||
@@ -216,6 +237,7 @@ function $convertAnchorElement(domNode: Node): DOMConversionOutput {
|
|||||||
const content = domNode.textContent
|
const content = domNode.textContent
|
||||||
if (content !== null && content !== '') {
|
if (content !== null && content !== '') {
|
||||||
node = $createLinkNode({
|
node = $createLinkNode({
|
||||||
|
id: new ObjectID.default().toHexString(),
|
||||||
fields: {
|
fields: {
|
||||||
doc: null,
|
doc: null,
|
||||||
linkType: 'custom',
|
linkType: 'custom',
|
||||||
@@ -228,8 +250,13 @@ function $convertAnchorElement(domNode: Node): DOMConversionOutput {
|
|||||||
return { node }
|
return { node }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $createLinkNode({ fields }: { fields: LinkFields }): LinkNode {
|
export function $createLinkNode({ id, fields }: { fields: LinkFields; id?: string }): LinkNode {
|
||||||
return $applyNodeReplacement(new LinkNode({ fields }))
|
return $applyNodeReplacement(
|
||||||
|
new LinkNode({
|
||||||
|
id: id ?? new ObjectID.default().toHexString(),
|
||||||
|
fields,
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $isLinkNode(node: LexicalNode | null | undefined): node is LinkNode {
|
export function $isLinkNode(node: LexicalNode | null | undefined): node is LinkNode {
|
||||||
@@ -349,8 +376,6 @@ export function $toggleLink(payload: LinkPayload): void {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/** @deprecated renamed to {@link $toggleLink} by @lexical/eslint-plugin rules-of-lexical */
|
|
||||||
export const toggleLink = $toggleLink
|
|
||||||
|
|
||||||
function $getLinkAncestor(node: LexicalNode): LinkNode | null {
|
function $getLinkAncestor(node: LexicalNode): LinkNode | null {
|
||||||
return $getAncestor(node, (ancestor) => $isLinkNode(ancestor)) as LinkNode
|
return $getAncestor(node, (ancestor) => $isLinkNode(ancestor)) as LinkNode
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ export type LinkFields = {
|
|||||||
export type SerializedLinkNode = Spread<
|
export type SerializedLinkNode = Spread<
|
||||||
{
|
{
|
||||||
fields: LinkFields
|
fields: LinkFields
|
||||||
|
id?: string // optional if AutoLinkNode
|
||||||
},
|
},
|
||||||
SerializedElementNode
|
SerializedElementNode
|
||||||
>
|
>
|
||||||
export type SerializedAutoLinkNode = SerializedLinkNode
|
export type SerializedAutoLinkNode = Omit<SerializedLinkNode, 'id'>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
|
|||||||
|
|
||||||
const { i18n, t } = useTranslation()
|
const { i18n, t } = useTranslation()
|
||||||
|
|
||||||
const [stateData, setStateData] = useState<{} | (LinkFields & { text: string })>({})
|
const [stateData, setStateData] = useState<{} | (LinkFields & { id?: string; text: string })>({})
|
||||||
|
|
||||||
const { closeModal, isModalOpen, toggleModal } = useModal()
|
const { closeModal, isModalOpen, toggleModal } = useModal()
|
||||||
const editDepth = useEditDepth()
|
const editDepth = useEditDepth()
|
||||||
@@ -114,6 +114,7 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
|
|||||||
newTab: undefined,
|
newTab: undefined,
|
||||||
url: '',
|
url: '',
|
||||||
...focusLinkParent.getFields(),
|
...focusLinkParent.getFields(),
|
||||||
|
id: focusLinkParent.getID(),
|
||||||
text: focusLinkParent.getTextContent(),
|
text: focusLinkParent.getTextContent(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { SanitizedConfig } from 'payload/config'
|
import type { SanitizedConfig } from 'payload/config'
|
||||||
import type { Field } from 'payload/types'
|
import type { Field, FieldAffectingData } from 'payload/types'
|
||||||
|
|
||||||
import { getBaseFields } from '../../drawer/baseFields.js'
|
import { getBaseFields } from '../../drawer/baseFields.js'
|
||||||
|
|
||||||
@@ -8,14 +8,14 @@ import { getBaseFields } from '../../drawer/baseFields.js'
|
|||||||
*/
|
*/
|
||||||
export function transformExtraFields(
|
export function transformExtraFields(
|
||||||
customFieldSchema:
|
customFieldSchema:
|
||||||
| ((args: { config: SanitizedConfig; defaultFields: Field[] }) => Field[])
|
| ((args: { config: SanitizedConfig; defaultFields: FieldAffectingData[] }) => Field[])
|
||||||
| Field[],
|
| Field[],
|
||||||
config: SanitizedConfig,
|
config: SanitizedConfig,
|
||||||
enabledCollections?: false | string[],
|
enabledCollections?: false | string[],
|
||||||
disabledCollections?: false | string[],
|
disabledCollections?: false | string[],
|
||||||
maxDepth?: number,
|
maxDepth?: number,
|
||||||
): Field[] {
|
): Field[] {
|
||||||
const baseFields: Field[] = getBaseFields(
|
const baseFields: FieldAffectingData[] = getBaseFields(
|
||||||
config,
|
config,
|
||||||
enabledCollections,
|
enabledCollections,
|
||||||
disabledCollections,
|
disabledCollections,
|
||||||
@@ -29,7 +29,7 @@ export function transformExtraFields(
|
|||||||
} else if (Array.isArray(customFieldSchema)) {
|
} else if (Array.isArray(customFieldSchema)) {
|
||||||
fields = customFieldSchema
|
fields = customFieldSchema
|
||||||
} else {
|
} else {
|
||||||
fields = baseFields
|
fields = baseFields as Field[]
|
||||||
}
|
}
|
||||||
|
|
||||||
return fields
|
return fields
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import type { LinkFields } from '../../nodes/types.js'
|
|||||||
import type { LinkPayload } from '../floatingLinkEditor/types.js'
|
import type { LinkPayload } from '../floatingLinkEditor/types.js'
|
||||||
|
|
||||||
import { validateUrl } from '../../../../lexical/utils/url.js'
|
import { validateUrl } from '../../../../lexical/utils/url.js'
|
||||||
import { LinkNode, TOGGLE_LINK_COMMAND, toggleLink } from '../../nodes/LinkNode.js'
|
import { $toggleLink, LinkNode, TOGGLE_LINK_COMMAND } from '../../nodes/LinkNode.js'
|
||||||
|
|
||||||
export const LinkPlugin: PluginComponent<ClientProps> = () => {
|
export const LinkPlugin: PluginComponent<ClientProps> = () => {
|
||||||
const [editor] = useLexicalComposerContext()
|
const [editor] = useLexicalComposerContext()
|
||||||
@@ -29,7 +29,7 @@ export const LinkPlugin: PluginComponent<ClientProps> = () => {
|
|||||||
editor.registerCommand(
|
editor.registerCommand(
|
||||||
TOGGLE_LINK_COMMAND,
|
TOGGLE_LINK_COMMAND,
|
||||||
(payload: LinkPayload) => {
|
(payload: LinkPayload) => {
|
||||||
toggleLink(payload)
|
$toggleLink(payload)
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
COMMAND_PRIORITY_LOW,
|
COMMAND_PRIORITY_LOW,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type { SerializedAutoLinkNode, SerializedLinkNode } from './nodes/types.j
|
|||||||
|
|
||||||
export const linkValidation = (
|
export const linkValidation = (
|
||||||
props: LinkFeatureServerProps,
|
props: LinkFeatureServerProps,
|
||||||
|
sanitizedFieldsWithoutText: Field[],
|
||||||
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
|
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
|
||||||
): NodeValidation<SerializedAutoLinkNode | SerializedLinkNode> => {
|
): NodeValidation<SerializedAutoLinkNode | SerializedLinkNode> => {
|
||||||
return async ({
|
return async ({
|
||||||
@@ -20,19 +21,14 @@ export const linkValidation = (
|
|||||||
* Run buildStateFromSchema as that properly validates link fields and link sub-fields
|
* Run buildStateFromSchema as that properly validates link fields and link sub-fields
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const data = {
|
|
||||||
...node.fields,
|
|
||||||
text: 'ignored',
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await buildStateFromSchema({
|
const result = await buildStateFromSchema({
|
||||||
id,
|
id,
|
||||||
data,
|
data: node.fields,
|
||||||
fieldSchema: props.fields as Field[], // Sanitized in feature.server.ts
|
fieldSchema: sanitizedFieldsWithoutText, // Sanitized in feature.server.ts
|
||||||
operation: operation === 'create' || operation === 'update' ? operation : 'update',
|
operation: operation === 'create' || operation === 'update' ? operation : 'update',
|
||||||
preferences,
|
preferences,
|
||||||
req,
|
req,
|
||||||
siblingData: data,
|
siblingData: node.fields,
|
||||||
})
|
})
|
||||||
|
|
||||||
let errorPaths = []
|
let errorPaths = []
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { FeatureProviderProviderServer } from '../types.js'
|
import type { FeatureProviderProviderServer } from '../types.js'
|
||||||
|
|
||||||
|
import { populate } from '../../../populateGraphQL/populate.js'
|
||||||
import { createNode } from '../typeUtilities.js'
|
import { createNode } from '../typeUtilities.js'
|
||||||
import { RelationshipFeatureClientComponent } from './feature.client.js'
|
import { RelationshipFeatureClientComponent } from './feature.client.js'
|
||||||
|
import { relationshipPopulationPromiseHOC } from './graphQLPopulationPromise.js'
|
||||||
import { i18n } from './i18n.js'
|
import { i18n } from './i18n.js'
|
||||||
import { RelationshipNode } from './nodes/RelationshipNode.js'
|
import { RelationshipNode } from './nodes/RelationshipNode.js'
|
||||||
import { relationshipPopulationPromiseHOC } from './populationPromise.js'
|
|
||||||
|
|
||||||
export type ExclusiveRelationshipFeatureProps =
|
export type ExclusiveRelationshipFeatureProps =
|
||||||
| {
|
| {
|
||||||
@@ -50,8 +51,55 @@ export const RelationshipFeature: FeatureProviderProviderServer<
|
|||||||
i18n,
|
i18n,
|
||||||
nodes: [
|
nodes: [
|
||||||
createNode({
|
createNode({
|
||||||
|
graphQLPopulationPromises: [relationshipPopulationPromiseHOC(props)],
|
||||||
|
hooks: {
|
||||||
|
afterRead: [
|
||||||
|
({
|
||||||
|
currentDepth,
|
||||||
|
depth,
|
||||||
|
draft,
|
||||||
|
node,
|
||||||
|
overrideAccess,
|
||||||
|
populationPromises,
|
||||||
|
req,
|
||||||
|
showHiddenFields,
|
||||||
|
}) => {
|
||||||
|
if (!node?.value) {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
const collection = req.payload.collections[node?.relationTo]
|
||||||
|
|
||||||
|
if (!collection) {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
// @ts-expect-error
|
||||||
|
const id = node?.value?.id || node?.value // for backwards-compatibility
|
||||||
|
|
||||||
|
const populateDepth =
|
||||||
|
props?.maxDepth !== undefined && props?.maxDepth < depth
|
||||||
|
? props?.maxDepth
|
||||||
|
: depth
|
||||||
|
|
||||||
|
populationPromises.push(
|
||||||
|
populate({
|
||||||
|
id,
|
||||||
|
collection,
|
||||||
|
currentDepth,
|
||||||
|
data: node,
|
||||||
|
depth: populateDepth,
|
||||||
|
draft,
|
||||||
|
key: 'value',
|
||||||
|
overrideAccess,
|
||||||
|
req,
|
||||||
|
showHiddenFields,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return node
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
node: RelationshipNode,
|
node: RelationshipNode,
|
||||||
populationPromises: [relationshipPopulationPromiseHOC(props)],
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
serverFeatureProps: props,
|
serverFeatureProps: props,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { PopulationPromise } from '../types.js'
|
|||||||
import type { RelationshipFeatureProps } from './feature.server.js'
|
import type { RelationshipFeatureProps } from './feature.server.js'
|
||||||
import type { SerializedRelationshipNode } from './nodes/RelationshipNode.js'
|
import type { SerializedRelationshipNode } from './nodes/RelationshipNode.js'
|
||||||
|
|
||||||
import { populate } from '../../../populate/populate.js'
|
import { populate } from '../../../populateGraphQL/populate.js'
|
||||||
|
|
||||||
export const relationshipPopulationPromiseHOC = (
|
export const relationshipPopulationPromiseHOC = (
|
||||||
props: RelationshipFeatureProps,
|
props: RelationshipFeatureProps,
|
||||||
@@ -11,7 +11,6 @@ export const relationshipPopulationPromiseHOC = (
|
|||||||
currentDepth,
|
currentDepth,
|
||||||
depth,
|
depth,
|
||||||
draft,
|
draft,
|
||||||
field,
|
|
||||||
node,
|
node,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
populationPromises,
|
populationPromises,
|
||||||
@@ -36,7 +35,6 @@ export const relationshipPopulationPromiseHOC = (
|
|||||||
data: node,
|
data: node,
|
||||||
depth: populateDepth,
|
depth: populateDepth,
|
||||||
draft,
|
draft,
|
||||||
field,
|
|
||||||
key: 'value',
|
key: 'value',
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
req,
|
req,
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Transformer } from '@lexical/markdown'
|
import type { Transformer } from '@lexical/markdown'
|
||||||
import type { GenericLanguages, I18n, I18nClient } from '@payloadcms/translations'
|
import type { GenericLanguages, I18nClient } from '@payloadcms/translations'
|
||||||
import type { JSONSchema4 } from 'json-schema'
|
import type { JSONSchema4 } from 'json-schema'
|
||||||
import type { Klass, LexicalEditor, LexicalNode, SerializedEditorState } from 'lexical'
|
import type { Klass, LexicalEditor, LexicalNode, SerializedEditorState } from 'lexical'
|
||||||
import type { SerializedLexicalNode } from 'lexical'
|
import type { SerializedLexicalNode } from 'lexical'
|
||||||
@@ -25,6 +25,7 @@ export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexica
|
|||||||
context,
|
context,
|
||||||
currentDepth,
|
currentDepth,
|
||||||
depth,
|
depth,
|
||||||
|
draft,
|
||||||
editorPopulationPromises,
|
editorPopulationPromises,
|
||||||
field,
|
field,
|
||||||
fieldPromises,
|
fieldPromises,
|
||||||
@@ -236,21 +237,103 @@ export type ClientComponentProps<ClientFeatureProps> = ClientFeatureProps extend
|
|||||||
order: number
|
order: number
|
||||||
} & ClientFeatureProps
|
} & ClientFeatureProps
|
||||||
|
|
||||||
export type FieldNodeHookArgs<T extends SerializedLexicalNode> = {
|
export type AfterReadNodeHookArgs<T extends SerializedLexicalNode> = {
|
||||||
context: RequestContext
|
/**
|
||||||
|
* Only available in `afterRead` hooks.
|
||||||
|
*/
|
||||||
|
currentDepth: number
|
||||||
|
/**
|
||||||
|
* Only available in `afterRead` hooks.
|
||||||
|
*/
|
||||||
|
depth: number
|
||||||
|
draft: boolean
|
||||||
|
fallbackLocale: string
|
||||||
|
/**
|
||||||
|
* Only available in `afterRead` field hooks.
|
||||||
|
*/
|
||||||
|
fieldPromises: Promise<void>[]
|
||||||
/** Boolean to denote if this hook is running against finding one, or finding many within the afterRead hook. */
|
/** Boolean to denote if this hook is running against finding one, or finding many within the afterRead hook. */
|
||||||
findMany?: boolean
|
findMany: boolean
|
||||||
/** The value of the field. */
|
flattenLocales: boolean
|
||||||
node?: T
|
/**
|
||||||
|
* The requested locale.
|
||||||
|
*/
|
||||||
|
locale: string
|
||||||
|
overrideAccess: boolean
|
||||||
|
/**
|
||||||
|
* Only available in `afterRead` field hooks.
|
||||||
|
*/
|
||||||
|
populationPromises: Promise<void>[]
|
||||||
|
/**
|
||||||
|
* Only available in `afterRead` hooks.
|
||||||
|
*/
|
||||||
|
showHiddenFields: boolean
|
||||||
|
/**
|
||||||
|
* Only available in `afterRead` hooks.
|
||||||
|
*/
|
||||||
|
triggerAccessControl: boolean
|
||||||
|
/**
|
||||||
|
* Only available in `afterRead` hooks.
|
||||||
|
*/
|
||||||
|
triggerHooks: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AfterChangeNodeHookArgs<T extends SerializedLexicalNode> = {
|
||||||
/** 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. */
|
/** 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'
|
operation: 'create' | 'delete' | 'read' | 'update'
|
||||||
overrideAccess?: boolean
|
/** The value of the node before any changes. Not available in afterRead hooks */
|
||||||
/** The Express request object. It is mocked for Local API operations. */
|
originalNode: T
|
||||||
|
}
|
||||||
|
export type BeforeValidateNodeHookArgs<T extends SerializedLexicalNode> = {
|
||||||
|
/** 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 value of the node before any changes. Not available in afterRead hooks */
|
||||||
|
originalNode: T
|
||||||
|
overrideAccess: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BeforeChangeNodeHookArgs<T extends SerializedLexicalNode> = {
|
||||||
|
duplicate: boolean
|
||||||
|
/**
|
||||||
|
* Only available in `beforeChange` hooks.
|
||||||
|
*/
|
||||||
|
errors: { field: string; message: string }[]
|
||||||
|
mergeLocaleActions: (() => Promise<void>)[]
|
||||||
|
/** 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 value of the node before any changes. Not available in afterRead hooks */
|
||||||
|
originalNode: T
|
||||||
|
/**
|
||||||
|
* The original node with locales (not modified by any hooks).
|
||||||
|
*/
|
||||||
|
originalNodeWithLocales?: T
|
||||||
|
skipValidation: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BaseNodeHookArgs<T extends SerializedLexicalNode> = {
|
||||||
|
context: RequestContext
|
||||||
|
/** The value of the node. */
|
||||||
|
node: T
|
||||||
|
parentRichTextFieldPath: (number | string)[]
|
||||||
|
parentRichTextFieldSchemaPath: string[]
|
||||||
|
/** The payload request object. It is mocked for Local API operations. */
|
||||||
req: PayloadRequestWithData
|
req: PayloadRequestWithData
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FieldNodeHook<T extends SerializedLexicalNode> = (
|
export type AfterReadNodeHook<T extends SerializedLexicalNode> = (
|
||||||
args: FieldNodeHookArgs<T>,
|
args: AfterReadNodeHookArgs<T> & BaseNodeHookArgs<T>,
|
||||||
|
) => Promise<T> | T
|
||||||
|
|
||||||
|
export type AfterChangeNodeHook<T extends SerializedLexicalNode> = (
|
||||||
|
args: AfterChangeNodeHookArgs<T> & BaseNodeHookArgs<T>,
|
||||||
|
) => Promise<T> | T
|
||||||
|
|
||||||
|
export type BeforeChangeNodeHook<T extends SerializedLexicalNode> = (
|
||||||
|
args: BeforeChangeNodeHookArgs<T> & BaseNodeHookArgs<T>,
|
||||||
|
) => Promise<T> | T
|
||||||
|
|
||||||
|
export type BeforeValidateNodeHook<T extends SerializedLexicalNode> = (
|
||||||
|
args: BeforeValidateNodeHookArgs<T> & BaseNodeHookArgs<T>,
|
||||||
) => Promise<T> | T
|
) => Promise<T> | T
|
||||||
|
|
||||||
// Define the node with hooks that use the node's exportJSON return type
|
// Define the node with hooks that use the node's exportJSON return type
|
||||||
@@ -258,20 +341,30 @@ export type NodeWithHooks<T extends LexicalNode = any> = {
|
|||||||
converters?: {
|
converters?: {
|
||||||
html?: HTMLConverter<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>
|
html?: HTMLConverter<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>
|
||||||
}
|
}
|
||||||
hooks?: {
|
/**
|
||||||
afterChange?: Array<FieldNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
|
* If a node includes sub-fields (e.g. block and link nodes), passing those subFields here will make payload
|
||||||
afterRead?: Array<FieldNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
|
* automatically populate & run hooks for them
|
||||||
beforeChange?: Array<FieldNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
|
*/
|
||||||
/**
|
getSubFields?: (args: {
|
||||||
* Runs before a document is duplicated to prevent errors in unique fields or return null to use defaultValue.
|
node: ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>
|
||||||
*/
|
req: PayloadRequestWithData
|
||||||
beforeDuplicate?: Array<FieldNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
|
}) => Field[] | null
|
||||||
beforeValidate?: Array<FieldNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
|
getSubFieldsData?: (args: {
|
||||||
}
|
node: ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>
|
||||||
node: Klass<T> | LexicalNodeReplacement
|
req: PayloadRequestWithData
|
||||||
populationPromises?: Array<
|
}) => Record<string, unknown>
|
||||||
|
graphQLPopulationPromises?: Array<
|
||||||
PopulationPromise<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>
|
PopulationPromise<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>
|
||||||
>
|
>
|
||||||
|
hooks?: {
|
||||||
|
afterChange?: Array<AfterChangeNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
|
||||||
|
afterRead?: Array<AfterReadNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
|
||||||
|
beforeChange?: Array<BeforeChangeNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
|
||||||
|
beforeValidate?: Array<
|
||||||
|
BeforeValidateNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>
|
||||||
|
>
|
||||||
|
}
|
||||||
|
node: Klass<T> | LexicalNodeReplacement
|
||||||
validations?: Array<NodeValidation<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
|
validations?: Array<NodeValidation<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,17 +544,21 @@ export type SanitizedServerFeatures = Required<
|
|||||||
}
|
}
|
||||||
/** The node types mapped to their hooks */
|
/** The node types mapped to their hooks */
|
||||||
|
|
||||||
|
getSubFields?: Map<
|
||||||
|
string,
|
||||||
|
(args: { node: SerializedLexicalNode; req: PayloadRequestWithData }) => Field[] | null
|
||||||
|
>
|
||||||
|
getSubFieldsData?: Map<
|
||||||
|
string,
|
||||||
|
(args: { node: SerializedLexicalNode; req: PayloadRequestWithData }) => Record<string, unknown>
|
||||||
|
>
|
||||||
|
graphQLPopulationPromises: Map<string, Array<PopulationPromise>>
|
||||||
hooks?: {
|
hooks?: {
|
||||||
afterChange?: Map<string, Array<FieldNodeHook<SerializedLexicalNode>>>
|
afterChange?: Map<string, Array<AfterChangeNodeHook<SerializedLexicalNode>>>
|
||||||
afterRead?: Map<string, Array<FieldNodeHook<SerializedLexicalNode>>>
|
afterRead?: Map<string, Array<AfterReadNodeHook<SerializedLexicalNode>>>
|
||||||
beforeChange?: Map<string, Array<FieldNodeHook<SerializedLexicalNode>>>
|
beforeChange?: Map<string, Array<BeforeChangeNodeHook<SerializedLexicalNode>>>
|
||||||
/**
|
beforeValidate?: Map<string, Array<BeforeValidateNodeHook<SerializedLexicalNode>>>
|
||||||
* Runs before a document is duplicated to prevent errors in unique fields or return null to use defaultValue.
|
|
||||||
*/
|
|
||||||
beforeDuplicate?: Map<string, Array<FieldNodeHook<SerializedLexicalNode>>>
|
|
||||||
beforeValidate?: Map<string, Array<FieldNodeHook<SerializedLexicalNode>>>
|
|
||||||
} /** The node types mapped to their populationPromises */
|
} /** The node types mapped to their populationPromises */
|
||||||
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>>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ import { $createUploadNode } from '../nodes/UploadNode.js'
|
|||||||
import { INSERT_UPLOAD_COMMAND } from '../plugin/index.js'
|
import { INSERT_UPLOAD_COMMAND } from '../plugin/index.js'
|
||||||
import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from './commands.js'
|
import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from './commands.js'
|
||||||
|
|
||||||
const baseClass = 'lexical-upload-drawer'
|
|
||||||
|
|
||||||
const insertUpload = ({
|
const insertUpload = ({
|
||||||
editor,
|
editor,
|
||||||
relationTo,
|
relationTo,
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ import { sanitizeFields } from 'payload/config'
|
|||||||
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 { populate } from '../../../populateGraphQL/populate.js'
|
||||||
import { createNode } from '../typeUtilities.js'
|
import { createNode } from '../typeUtilities.js'
|
||||||
import { UploadFeatureClientComponent } from './feature.client.js'
|
import { UploadFeatureClientComponent } from './feature.client.js'
|
||||||
|
import { uploadPopulationPromiseHOC } from './graphQLPopulationPromise.js'
|
||||||
import { i18n } from './i18n.js'
|
import { i18n } from './i18n.js'
|
||||||
import { UploadNode } from './nodes/UploadNode.js'
|
import { UploadNode } from './nodes/UploadNode.js'
|
||||||
import { uploadPopulationPromiseHOC } from './populationPromise.js'
|
|
||||||
import { uploadValidation } from './validate.js'
|
import { uploadValidation } from './validate.js'
|
||||||
|
|
||||||
export type UploadFeatureProps = {
|
export type UploadFeatureProps = {
|
||||||
@@ -177,8 +178,73 @@ export const UploadFeature: FeatureProviderProviderServer<
|
|||||||
nodeTypes: [UploadNode.getType()],
|
nodeTypes: [UploadNode.getType()],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
getSubFields: ({ node, req }) => {
|
||||||
|
const collection = req.payload.collections[node?.relationTo]
|
||||||
|
|
||||||
|
if (collection) {
|
||||||
|
const collectionFieldSchema = props?.collections?.[node?.relationTo]?.fields
|
||||||
|
|
||||||
|
if (Array.isArray(collectionFieldSchema)) {
|
||||||
|
if (!collectionFieldSchema?.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return collectionFieldSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
getSubFieldsData: ({ node }) => {
|
||||||
|
return node?.fields
|
||||||
|
},
|
||||||
|
graphQLPopulationPromises: [uploadPopulationPromiseHOC(props)],
|
||||||
|
hooks: {
|
||||||
|
afterRead: [
|
||||||
|
({
|
||||||
|
currentDepth,
|
||||||
|
depth,
|
||||||
|
draft,
|
||||||
|
node,
|
||||||
|
overrideAccess,
|
||||||
|
populationPromises,
|
||||||
|
req,
|
||||||
|
showHiddenFields,
|
||||||
|
}) => {
|
||||||
|
if (!node?.value) {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
const collection = req.payload.collections[node?.relationTo]
|
||||||
|
|
||||||
|
if (!collection) {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
// @ts-expect-error
|
||||||
|
const id = node?.value?.id || node?.value // for backwards-compatibility
|
||||||
|
|
||||||
|
const populateDepth =
|
||||||
|
props?.maxDepth !== undefined && props?.maxDepth < depth
|
||||||
|
? props?.maxDepth
|
||||||
|
: depth
|
||||||
|
|
||||||
|
populationPromises.push(
|
||||||
|
populate({
|
||||||
|
id,
|
||||||
|
collection,
|
||||||
|
currentDepth,
|
||||||
|
data: node,
|
||||||
|
depth: populateDepth,
|
||||||
|
draft,
|
||||||
|
key: 'value',
|
||||||
|
overrideAccess,
|
||||||
|
req,
|
||||||
|
showHiddenFields,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return node
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
node: UploadNode,
|
node: UploadNode,
|
||||||
populationPromises: [uploadPopulationPromiseHOC(props)],
|
|
||||||
validations: [uploadValidation(props)],
|
validations: [uploadValidation(props)],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ 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'
|
||||||
|
|
||||||
import { populate } from '../../../populate/populate.js'
|
import { populate } from '../../../populateGraphQL/populate.js'
|
||||||
import { recurseNestedFields } from '../../../populate/recurseNestedFields.js'
|
import { recursivelyPopulateFieldsForGraphQL } from '../../../populateGraphQL/recursivelyPopulateFieldsForGraphQL.js'
|
||||||
|
|
||||||
export const uploadPopulationPromiseHOC = (
|
export const uploadPopulationPromiseHOC = (
|
||||||
props?: UploadFeatureProps,
|
props?: UploadFeatureProps,
|
||||||
@@ -14,7 +14,6 @@ export const uploadPopulationPromiseHOC = (
|
|||||||
depth,
|
depth,
|
||||||
draft,
|
draft,
|
||||||
editorPopulationPromises,
|
editorPopulationPromises,
|
||||||
field,
|
|
||||||
fieldPromises,
|
fieldPromises,
|
||||||
findMany,
|
findMany,
|
||||||
flattenLocales,
|
flattenLocales,
|
||||||
@@ -42,35 +41,37 @@ export const uploadPopulationPromiseHOC = (
|
|||||||
data: node,
|
data: node,
|
||||||
depth: populateDepth,
|
depth: populateDepth,
|
||||||
draft,
|
draft,
|
||||||
field,
|
|
||||||
key: 'value',
|
key: 'value',
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
if (Array.isArray(props?.collections?.[node?.relationTo]?.fields)) {
|
const collectionFieldSchema = props?.collections?.[node?.relationTo]?.fields
|
||||||
if (!props?.collections?.[node?.relationTo]?.fields?.length) {
|
|
||||||
return
|
if (Array.isArray(collectionFieldSchema)) {
|
||||||
|
if (!collectionFieldSchema?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
recursivelyPopulateFieldsForGraphQL({
|
||||||
|
context,
|
||||||
|
currentDepth,
|
||||||
|
data: node.fields || {},
|
||||||
|
depth,
|
||||||
|
draft,
|
||||||
|
editorPopulationPromises,
|
||||||
|
fieldPromises,
|
||||||
|
fields: collectionFieldSchema,
|
||||||
|
findMany,
|
||||||
|
flattenLocales,
|
||||||
|
overrideAccess,
|
||||||
|
populationPromises,
|
||||||
|
req,
|
||||||
|
showHiddenFields,
|
||||||
|
siblingDoc: node.fields || {},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
recurseNestedFields({
|
|
||||||
context,
|
|
||||||
currentDepth,
|
|
||||||
data: node.fields || {},
|
|
||||||
depth,
|
|
||||||
draft,
|
|
||||||
editorPopulationPromises,
|
|
||||||
fieldPromises,
|
|
||||||
fields: props?.collections?.[node?.relationTo]?.fields,
|
|
||||||
findMany,
|
|
||||||
flattenLocales: false, // Disable localization handling which does not work properly yet. Once we fully support hooks, this can be enabled (pass through flattenLocales again)
|
|
||||||
overrideAccess,
|
|
||||||
populationPromises,
|
|
||||||
req,
|
|
||||||
showHiddenFields,
|
|
||||||
siblingDoc: node.fields || {},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ import type {
|
|||||||
import type { JSX } from 'react'
|
import type { JSX } from 'react'
|
||||||
|
|
||||||
import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode.js'
|
import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode.js'
|
||||||
|
import ObjectID from 'bson-objectid'
|
||||||
import { $applyNodeReplacement } from 'lexical'
|
import { $applyNodeReplacement } from 'lexical'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ export type UploadData = {
|
|||||||
// unknown, custom fields:
|
// unknown, custom fields:
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
id: string
|
||||||
relationTo: string
|
relationTo: string
|
||||||
value: number | string
|
value: number | string
|
||||||
}
|
}
|
||||||
@@ -106,8 +108,13 @@ export class UploadNode extends DecoratorBlockNode {
|
|||||||
if (serializedNode.version === 1 && (serializedNode?.value as unknown as { id: string })?.id) {
|
if (serializedNode.version === 1 && (serializedNode?.value as unknown as { id: string })?.id) {
|
||||||
serializedNode.value = (serializedNode.value as unknown as { id: string }).id
|
serializedNode.value = (serializedNode.value as unknown as { id: string }).id
|
||||||
}
|
}
|
||||||
|
if (serializedNode.version === 2 && !serializedNode?.id) {
|
||||||
|
serializedNode.id = new ObjectID.default().toHexString()
|
||||||
|
serializedNode.version = 3
|
||||||
|
}
|
||||||
|
|
||||||
const importedData: UploadData = {
|
const importedData: UploadData = {
|
||||||
|
id: serializedNode.id,
|
||||||
fields: serializedNode.fields,
|
fields: serializedNode.fields,
|
||||||
relationTo: serializedNode.relationTo,
|
relationTo: serializedNode.relationTo,
|
||||||
value: serializedNode.value,
|
value: serializedNode.value,
|
||||||
@@ -141,7 +148,7 @@ export class UploadNode extends DecoratorBlockNode {
|
|||||||
...super.exportJSON(),
|
...super.exportJSON(),
|
||||||
...this.getData(),
|
...this.getData(),
|
||||||
type: this.getType(),
|
type: this.getType(),
|
||||||
version: 2,
|
version: 3,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,8 +167,15 @@ export class UploadNode extends DecoratorBlockNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $createUploadNode({ data }: { data: UploadData }): UploadNode {
|
export function $createUploadNode({
|
||||||
return $applyNodeReplacement(new UploadNode({ data }))
|
data,
|
||||||
|
}: {
|
||||||
|
data: Omit<UploadData, 'id'> & Partial<Pick<UploadData, 'id'>>
|
||||||
|
}): UploadNode {
|
||||||
|
if (!data?.id) {
|
||||||
|
data.id = new ObjectID.default().toHexString()
|
||||||
|
}
|
||||||
|
return $applyNodeReplacement(new UploadNode({ data: data as UploadData }))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $isUploadNode(node: LexicalNode | null | undefined): node is UploadNode {
|
export function $isUploadNode(node: LexicalNode | null | undefined): node is UploadNode {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ 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<UploadData>
|
export type InsertUploadPayload = Readonly<Omit<UploadData, 'id'> & Partial<Pick<UploadData, 'id'>>>
|
||||||
|
|
||||||
export const INSERT_UPLOAD_COMMAND: LexicalCommand<InsertUploadPayload> =
|
export const INSERT_UPLOAD_COMMAND: LexicalCommand<InsertUploadPayload> =
|
||||||
createCommand('INSERT_UPLOAD_COMMAND')
|
createCommand('INSERT_UPLOAD_COMMAND')
|
||||||
@@ -47,6 +47,7 @@ export const UploadPlugin: PluginComponentWithAnchor<UploadFeaturePropsClient> =
|
|||||||
if ($isRangeSelection(selection)) {
|
if ($isRangeSelection(selection)) {
|
||||||
const uploadNode = $createUploadNode({
|
const uploadNode = $createUploadNode({
|
||||||
data: {
|
data: {
|
||||||
|
id: payload.id,
|
||||||
fields: payload.fields,
|
fields: payload.fields,
|
||||||
relationTo: payload.relationTo,
|
relationTo: payload.relationTo,
|
||||||
value: payload.value,
|
value: payload.value,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Config, SanitizedConfig } from 'payload/config'
|
import type { SanitizedConfig } from 'payload/config'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
FeatureProviderServer,
|
FeatureProviderServer,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Config, SanitizedConfig } from 'payload/config'
|
import type { SanitizedConfig } from 'payload/config'
|
||||||
|
|
||||||
import type { ResolvedServerFeatureMap, SanitizedServerFeatures } from '../../../features/types.js'
|
import type { ResolvedServerFeatureMap, SanitizedServerFeatures } from '../../../features/types.js'
|
||||||
import type { SanitizedServerEditorConfig, ServerEditorConfig } from '../types.js'
|
import type { SanitizedServerEditorConfig, ServerEditorConfig } from '../types.js'
|
||||||
@@ -16,18 +16,18 @@ export const sanitizeServerFeatures = (
|
|||||||
generatedTypes: {
|
generatedTypes: {
|
||||||
modifyOutputSchemas: [],
|
modifyOutputSchemas: [],
|
||||||
},
|
},
|
||||||
|
getSubFields: new Map(),
|
||||||
|
getSubFieldsData: new Map(),
|
||||||
|
graphQLPopulationPromises: new Map(),
|
||||||
hooks: {
|
hooks: {
|
||||||
afterChange: new Map(),
|
afterChange: new Map(),
|
||||||
afterRead: new Map(),
|
afterRead: new Map(),
|
||||||
beforeChange: new Map(),
|
beforeChange: new Map(),
|
||||||
beforeDuplicate: new Map(),
|
|
||||||
beforeValidate: new Map(),
|
beforeValidate: new Map(),
|
||||||
},
|
},
|
||||||
i18n: {},
|
i18n: {},
|
||||||
markdownTransformers: [],
|
markdownTransformers: [],
|
||||||
nodes: [],
|
nodes: [],
|
||||||
populationPromises: new Map(),
|
|
||||||
|
|
||||||
validations: new Map(),
|
validations: new Map(),
|
||||||
}
|
}
|
||||||
@@ -56,8 +56,8 @@ export const sanitizeServerFeatures = (
|
|||||||
sanitized.nodes = sanitized.nodes.concat(feature.nodes)
|
sanitized.nodes = sanitized.nodes.concat(feature.nodes)
|
||||||
feature.nodes.forEach((node) => {
|
feature.nodes.forEach((node) => {
|
||||||
const nodeType = 'with' in node.node ? node.node.replace.getType() : node.node.getType() // TODO: Idk if this works for node replacements
|
const nodeType = 'with' in node.node ? node.node.replace.getType() : node.node.getType() // TODO: Idk if this works for node replacements
|
||||||
if (node?.populationPromises?.length) {
|
if (node?.graphQLPopulationPromises?.length) {
|
||||||
sanitized.populationPromises.set(nodeType, node.populationPromises)
|
sanitized.graphQLPopulationPromises.set(nodeType, node.graphQLPopulationPromises)
|
||||||
}
|
}
|
||||||
if (node?.validations?.length) {
|
if (node?.validations?.length) {
|
||||||
sanitized.validations.set(nodeType, node.validations)
|
sanitized.validations.set(nodeType, node.validations)
|
||||||
@@ -74,12 +74,15 @@ export const sanitizeServerFeatures = (
|
|||||||
if (node?.hooks?.beforeChange) {
|
if (node?.hooks?.beforeChange) {
|
||||||
sanitized.hooks.beforeChange.set(nodeType, 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) {
|
if (node?.hooks?.beforeValidate) {
|
||||||
sanitized.hooks.beforeValidate.set(nodeType, node.hooks.beforeValidate)
|
sanitized.hooks.beforeValidate.set(nodeType, node.hooks.beforeValidate)
|
||||||
}
|
}
|
||||||
|
if (node?.getSubFields) {
|
||||||
|
sanitized.getSubFields.set(nodeType, node.getSubFields)
|
||||||
|
}
|
||||||
|
if (node?.getSubFieldsData) {
|
||||||
|
sanitized.getSubFieldsData.set(nodeType, node.getSubFieldsData)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
import type { JSONSchema4 } from 'json-schema'
|
import type { JSONSchema4 } from 'json-schema'
|
||||||
import type { EditorConfig as LexicalEditorConfig } from 'lexical'
|
import type {
|
||||||
|
EditorConfig as LexicalEditorConfig,
|
||||||
|
SerializedEditorState,
|
||||||
|
SerializedLexicalNode,
|
||||||
|
} from 'lexical'
|
||||||
|
|
||||||
import { withMergedProps } from '@payloadcms/ui/elements/withMergedProps'
|
import { withMergedProps } from '@payloadcms/ui/elements/withMergedProps'
|
||||||
import { withNullableJSONSchemaType } from 'payload/utilities'
|
import {
|
||||||
|
afterChangeTraverseFields,
|
||||||
|
afterReadTraverseFields,
|
||||||
|
beforeChangeTraverseFields,
|
||||||
|
beforeValidateTraverseFields,
|
||||||
|
withNullableJSONSchemaType,
|
||||||
|
} from 'payload/utilities'
|
||||||
|
|
||||||
import type { FeatureProviderServer, ResolvedServerFeatureMap } from './field/features/types.js'
|
import type { FeatureProviderServer, ResolvedServerFeatureMap } from './field/features/types.js'
|
||||||
import type { SanitizedServerEditorConfig } from './field/lexical/config/types.js'
|
import type { SanitizedServerEditorConfig } from './field/lexical/config/types.js'
|
||||||
@@ -28,7 +38,8 @@ 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 { i18n } from './i18n.js'
|
import { i18n } from './i18n.js'
|
||||||
import { populateLexicalPopulationPromises } from './populate/populateLexicalPopulationPromises.js'
|
import { populateLexicalPopulationPromises } from './populateGraphQL/populateLexicalPopulationPromises.js'
|
||||||
|
import { recurseNodeTree } from './recurseNodeTree.js'
|
||||||
import { richTextValidateHOC } from './validate/index.js'
|
import { richTextValidateHOC } from './validate/index.js'
|
||||||
|
|
||||||
let defaultSanitizedServerEditorConfig: SanitizedServerEditorConfig = null
|
let defaultSanitizedServerEditorConfig: SanitizedServerEditorConfig = null
|
||||||
@@ -121,105 +132,560 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
|||||||
generateSchemaMap: getGenerateSchemaMap({
|
generateSchemaMap: getGenerateSchemaMap({
|
||||||
resolvedFeatureMap,
|
resolvedFeatureMap,
|
||||||
}),
|
}),
|
||||||
i18n: featureI18n,
|
graphQLPopulationPromises({
|
||||||
/* hooks: {
|
context,
|
||||||
afterChange: finalSanitizedEditorConfig.features.hooks.afterChange,
|
currentDepth,
|
||||||
afterRead: finalSanitizedEditorConfig.features.hooks.afterRead,
|
depth,
|
||||||
beforeChange: finalSanitizedEditorConfig.features.hooks.beforeChange,
|
draft,
|
||||||
beforeDuplicate: finalSanitizedEditorConfig.features.hooks.beforeDuplicate,
|
field,
|
||||||
beforeValidate: finalSanitizedEditorConfig.features.hooks.beforeValidate,
|
fieldPromises,
|
||||||
},*/
|
findMany,
|
||||||
/* // TODO: Figure out docWithLocales / originalSiblingDoc => node matching. Can't use indexes, as the order of nodes could technically change between hooks.
|
flattenLocales,
|
||||||
|
overrideAccess,
|
||||||
|
populationPromises,
|
||||||
|
req,
|
||||||
|
showHiddenFields,
|
||||||
|
siblingDoc,
|
||||||
|
}) {
|
||||||
|
// check if there are any features with nodes which have populationPromises for this field
|
||||||
|
if (finalSanitizedEditorConfig?.features?.graphQLPopulationPromises?.size) {
|
||||||
|
populateLexicalPopulationPromises({
|
||||||
|
context,
|
||||||
|
currentDepth: currentDepth ?? 0,
|
||||||
|
depth,
|
||||||
|
draft,
|
||||||
|
editorPopulationPromises: finalSanitizedEditorConfig.features.graphQLPopulationPromises,
|
||||||
|
field,
|
||||||
|
fieldPromises,
|
||||||
|
findMany,
|
||||||
|
flattenLocales,
|
||||||
|
overrideAccess,
|
||||||
|
populationPromises,
|
||||||
|
req,
|
||||||
|
showHiddenFields,
|
||||||
|
siblingDoc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
hooks: {
|
hooks: {
|
||||||
afterChange: [
|
afterChange: [
|
||||||
async ({ context, findMany, operation, overrideAccess, req, value }) => {
|
async ({
|
||||||
await recurseNodesAsync({
|
collection,
|
||||||
callback: async (node) => {
|
context: _context,
|
||||||
const afterChangeHooks = finalSanitizedEditorConfig.features.hooks.afterChange
|
global,
|
||||||
if (afterChangeHooks?.has(node.type)) {
|
operation,
|
||||||
for (const hook of afterChangeHooks.get(node.type)) {
|
path,
|
||||||
node = await hook({ context, findMany, node, operation, overrideAccess, req })
|
req,
|
||||||
}
|
schemaPath,
|
||||||
}
|
value,
|
||||||
},
|
}) => {
|
||||||
|
if (
|
||||||
|
!finalSanitizedEditorConfig.features.hooks.afterChange.size &&
|
||||||
|
!finalSanitizedEditorConfig.features.getSubFields.size
|
||||||
|
) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
const context: any = _context
|
||||||
|
const nodeIDMap: {
|
||||||
|
[key: string]: SerializedLexicalNode
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the originalNodeIDMap from the beforeValidate hook, which is always run before this hook.
|
||||||
|
*/
|
||||||
|
const originalNodeIDMap: {
|
||||||
|
[key: string]: SerializedLexicalNode
|
||||||
|
} = context?.internal?.richText?.[path.join('.')]?.originalNodeIDMap
|
||||||
|
|
||||||
|
if (!originalNodeIDMap || !Object.keys(originalNodeIDMap).length || !value) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
recurseNodeTree({
|
||||||
|
nodeIDMap,
|
||||||
nodes: (value as SerializedEditorState)?.root?.children ?? [],
|
nodes: (value as SerializedEditorState)?.root?.children ?? [],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
for (let [id, node] of Object.entries(nodeIDMap)) {
|
||||||
|
const afterChangeHooks = finalSanitizedEditorConfig.features.hooks.afterChange
|
||||||
|
if (afterChangeHooks?.has(node.type)) {
|
||||||
|
for (const hook of afterChangeHooks.get(node.type)) {
|
||||||
|
if (!originalNodeIDMap[id]) {
|
||||||
|
console.warn(
|
||||||
|
'(afterChange) No original node found for node with id',
|
||||||
|
id,
|
||||||
|
'node:',
|
||||||
|
node,
|
||||||
|
'path',
|
||||||
|
path.join('.'),
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
node = await hook({
|
||||||
|
context,
|
||||||
|
node,
|
||||||
|
operation,
|
||||||
|
originalNode: originalNodeIDMap[id],
|
||||||
|
parentRichTextFieldPath: path,
|
||||||
|
parentRichTextFieldSchemaPath: schemaPath,
|
||||||
|
req,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const subFieldFn = finalSanitizedEditorConfig.features.getSubFields.get(node.type)
|
||||||
|
const subFieldDataFn = finalSanitizedEditorConfig.features.getSubFieldsData.get(
|
||||||
|
node.type,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (subFieldFn) {
|
||||||
|
const subFields = subFieldFn({ node, req })
|
||||||
|
const data = subFieldDataFn({ node, req })
|
||||||
|
const originalData = subFieldDataFn({ node: originalNodeIDMap[id], req })
|
||||||
|
|
||||||
|
if (subFields?.length) {
|
||||||
|
await afterChangeTraverseFields({
|
||||||
|
collection,
|
||||||
|
context,
|
||||||
|
data: originalData,
|
||||||
|
doc: data,
|
||||||
|
fields: subFields,
|
||||||
|
global,
|
||||||
|
operation,
|
||||||
|
path,
|
||||||
|
previousDoc: data,
|
||||||
|
previousSiblingDoc: { ...data },
|
||||||
|
req,
|
||||||
|
schemaPath,
|
||||||
|
siblingData: originalData || {},
|
||||||
|
siblingDoc: { ...data },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return value
|
return value
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
afterRead: [
|
afterRead: [
|
||||||
async ({ context, findMany, operation, overrideAccess, req, value }) => {
|
/**
|
||||||
await recurseNodesAsync({
|
* afterRead hooks do not receive the originalNode. Thus, they can run on all nodes, not just nodes with an ID.
|
||||||
callback: async (node) => {
|
*/
|
||||||
const afterReadHooks = finalSanitizedEditorConfig.features.hooks.afterRead
|
async ({
|
||||||
if (afterReadHooks?.has(node.type)) {
|
collection,
|
||||||
for (const hook of afterReadHooks.get(node.type)) {
|
context: context,
|
||||||
node = await hook({ context, findMany, node, operation, overrideAccess, req })
|
currentDepth,
|
||||||
}
|
depth,
|
||||||
}
|
draft,
|
||||||
},
|
fallbackLocale,
|
||||||
|
fieldPromises,
|
||||||
|
findMany,
|
||||||
|
flattenLocales,
|
||||||
|
global,
|
||||||
|
locale,
|
||||||
|
overrideAccess,
|
||||||
|
path,
|
||||||
|
populationPromises,
|
||||||
|
req,
|
||||||
|
schemaPath,
|
||||||
|
showHiddenFields,
|
||||||
|
triggerAccessControl,
|
||||||
|
triggerHooks,
|
||||||
|
value,
|
||||||
|
}) => {
|
||||||
|
if (
|
||||||
|
!finalSanitizedEditorConfig.features.hooks.afterRead.size &&
|
||||||
|
!finalSanitizedEditorConfig.features.getSubFields.size
|
||||||
|
) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
const flattenedNodes: SerializedLexicalNode[] = []
|
||||||
|
|
||||||
|
recurseNodeTree({
|
||||||
|
flattenedNodes,
|
||||||
nodes: (value as SerializedEditorState)?.root?.children ?? [],
|
nodes: (value as SerializedEditorState)?.root?.children ?? [],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
for (let node of flattenedNodes) {
|
||||||
|
const afterReadHooks = finalSanitizedEditorConfig.features.hooks.afterRead
|
||||||
|
if (afterReadHooks?.has(node.type)) {
|
||||||
|
for (const hook of afterReadHooks.get(node.type)) {
|
||||||
|
node = await hook({
|
||||||
|
context,
|
||||||
|
currentDepth,
|
||||||
|
depth,
|
||||||
|
draft,
|
||||||
|
fallbackLocale,
|
||||||
|
fieldPromises,
|
||||||
|
findMany,
|
||||||
|
flattenLocales,
|
||||||
|
locale,
|
||||||
|
node,
|
||||||
|
overrideAccess,
|
||||||
|
parentRichTextFieldPath: path,
|
||||||
|
parentRichTextFieldSchemaPath: schemaPath,
|
||||||
|
populationPromises,
|
||||||
|
req,
|
||||||
|
showHiddenFields,
|
||||||
|
triggerAccessControl,
|
||||||
|
triggerHooks,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const subFieldFn = finalSanitizedEditorConfig.features.getSubFields.get(node.type)
|
||||||
|
const subFieldDataFn = finalSanitizedEditorConfig.features.getSubFieldsData.get(
|
||||||
|
node.type,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (subFieldFn) {
|
||||||
|
const subFields = subFieldFn({ node, req })
|
||||||
|
const data = subFieldDataFn({ node, req })
|
||||||
|
|
||||||
|
if (subFields?.length) {
|
||||||
|
afterReadTraverseFields({
|
||||||
|
collection,
|
||||||
|
context,
|
||||||
|
currentDepth,
|
||||||
|
depth,
|
||||||
|
doc: data,
|
||||||
|
draft,
|
||||||
|
fallbackLocale,
|
||||||
|
fieldPromises,
|
||||||
|
fields: subFields,
|
||||||
|
findMany,
|
||||||
|
flattenLocales,
|
||||||
|
global,
|
||||||
|
locale,
|
||||||
|
overrideAccess,
|
||||||
|
path,
|
||||||
|
populationPromises,
|
||||||
|
req,
|
||||||
|
schemaPath,
|
||||||
|
showHiddenFields,
|
||||||
|
siblingDoc: data,
|
||||||
|
triggerAccessControl,
|
||||||
|
triggerHooks,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return value
|
return value
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
beforeChange: [
|
beforeChange: [
|
||||||
async ({ context, findMany, operation, overrideAccess, req, value }) => {
|
async ({
|
||||||
await recurseNodesAsync({
|
collection,
|
||||||
callback: async (node) => {
|
context: _context,
|
||||||
const beforeChangeHooks = finalSanitizedEditorConfig.features.hooks.beforeChange
|
duplicate,
|
||||||
if (beforeChangeHooks?.has(node.type)) {
|
errors,
|
||||||
for (const hook of beforeChangeHooks.get(node.type)) {
|
field,
|
||||||
node = await hook({ context, findMany, node, operation, overrideAccess, req })
|
global,
|
||||||
}
|
mergeLocaleActions,
|
||||||
}
|
operation,
|
||||||
},
|
path,
|
||||||
|
req,
|
||||||
|
schemaPath,
|
||||||
|
siblingData,
|
||||||
|
siblingDocWithLocales,
|
||||||
|
skipValidation,
|
||||||
|
value,
|
||||||
|
}) => {
|
||||||
|
if (
|
||||||
|
!finalSanitizedEditorConfig.features.hooks.beforeChange.size &&
|
||||||
|
!finalSanitizedEditorConfig.features.getSubFields.size
|
||||||
|
) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const context: any = _context
|
||||||
|
const nodeIDMap: {
|
||||||
|
[key: string]: SerializedLexicalNode
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the originalNodeIDMap from the beforeValidate hook, which is always run before this hook.
|
||||||
|
*/
|
||||||
|
const originalNodeIDMap: {
|
||||||
|
[key: string]: SerializedLexicalNode
|
||||||
|
} = context?.internal?.richText?.[path.join('.')]?.originalNodeIDMap
|
||||||
|
|
||||||
|
if (!originalNodeIDMap || !Object.keys(originalNodeIDMap).length || !value) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalNodeWithLocalesIDMap: {
|
||||||
|
[key: string]: SerializedLexicalNode
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
recurseNodeTree({
|
||||||
|
nodeIDMap,
|
||||||
nodes: (value as SerializedEditorState)?.root?.children ?? [],
|
nodes: (value as SerializedEditorState)?.root?.children ?? [],
|
||||||
})
|
})
|
||||||
|
|
||||||
return value
|
if (siblingDocWithLocales?.[field.name]) {
|
||||||
},
|
recurseNodeTree({
|
||||||
],
|
nodeIDMap: originalNodeWithLocalesIDMap,
|
||||||
beforeDuplicate: [
|
nodes:
|
||||||
async ({ context, findMany, operation, overrideAccess, req, value }) => {
|
(siblingDocWithLocales[field.name] as SerializedEditorState)?.root?.children ??
|
||||||
await recurseNodesAsync({
|
[],
|
||||||
callback: async (node) => {
|
})
|
||||||
const beforeDuplicateHooks = finalSanitizedEditorConfig.features.hooks.beforeDuplicate
|
}
|
||||||
if (beforeDuplicateHooks?.has(node.type)) {
|
|
||||||
for (const hook of beforeDuplicateHooks.get(node.type)) {
|
// eslint-disable-next-line prefer-const
|
||||||
node = await hook({ context, findMany, node, operation, overrideAccess, req })
|
for (let [id, node] of Object.entries(nodeIDMap)) {
|
||||||
|
const beforeChangeHooks = finalSanitizedEditorConfig.features.hooks.beforeChange
|
||||||
|
if (beforeChangeHooks?.has(node.type)) {
|
||||||
|
for (const hook of beforeChangeHooks.get(node.type)) {
|
||||||
|
if (!originalNodeIDMap[id]) {
|
||||||
|
console.warn(
|
||||||
|
'(beforeChange) No original node found for node with id',
|
||||||
|
id,
|
||||||
|
'node:',
|
||||||
|
node,
|
||||||
|
'path',
|
||||||
|
path.join('.'),
|
||||||
|
)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
node = await hook({
|
||||||
|
context,
|
||||||
|
duplicate,
|
||||||
|
errors,
|
||||||
|
mergeLocaleActions,
|
||||||
|
node,
|
||||||
|
operation,
|
||||||
|
originalNode: originalNodeIDMap[id],
|
||||||
|
originalNodeWithLocales: originalNodeWithLocalesIDMap[id],
|
||||||
|
parentRichTextFieldPath: path,
|
||||||
|
parentRichTextFieldSchemaPath: schemaPath,
|
||||||
|
req,
|
||||||
|
skipValidation,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
nodes: (value as SerializedEditorState)?.root?.children ?? [],
|
|
||||||
|
const subFieldFn = finalSanitizedEditorConfig.features.getSubFields.get(node.type)
|
||||||
|
const subFieldDataFn = finalSanitizedEditorConfig.features.getSubFieldsData.get(
|
||||||
|
node.type,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (subFieldFn) {
|
||||||
|
const subFields = subFieldFn({ node, req })
|
||||||
|
const data = subFieldDataFn({ node, req })
|
||||||
|
const originalData = subFieldDataFn({ node: originalNodeIDMap[id], req })
|
||||||
|
const originalDataWithLocales = subFieldDataFn({
|
||||||
|
node: originalNodeWithLocalesIDMap[id],
|
||||||
|
req,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (subFields?.length) {
|
||||||
|
await beforeChangeTraverseFields({
|
||||||
|
id,
|
||||||
|
collection,
|
||||||
|
context,
|
||||||
|
data,
|
||||||
|
doc: originalData,
|
||||||
|
docWithLocales: originalDataWithLocales ?? {},
|
||||||
|
duplicate,
|
||||||
|
errors,
|
||||||
|
fields: subFields,
|
||||||
|
global,
|
||||||
|
mergeLocaleActions,
|
||||||
|
operation,
|
||||||
|
path,
|
||||||
|
req,
|
||||||
|
schemaPath,
|
||||||
|
siblingData: data,
|
||||||
|
siblingDoc: originalData,
|
||||||
|
siblingDocWithLocales: originalDataWithLocales ?? {},
|
||||||
|
skipValidation,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* within the beforeChange hook, id's may be re-generated.
|
||||||
|
* Example:
|
||||||
|
* 1. Seed data contains IDs for block feature blocks.
|
||||||
|
* 2. Those are used in beforeValidate
|
||||||
|
* 3. in beforeChange, those IDs are regenerated, because you cannot provide IDs during document creation. See baseIDField beforeChange hook for reasoning
|
||||||
|
* 4. Thus, in order for all post-beforeChange hooks to receive the correct ID, we need to update the originalNodeIDMap with the new ID's, by regenerating the nodeIDMap.
|
||||||
|
* The reason this is not generated for every hook, is to save on performance. We know we only really have to generate it in beforeValidate, which is the first hook,
|
||||||
|
* and in beforeChange, which is where modifications to the provided IDs can occur.
|
||||||
|
*/
|
||||||
|
const newOriginalNodeIDMap: {
|
||||||
|
[key: string]: SerializedLexicalNode
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
const previousValue = siblingData[field.name]
|
||||||
|
|
||||||
|
recurseNodeTree({
|
||||||
|
nodeIDMap: newOriginalNodeIDMap,
|
||||||
|
nodes: (previousValue as SerializedEditorState)?.root?.children ?? [],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!context.internal) {
|
||||||
|
// Add to context, for other hooks to use
|
||||||
|
context.internal = {}
|
||||||
|
}
|
||||||
|
if (!context.internal.richText) {
|
||||||
|
context.internal.richText = {}
|
||||||
|
}
|
||||||
|
context.internal.richText[path.join('.')] = {
|
||||||
|
originalNodeIDMap: newOriginalNodeIDMap,
|
||||||
|
}
|
||||||
|
|
||||||
return value
|
return value
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
beforeValidate: [
|
beforeValidate: [
|
||||||
async ({ context, findMany, operation, overrideAccess, req, value }) => {
|
async ({
|
||||||
await recurseNodesAsync({
|
collection,
|
||||||
callback: async (node) => {
|
context,
|
||||||
const beforeValidateHooks = finalSanitizedEditorConfig.features.hooks.beforeValidate
|
global,
|
||||||
if (beforeValidateHooks?.has(node.type)) {
|
operation,
|
||||||
for (const hook of beforeValidateHooks.get(node.type)) {
|
overrideAccess,
|
||||||
/**
|
path,
|
||||||
* We cannot pass the originalNode here, as there is no way to map one node to a previous one, as a previous originalNode might be in a different position
|
previousValue,
|
||||||
*/ /*
|
req,
|
||||||
node = await hook({ context, findMany, node, operation, overrideAccess, req })
|
schemaPath,
|
||||||
|
value,
|
||||||
|
}) => {
|
||||||
|
// return value if there are NO hooks
|
||||||
|
if (
|
||||||
|
!finalSanitizedEditorConfig.features.hooks.beforeValidate.size &&
|
||||||
|
!finalSanitizedEditorConfig.features.hooks.afterChange.size &&
|
||||||
|
!finalSanitizedEditorConfig.features.hooks.beforeChange.size &&
|
||||||
|
!finalSanitizedEditorConfig.features.getSubFields.size
|
||||||
|
) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* beforeValidate is the first field hook which runs. This is where we can create the node map, which can then be used in the other hooks.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* flattenedNodes contains all nodes in the editor, in the order they appear in the editor. They will be used for the following hooks:
|
||||||
|
* - afterRead
|
||||||
|
*
|
||||||
|
* The other hooks require nodes to have IDs, which is why those are ran only from the nodeIDMap. They require IDs because they have both doc/siblingDoc and data/siblingData, and
|
||||||
|
* thus require a reliable way to match new node data to old node data. Given that node positions can change in between hooks, this is only reliably possible for nodes which are saved with
|
||||||
|
* an ID.
|
||||||
|
*/
|
||||||
|
//const flattenedNodes: SerializedLexicalNode[] = []
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only nodes with id's (so, nodes with hooks added to them) will be added to the nodeIDMap. They will be used for the following hooks:
|
||||||
|
* - afterChange
|
||||||
|
* - beforeChange
|
||||||
|
* - beforeValidate
|
||||||
|
* - beforeDuplicate
|
||||||
|
*
|
||||||
|
* Other hooks are handled by the flattenedNodes. All nodes in the nodeIDMap are part of flattenedNodes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const originalNodeIDMap: {
|
||||||
|
[key: string]: SerializedLexicalNode
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
recurseNodeTree({
|
||||||
|
nodeIDMap: originalNodeIDMap,
|
||||||
|
nodes: (previousValue as SerializedEditorState)?.root?.children ?? [],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!context.internal) {
|
||||||
|
// Add to context, for other hooks to use
|
||||||
|
context.internal = {}
|
||||||
|
}
|
||||||
|
if (!(context as any).internal.richText) {
|
||||||
|
;(context as any).internal.richText = {}
|
||||||
|
}
|
||||||
|
;(context as any).internal.richText[path.join('.')] = {
|
||||||
|
originalNodeIDMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Now that the maps for all hooks are set up, we can run the validate hook
|
||||||
|
*/
|
||||||
|
if (!finalSanitizedEditorConfig.features.hooks.beforeValidate.size) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
const nodeIDMap: {
|
||||||
|
[key: string]: SerializedLexicalNode
|
||||||
|
} = {}
|
||||||
|
recurseNodeTree({
|
||||||
|
//flattenedNodes,
|
||||||
|
nodeIDMap,
|
||||||
|
nodes: (value as SerializedEditorState)?.root?.children ?? [],
|
||||||
|
})
|
||||||
|
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
for (let [id, node] of Object.entries(nodeIDMap)) {
|
||||||
|
const beforeValidateHooks = finalSanitizedEditorConfig.features.hooks.beforeValidate
|
||||||
|
if (beforeValidateHooks?.has(node.type)) {
|
||||||
|
for (const hook of beforeValidateHooks.get(node.type)) {
|
||||||
|
if (!originalNodeIDMap[id]) {
|
||||||
|
console.warn(
|
||||||
|
'(beforeValidate) No original node found for node with id',
|
||||||
|
id,
|
||||||
|
'node:',
|
||||||
|
node,
|
||||||
|
'path',
|
||||||
|
path.join('.'),
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
node = await hook({
|
||||||
|
context,
|
||||||
|
node,
|
||||||
|
operation,
|
||||||
|
originalNode: originalNodeIDMap[id],
|
||||||
|
overrideAccess,
|
||||||
|
parentRichTextFieldPath: path,
|
||||||
|
parentRichTextFieldSchemaPath: schemaPath,
|
||||||
|
req,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
const subFieldFn = finalSanitizedEditorConfig.features.getSubFields.get(node.type)
|
||||||
nodes: (value as SerializedEditorState)?.root?.children ?? [],
|
const subFieldDataFn = finalSanitizedEditorConfig.features.getSubFieldsData.get(
|
||||||
})
|
node.type,
|
||||||
|
)
|
||||||
|
|
||||||
return value
|
if (subFieldFn) {
|
||||||
},
|
const subFields = subFieldFn({ node, req })
|
||||||
],
|
const data = subFieldDataFn({ node, req })
|
||||||
},*/
|
const originalData = subFieldDataFn({ node: originalNodeIDMap[id], req })
|
||||||
|
|
||||||
|
if (subFields?.length) {
|
||||||
|
await beforeValidateTraverseFields({
|
||||||
|
id,
|
||||||
|
collection,
|
||||||
|
context,
|
||||||
|
data,
|
||||||
|
doc: originalData,
|
||||||
|
fields: subFields,
|
||||||
|
global,
|
||||||
|
operation,
|
||||||
|
overrideAccess,
|
||||||
|
path,
|
||||||
|
req,
|
||||||
|
schemaPath,
|
||||||
|
siblingData: data,
|
||||||
|
siblingDoc: originalData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
i18n: featureI18n,
|
||||||
outputSchema: ({
|
outputSchema: ({
|
||||||
collectionIDFieldTypes,
|
collectionIDFieldTypes,
|
||||||
config,
|
config,
|
||||||
@@ -297,41 +763,6 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
|||||||
|
|
||||||
return outputSchema
|
return outputSchema
|
||||||
},
|
},
|
||||||
populationPromises({
|
|
||||||
context,
|
|
||||||
currentDepth,
|
|
||||||
depth,
|
|
||||||
draft,
|
|
||||||
field,
|
|
||||||
fieldPromises,
|
|
||||||
findMany,
|
|
||||||
flattenLocales,
|
|
||||||
overrideAccess,
|
|
||||||
populationPromises,
|
|
||||||
req,
|
|
||||||
showHiddenFields,
|
|
||||||
siblingDoc,
|
|
||||||
}) {
|
|
||||||
// check if there are any features with nodes which have populationPromises for this field
|
|
||||||
if (finalSanitizedEditorConfig?.features?.populationPromises?.size) {
|
|
||||||
populateLexicalPopulationPromises({
|
|
||||||
context,
|
|
||||||
currentDepth,
|
|
||||||
depth,
|
|
||||||
draft,
|
|
||||||
editorPopulationPromises: finalSanitizedEditorConfig.features.populationPromises,
|
|
||||||
field,
|
|
||||||
fieldPromises,
|
|
||||||
findMany,
|
|
||||||
flattenLocales,
|
|
||||||
overrideAccess,
|
|
||||||
populationPromises,
|
|
||||||
req,
|
|
||||||
showHiddenFields,
|
|
||||||
siblingDoc,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
validate: richTextValidateHOC({
|
validate: richTextValidateHOC({
|
||||||
editorConfig: finalSanitizedEditorConfig,
|
editorConfig: finalSanitizedEditorConfig,
|
||||||
}),
|
}),
|
||||||
@@ -452,6 +883,15 @@ export { InlineToolbarFeature } from './field/features/toolbars/inline/feature.s
|
|||||||
export type { ToolbarGroup, ToolbarGroupItem } from './field/features/toolbars/types.js'
|
export type { ToolbarGroup, ToolbarGroupItem } from './field/features/toolbars/types.js'
|
||||||
export { createNode } from './field/features/typeUtilities.js'
|
export { createNode } from './field/features/typeUtilities.js'
|
||||||
export type {
|
export type {
|
||||||
|
AfterChangeNodeHook,
|
||||||
|
AfterChangeNodeHookArgs,
|
||||||
|
AfterReadNodeHook,
|
||||||
|
AfterReadNodeHookArgs,
|
||||||
|
BaseNodeHookArgs,
|
||||||
|
BeforeChangeNodeHook,
|
||||||
|
BeforeChangeNodeHookArgs,
|
||||||
|
BeforeValidateNodeHook,
|
||||||
|
BeforeValidateNodeHookArgs,
|
||||||
ClientComponentProps,
|
ClientComponentProps,
|
||||||
ClientFeature,
|
ClientFeature,
|
||||||
ClientFeatureProviderMap,
|
ClientFeatureProviderMap,
|
||||||
@@ -459,8 +899,6 @@ export type {
|
|||||||
FeatureProviderProviderClient,
|
FeatureProviderProviderClient,
|
||||||
FeatureProviderProviderServer,
|
FeatureProviderProviderServer,
|
||||||
FeatureProviderServer,
|
FeatureProviderServer,
|
||||||
FieldNodeHook,
|
|
||||||
FieldNodeHookArgs,
|
|
||||||
NodeValidation,
|
NodeValidation,
|
||||||
NodeWithHooks,
|
NodeWithHooks,
|
||||||
PluginComponent,
|
PluginComponent,
|
||||||
@@ -559,6 +997,6 @@ export {
|
|||||||
addSwipeUpListener,
|
addSwipeUpListener,
|
||||||
} from './field/lexical/utils/swipe.js'
|
} from './field/lexical/utils/swipe.js'
|
||||||
export { sanitizeUrl, validateUrl } from './field/lexical/utils/url.js'
|
export { sanitizeUrl, validateUrl } from './field/lexical/utils/url.js'
|
||||||
export { defaultRichTextValue } from './populate/defaultValue.js'
|
export { defaultRichTextValue } from './populateGraphQL/defaultValue.js'
|
||||||
|
|
||||||
export type { LexicalEditorProps, LexicalRichTextAdapter } from './types.js'
|
export type { LexicalEditorProps, LexicalRichTextAdapter } from './types.js'
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
import type { SerializedEditorState } from 'lexical'
|
|
||||||
import type { PayloadRequestWithData } from 'payload/types'
|
import type { PayloadRequestWithData } from 'payload/types'
|
||||||
import type { Collection, Field, RichTextField } from 'payload/types'
|
import type { Collection } from 'payload/types'
|
||||||
|
|
||||||
import { createDataloaderCacheKey } from 'payload/utilities'
|
import { createDataloaderCacheKey } from 'payload/utilities'
|
||||||
|
|
||||||
import type { AdapterProps } from '../types.js'
|
|
||||||
|
|
||||||
type Arguments = {
|
type Arguments = {
|
||||||
currentDepth?: number
|
currentDepth?: number
|
||||||
data: unknown
|
data: unknown
|
||||||
depth: number
|
depth: number
|
||||||
draft: boolean
|
draft: boolean
|
||||||
field: RichTextField<SerializedEditorState, AdapterProps>
|
|
||||||
key: number | string
|
key: number | string
|
||||||
overrideAccess?: boolean
|
overrideAccess: boolean
|
||||||
req: PayloadRequestWithData
|
req: PayloadRequestWithData
|
||||||
showHiddenFields: boolean
|
showHiddenFields: boolean
|
||||||
}
|
}
|
||||||
@@ -29,11 +25,16 @@ export const populate = async ({
|
|||||||
overrideAccess,
|
overrideAccess,
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
}: Omit<Arguments, 'field'> & {
|
}: Arguments & {
|
||||||
collection: Collection
|
collection: Collection
|
||||||
field: Field
|
|
||||||
id: number | string
|
id: number | string
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
|
const shouldPopulate = depth && currentDepth <= depth
|
||||||
|
// usually depth is checked within recursivelyPopulateFieldsForGraphQL. But since this populate function can be called outside of that (in rest afterRead node hooks) we need to check here too
|
||||||
|
if (!shouldPopulate) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const dataRef = data as Record<string, unknown>
|
const dataRef = data as Record<string, unknown>
|
||||||
|
|
||||||
const doc = await req.payloadDataLoader.load(
|
const doc = await req.payloadDataLoader.load(
|
||||||
@@ -45,7 +46,7 @@ export const populate = async ({
|
|||||||
draft,
|
draft,
|
||||||
fallbackLocale: req.fallbackLocale,
|
fallbackLocale: req.fallbackLocale,
|
||||||
locale: req.locale,
|
locale: req.locale,
|
||||||
overrideAccess: typeof overrideAccess === 'undefined' ? false : overrideAccess,
|
overrideAccess,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
transactionID: req.transactionID,
|
transactionID: req.transactionID,
|
||||||
}),
|
}),
|
||||||
@@ -7,7 +7,7 @@ import type { AdapterProps } from '../types.js'
|
|||||||
import { recurseNodes } from '../forEachNodeRecursively.js'
|
import { recurseNodes } from '../forEachNodeRecursively.js'
|
||||||
|
|
||||||
export type Args = Parameters<
|
export type Args = Parameters<
|
||||||
RichTextAdapter<SerializedEditorState, AdapterProps>['populationPromises']
|
RichTextAdapter<SerializedEditorState, AdapterProps>['graphQLPopulationPromises']
|
||||||
>[0] & {
|
>[0] & {
|
||||||
editorPopulationPromises: Map<string, Array<PopulationPromise>>
|
editorPopulationPromises: Map<string, Array<PopulationPromise>>
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,9 @@ export const populateLexicalPopulationPromises = ({
|
|||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
}: Args) => {
|
}: Args) => {
|
||||||
if (depth <= 0 || currentDepth > depth) {
|
const shouldPopulate = depth && currentDepth <= depth
|
||||||
|
|
||||||
|
if (!shouldPopulate) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ type NestedRichTextFieldsArgs = {
|
|||||||
siblingDoc: Record<string, unknown>
|
siblingDoc: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const recurseNestedFields = ({
|
export const recursivelyPopulateFieldsForGraphQL = ({
|
||||||
context,
|
context,
|
||||||
currentDepth = 0,
|
currentDepth = 0,
|
||||||
data,
|
data,
|
||||||
@@ -60,11 +60,12 @@ export const recurseNestedFields = ({
|
|||||||
global: null, // Pass from core? This is only needed for hooks, so we can leave this null for now
|
global: null, // Pass from core? This is only needed for hooks, so we can leave this null for now
|
||||||
locale: req.locale,
|
locale: req.locale,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
path: [],
|
||||||
populationPromises, // This is not the same as populationPromises passed into this recurseNestedFields. These are just promises resolved at the very end.
|
populationPromises, // This is not the same as populationPromises passed into this recurseNestedFields. These are just promises resolved at the very end.
|
||||||
req,
|
req,
|
||||||
|
schemaPath: [],
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
siblingDoc,
|
siblingDoc,
|
||||||
//triggerAccessControl: false, // TODO: Enable this to support access control
|
triggerHooks: false,
|
||||||
//triggerHooks: false, // TODO: Enable this to support hooks
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
45
packages/richtext-lexical/src/recurseNodeTree.ts
Normal file
45
packages/richtext-lexical/src/recurseNodeTree.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import type { SerializedLexicalNode } from 'lexical'
|
||||||
|
|
||||||
|
// Initialize both flattenedNodes and nodeIDMap
|
||||||
|
export const recurseNodeTree = ({
|
||||||
|
flattenedNodes,
|
||||||
|
nodeIDMap,
|
||||||
|
nodes,
|
||||||
|
}: {
|
||||||
|
flattenedNodes?: SerializedLexicalNode[]
|
||||||
|
nodeIDMap?: {
|
||||||
|
[key: string]: SerializedLexicalNode
|
||||||
|
}
|
||||||
|
nodes: SerializedLexicalNode[]
|
||||||
|
}): void => {
|
||||||
|
if (!nodes?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (flattenedNodes) {
|
||||||
|
flattenedNodes.push(node)
|
||||||
|
}
|
||||||
|
if (nodeIDMap) {
|
||||||
|
if (node && 'id' in node && node.id) {
|
||||||
|
nodeIDMap[node.id as string] = node
|
||||||
|
} else if (
|
||||||
|
'fields' in node &&
|
||||||
|
typeof node.fields === 'object' &&
|
||||||
|
node.fields &&
|
||||||
|
'id' in node.fields &&
|
||||||
|
node?.fields?.id
|
||||||
|
) {
|
||||||
|
nodeIDMap[node.fields.id as string] = node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('children' in node && Array.isArray(node?.children) && node?.children?.length) {
|
||||||
|
recurseNodeTree({
|
||||||
|
flattenedNodes,
|
||||||
|
nodeIDMap,
|
||||||
|
nodes: node.children as SerializedLexicalNode[],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,9 @@ 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>['populationPromises']>[0]
|
export type Args = Parameters<
|
||||||
|
RichTextAdapter<any[], AdapterArguments>['graphQLPopulationPromises']
|
||||||
|
>[0]
|
||||||
|
|
||||||
type RecurseRichTextArgs = {
|
type RecurseRichTextArgs = {
|
||||||
children: unknown[]
|
children: unknown[]
|
||||||
|
|||||||
@@ -52,15 +52,7 @@ export function slateEditor(
|
|||||||
FieldComponent: RichTextField,
|
FieldComponent: RichTextField,
|
||||||
generateComponentMap: getGenerateComponentMap(args),
|
generateComponentMap: getGenerateComponentMap(args),
|
||||||
generateSchemaMap: getGenerateSchemaMap(args),
|
generateSchemaMap: getGenerateSchemaMap(args),
|
||||||
outputSchema: ({ isRequired }) => {
|
graphQLPopulationPromises({
|
||||||
return {
|
|
||||||
type: withNullableJSONSchemaType('array', isRequired),
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
populationPromises({
|
|
||||||
context,
|
context,
|
||||||
currentDepth,
|
currentDepth,
|
||||||
depth,
|
depth,
|
||||||
@@ -98,6 +90,58 @@ export function slateEditor(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
hooks: {
|
||||||
|
afterRead: [
|
||||||
|
({
|
||||||
|
context: _context,
|
||||||
|
currentDepth,
|
||||||
|
depth,
|
||||||
|
draft,
|
||||||
|
field: _field,
|
||||||
|
fieldPromises,
|
||||||
|
findMany,
|
||||||
|
flattenLocales,
|
||||||
|
overrideAccess,
|
||||||
|
populationPromises,
|
||||||
|
req,
|
||||||
|
showHiddenFields,
|
||||||
|
siblingData,
|
||||||
|
}) => {
|
||||||
|
const context: any = _context
|
||||||
|
const field = _field as any
|
||||||
|
if (
|
||||||
|
field.admin?.elements?.includes('relationship') ||
|
||||||
|
field.admin?.elements?.includes('upload') ||
|
||||||
|
field.admin?.elements?.includes('link') ||
|
||||||
|
!field?.admin?.elements
|
||||||
|
) {
|
||||||
|
richTextRelationshipPromise({
|
||||||
|
context,
|
||||||
|
currentDepth,
|
||||||
|
depth,
|
||||||
|
draft,
|
||||||
|
field,
|
||||||
|
fieldPromises,
|
||||||
|
findMany,
|
||||||
|
flattenLocales,
|
||||||
|
overrideAccess,
|
||||||
|
populationPromises,
|
||||||
|
req,
|
||||||
|
showHiddenFields,
|
||||||
|
siblingDoc: siblingData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
outputSchema: ({ isRequired }) => {
|
||||||
|
return {
|
||||||
|
type: withNullableJSONSchemaType('array', isRequired),
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
validate: richTextValidate,
|
validate: richTextValidate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -920,10 +920,9 @@ describe('lexicalBlocks', () => {
|
|||||||
await wait(300)
|
await wait(300)
|
||||||
|
|
||||||
await page.click('#action-save', { delay: 100 })
|
await page.click('#action-save', { delay: 100 })
|
||||||
await wait(300)
|
|
||||||
|
|
||||||
await expect(page.locator('.payload-toast-container')).toContainText(
|
await expect(page.locator('.payload-toast-container')).toContainText(
|
||||||
'The following field is invalid',
|
'The following fields are invalid',
|
||||||
)
|
)
|
||||||
await wait(300)
|
await wait(300)
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export function generateLexicalRichText() {
|
|||||||
format: '',
|
format: '',
|
||||||
type: 'upload',
|
type: 'upload',
|
||||||
version: 2,
|
version: 2,
|
||||||
|
id: '665d105a91e1c337ba8308dd',
|
||||||
fields: {
|
fields: {
|
||||||
caption: {
|
caption: {
|
||||||
root: {
|
root: {
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
export function generateLexicalLocalizedRichText(text1: string, text2: string, blockID?: string) {
|
||||||
|
return {
|
||||||
|
root: {
|
||||||
|
type: 'root',
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
version: 1,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
detail: 0,
|
||||||
|
format: 0,
|
||||||
|
mode: 'normal',
|
||||||
|
style: '',
|
||||||
|
text: text1,
|
||||||
|
type: 'text',
|
||||||
|
version: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
direction: 'ltr',
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
type: 'paragraph',
|
||||||
|
version: 1,
|
||||||
|
textFormat: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: '',
|
||||||
|
type: 'block',
|
||||||
|
version: 2,
|
||||||
|
fields: {
|
||||||
|
id: blockID ?? '66685716795f191f08367b1a',
|
||||||
|
blockName: '',
|
||||||
|
textLocalized: text2,
|
||||||
|
counter: 1,
|
||||||
|
blockType: 'block',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
direction: 'ltr',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,11 +21,50 @@ export const LexicalLocalizedFields: CollectionConfig = {
|
|||||||
localized: true,
|
localized: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'lexicalSimple',
|
name: 'lexicalBlocksSubLocalized',
|
||||||
type: 'richText',
|
type: 'richText',
|
||||||
localized: true,
|
admin: {
|
||||||
|
description: 'Non-localized field with localized block subfields',
|
||||||
|
},
|
||||||
editor: lexicalEditor({
|
editor: lexicalEditor({
|
||||||
features: ({ defaultFeatures }) => [...defaultFeatures],
|
features: ({ defaultFeatures }) => [
|
||||||
|
...defaultFeatures,
|
||||||
|
BlocksFeature({
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
slug: 'block',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'textLocalized',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'counter',
|
||||||
|
type: 'number',
|
||||||
|
hooks: {
|
||||||
|
beforeChange: [
|
||||||
|
({ value }) => {
|
||||||
|
return value ? value + 1 : 1
|
||||||
|
},
|
||||||
|
],
|
||||||
|
afterRead: [
|
||||||
|
({ value }) => {
|
||||||
|
return value ? value * 10 : 10
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rel',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: lexicalLocalizedFieldsSlug,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -60,36 +99,5 @@ export const LexicalLocalizedFields: CollectionConfig = {
|
|||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export function generateLexicalRichText() {
|
|||||||
direction: 'ltr',
|
direction: 'ltr',
|
||||||
format: '',
|
format: '',
|
||||||
indent: 0,
|
indent: 0,
|
||||||
|
id: '665d10938106ab380c7f3730',
|
||||||
type: 'link',
|
type: 'link',
|
||||||
version: 2,
|
version: 2,
|
||||||
fields: {
|
fields: {
|
||||||
@@ -86,6 +87,7 @@ export function generateLexicalRichText() {
|
|||||||
direction: 'ltr',
|
direction: 'ltr',
|
||||||
format: '',
|
format: '',
|
||||||
indent: 0,
|
indent: 0,
|
||||||
|
id: '665d10938106ab380c7f3730',
|
||||||
type: 'link',
|
type: 'link',
|
||||||
version: 2,
|
version: 2,
|
||||||
fields: {
|
fields: {
|
||||||
@@ -230,6 +232,7 @@ export function generateLexicalRichText() {
|
|||||||
format: '',
|
format: '',
|
||||||
type: 'upload',
|
type: 'upload',
|
||||||
version: 2,
|
version: 2,
|
||||||
|
id: '665d10938106ab380c7f372f',
|
||||||
relationTo: 'uploads',
|
relationTo: 'uploads',
|
||||||
value: '{{UPLOAD_DOC_ID}}',
|
value: '{{UPLOAD_DOC_ID}}',
|
||||||
fields: {
|
fields: {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { devUser } from '../credentials.js'
|
|||||||
import { NextRESTClient } from '../helpers/NextRESTClient.js'
|
import { NextRESTClient } from '../helpers/NextRESTClient.js'
|
||||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||||
import { lexicalDocData } from './collections/Lexical/data.js'
|
import { lexicalDocData } from './collections/Lexical/data.js'
|
||||||
|
import { generateLexicalLocalizedRichText } from './collections/LexicalLocalized/generateLexicalRichText.js'
|
||||||
|
import { textToLexicalJSON } from './collections/LexicalLocalized/textToLexicalJSON.js'
|
||||||
import { lexicalMigrateDocData } from './collections/LexicalMigrate/data.js'
|
import { lexicalMigrateDocData } from './collections/LexicalMigrate/data.js'
|
||||||
import { richTextDocData } from './collections/RichText/data.js'
|
import { richTextDocData } from './collections/RichText/data.js'
|
||||||
import { generateLexicalRichText } from './collections/RichText/generateLexicalRichText.js'
|
import { generateLexicalRichText } from './collections/RichText/generateLexicalRichText.js'
|
||||||
@@ -569,4 +571,100 @@ describe('Lexical', () => {
|
|||||||
expect(populatedDocEditorRelationshipNode.value.text).toStrictEqual(textDoc.text)
|
expect(populatedDocEditorRelationshipNode.value.text).toStrictEqual(textDoc.text)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Localization', () => {
|
||||||
|
it('ensure localized lexical field is different across locales', async () => {
|
||||||
|
const lexicalDocEN = await payload.find({
|
||||||
|
collection: 'lexical-localized-fields',
|
||||||
|
locale: 'en',
|
||||||
|
where: {
|
||||||
|
title: {
|
||||||
|
equals: 'Localized Lexical en',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(lexicalDocEN.docs[0].lexicalBlocksLocalized.root.children[0].children[0].text).toEqual(
|
||||||
|
'English text',
|
||||||
|
)
|
||||||
|
|
||||||
|
const lexicalDocES = await payload.findByID({
|
||||||
|
collection: 'lexical-localized-fields',
|
||||||
|
locale: 'es',
|
||||||
|
id: lexicalDocEN.docs[0].id,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(lexicalDocES.lexicalBlocksLocalized.root.children[0].children[0].text).toEqual(
|
||||||
|
'Spanish text',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ensure localized text field within blocks field within unlocalized lexical field is different across locales', async () => {
|
||||||
|
const lexicalDocEN = await payload.find({
|
||||||
|
collection: 'lexical-localized-fields',
|
||||||
|
locale: 'en',
|
||||||
|
where: {
|
||||||
|
title: {
|
||||||
|
equals: 'Localized Lexical en',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(
|
||||||
|
lexicalDocEN.docs[0].lexicalBlocksSubLocalized.root.children[0].children[0].text,
|
||||||
|
).toEqual('Shared text')
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(lexicalDocEN.docs[0].lexicalBlocksSubLocalized.root.children[1].fields as any)
|
||||||
|
.textLocalized,
|
||||||
|
).toEqual('English text in block')
|
||||||
|
|
||||||
|
const lexicalDocES = await payload.findByID({
|
||||||
|
collection: 'lexical-localized-fields',
|
||||||
|
locale: 'es',
|
||||||
|
id: lexicalDocEN.docs[0].id,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(lexicalDocES.lexicalBlocksSubLocalized.root.children[0].children[0].text).toEqual(
|
||||||
|
'Shared text',
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(lexicalDocES.lexicalBlocksSubLocalized.root.children[1].fields as any).textLocalized,
|
||||||
|
).toEqual('Spanish text in block')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Hooks', () => {
|
||||||
|
it('ensure hook within number field within lexical block runs', async () => {
|
||||||
|
const lexicalDocEN = await payload.create({
|
||||||
|
collection: 'lexical-localized-fields',
|
||||||
|
locale: 'en',
|
||||||
|
data: {
|
||||||
|
title: 'Localized Lexical hooks',
|
||||||
|
lexicalBlocksLocalized: textToLexicalJSON({ text: 'some text' }) as any,
|
||||||
|
lexicalBlocksSubLocalized: generateLexicalLocalizedRichText(
|
||||||
|
'Shared text',
|
||||||
|
'English text in block',
|
||||||
|
) as any,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(lexicalDocEN.lexicalBlocksSubLocalized.root.children[1].fields as any).counter,
|
||||||
|
).toEqual(20) // Initial: 1. BeforeChange: +1 (2). AfterRead: *10 (20)
|
||||||
|
|
||||||
|
// update document with same data
|
||||||
|
const lexicalDocENUpdated = await payload.update({
|
||||||
|
collection: 'lexical-localized-fields',
|
||||||
|
locale: 'en',
|
||||||
|
id: lexicalDocEN.id,
|
||||||
|
data: lexicalDocEN,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(lexicalDocENUpdated.lexicalBlocksSubLocalized.root.children[1].fields as any).counter,
|
||||||
|
).toEqual(210) // Initial: 20. BeforeChange: +1 (21). AfterRead: *10 (210)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ export interface LexicalMigrateField {
|
|||||||
export interface LexicalLocalizedField {
|
export interface LexicalLocalizedField {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
lexicalSimple?: {
|
lexicalBlocksSubLocalized?: {
|
||||||
root: {
|
root: {
|
||||||
type: string;
|
type: string;
|
||||||
children: {
|
children: {
|
||||||
@@ -237,21 +237,6 @@ export interface LexicalLocalizedField {
|
|||||||
};
|
};
|
||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
} | null;
|
} | null;
|
||||||
lexicalBlocksSubLocalized?: {
|
|
||||||
root: {
|
|
||||||
type: string;
|
|
||||||
children: {
|
|
||||||
type: string;
|
|
||||||
version: number;
|
|
||||||
[k: string]: unknown;
|
|
||||||
}[];
|
|
||||||
direction: ('ltr' | 'rtl') | null;
|
|
||||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
|
||||||
indent: number;
|
|
||||||
version: number;
|
|
||||||
};
|
|
||||||
[k: string]: unknown;
|
|
||||||
} | null;
|
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { generateLexicalLocalizedRichText } from './collections/LexicalLocalized/generateLexicalRichText.js'
|
||||||
import { textToLexicalJSON } from './collections/LexicalLocalized/textToLexicalJSON.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'
|
||||||
@@ -281,9 +282,11 @@ export const seed = async (_payload: Payload) => {
|
|||||||
collection: lexicalLocalizedFieldsSlug,
|
collection: lexicalLocalizedFieldsSlug,
|
||||||
data: {
|
data: {
|
||||||
title: 'Localized Lexical en',
|
title: 'Localized Lexical en',
|
||||||
lexicalSimple: textToLexicalJSON({ text: 'English text' }),
|
lexicalBlocksLocalized: textToLexicalJSON({ text: 'English text' }) as any,
|
||||||
lexicalBlocksLocalized: textToLexicalJSON({ text: 'English text' }),
|
lexicalBlocksSubLocalized: generateLexicalLocalizedRichText(
|
||||||
lexicalBlocksSubLocalized: textToLexicalJSON({ text: 'English text' }),
|
'Shared text',
|
||||||
|
'English text in block',
|
||||||
|
) as any,
|
||||||
},
|
},
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
depth: 0,
|
depth: 0,
|
||||||
@@ -295,9 +298,12 @@ export const seed = async (_payload: Payload) => {
|
|||||||
id: lexicalLocalizedDoc1.id,
|
id: lexicalLocalizedDoc1.id,
|
||||||
data: {
|
data: {
|
||||||
title: 'Localized Lexical es',
|
title: 'Localized Lexical es',
|
||||||
lexicalSimple: textToLexicalJSON({ text: 'Spanish text' }),
|
lexicalBlocksLocalized: textToLexicalJSON({ text: 'Spanish text' }) as any,
|
||||||
lexicalBlocksLocalized: textToLexicalJSON({ text: 'Spanish text' }),
|
lexicalBlocksSubLocalized: generateLexicalLocalizedRichText(
|
||||||
lexicalBlocksSubLocalized: textToLexicalJSON({ text: 'Spanish text' }),
|
'Shared text',
|
||||||
|
'Spanish text in block',
|
||||||
|
(lexicalLocalizedDoc1.lexicalBlocksSubLocalized.root.children[1].fields as any).id,
|
||||||
|
) as any,
|
||||||
},
|
},
|
||||||
locale: 'es',
|
locale: 'es',
|
||||||
depth: 0,
|
depth: 0,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { NestedAfterReadHook } from './payload-types.js'
|
|||||||
|
|
||||||
import { devUser, regularUser } from '../credentials.js'
|
import { devUser, regularUser } from '../credentials.js'
|
||||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||||
|
import { isMongoose } from '../helpers/isMongoose.js'
|
||||||
import { afterOperationSlug } from './collections/AfterOperation/index.js'
|
import { afterOperationSlug } from './collections/AfterOperation/index.js'
|
||||||
import { chainingHooksSlug } from './collections/ChainingHooks/index.js'
|
import { chainingHooksSlug } from './collections/ChainingHooks/index.js'
|
||||||
import { contextHooksSlug } from './collections/ContextHooks/index.js'
|
import { contextHooksSlug } from './collections/ContextHooks/index.js'
|
||||||
@@ -35,22 +36,23 @@ describe('Hooks', () => {
|
|||||||
await payload.db.destroy()
|
await payload.db.destroy()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if (isMongoose(payload)) {
|
||||||
|
describe('transform actions', () => {
|
||||||
|
it('should create and not throw an error', async () => {
|
||||||
|
// the collection has hooks that will cause an error if transform actions is not handled properly
|
||||||
|
const doc = await payload.create({
|
||||||
|
collection: transformSlug,
|
||||||
|
data: {
|
||||||
|
localizedTransform: [2, 8],
|
||||||
|
transform: [2, 8],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
describe('transform actions', () => {
|
expect(doc.transform).toBeDefined()
|
||||||
it('should create and not throw an error', async () => {
|
expect(doc.localizedTransform).toBeDefined()
|
||||||
// the collection has hooks that will cause an error if transform actions is not handled properly
|
|
||||||
const doc = await payload.create({
|
|
||||||
collection: transformSlug,
|
|
||||||
data: {
|
|
||||||
localizedTransform: [2, 8],
|
|
||||||
transform: [2, 8],
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(doc.transform).toBeDefined()
|
|
||||||
expect(doc.localizedTransform).toBeDefined()
|
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
describe('hook execution', () => {
|
describe('hook execution', () => {
|
||||||
let doc
|
let doc
|
||||||
|
|||||||
Reference in New Issue
Block a user