feat: add siblingFields arg to field hooks (#11117)

This PR adds a new `siblingFields` argument to field hooks. This allows
us to dramatically simplify the `lexicalHTML` field, which previously
had to use a complex `findFieldPathAndSiblingFields` function that
deeply traverses the entire `CollectionConfig` just to find the sibling
fields.
This commit is contained in:
Alessio Gravili
2025-02-11 14:22:31 -07:00
committed by GitHub
parent 2056e9b740
commit 155f9f80fe
13 changed files with 34 additions and 65 deletions

View File

@@ -71,6 +71,8 @@ The following arguments are provided to all Field Hooks:
| **`schemaPath`** | The path of the [Field](../fields/overview) in the schema. |
| **`siblingData`** | The data of sibling fields adjacent to the field that the Hook is running against. |
| **`siblingDocWithLocales`** | The sibling data of the Document with all [Locales](../configuration/localization). |
| **`siblingFields`** | The sibling fields of the field which the hook is running against.
|
| **`value`** | The value of the [Field](../fields/overview). |
<Banner type="success">

View File

@@ -164,7 +164,8 @@ export type FieldHookArgs<TData extends TypeWithID = any, TValue = any, TSibling
/**
* Only available in `afterRead` hooks
*/
currentDepth?: number /**
currentDepth?: number
/**
* Only available in `afterRead` hooks
*/
/** The data passed to update the document within create and update operations, and the full document itself in the afterRead hook. */
@@ -212,6 +213,10 @@ export type FieldHookArgs<TData extends TypeWithID = any, TValue = any, TSibling
* The original siblingData with locales (not modified by any hooks). Only available in `beforeChange` and `beforeDuplicate` field hooks.
*/
siblingDocWithLocales?: Record<string, unknown>
/**
* The sibling fields of the field which the hook is running against.
*/
siblingFields: (Field | TabAsField)[]
/** The value of the field. */
value?: TValue
}

View File

@@ -31,6 +31,7 @@ type Args = {
req: PayloadRequest
siblingData: JsonObject
siblingDoc: JsonObject
siblingFields?: (Field | TabAsField)[]
}
// This function is responsible for the following actions, in order:
@@ -54,6 +55,7 @@ export const promise = async ({
req,
siblingData,
siblingDoc,
siblingFields,
}: Args): Promise<void> => {
const { indexPath, path, schemaPath } = getFieldPaths({
field,
@@ -90,6 +92,7 @@ export const promise = async ({
req,
schemaPath: schemaPathSegments,
siblingData,
siblingFields,
value: siblingDoc[field.name],
})

View File

@@ -26,6 +26,7 @@ type Args = {
req: PayloadRequest
siblingData: JsonObject
siblingDoc: JsonObject
siblingFields?: (Field | TabAsField)[]
}
export const traverseFields = async ({
@@ -45,6 +46,7 @@ export const traverseFields = async ({
req,
siblingData,
siblingDoc,
siblingFields,
}: Args): Promise<void> => {
const promises = []
@@ -68,6 +70,7 @@ export const traverseFields = async ({
req,
siblingData,
siblingDoc,
siblingFields,
}),
)
})

View File

@@ -51,6 +51,7 @@ type Args = {
selectMode?: SelectMode
showHiddenFields: boolean
siblingDoc: JsonObject
siblingFields?: (Field | TabAsField)[]
triggerAccessControl?: boolean
triggerHooks?: boolean
}
@@ -90,6 +91,7 @@ export const promise = async ({
selectMode,
showHiddenFields,
siblingDoc,
siblingFields,
triggerAccessControl = true,
triggerHooks = true,
}: Args): Promise<void> => {
@@ -260,6 +262,7 @@ export const promise = async ({
schemaPath: schemaPathSegments,
showHiddenFields,
siblingData: siblingDoc,
siblingFields,
value,
})
@@ -291,6 +294,7 @@ export const promise = async ({
schemaPath: schemaPathSegments,
showHiddenFields,
siblingData: siblingDoc,
siblingFields,
value: siblingDoc[field.name],
})

View File

@@ -106,6 +106,7 @@ export const traverseFields = ({
selectMode,
showHiddenFields,
siblingDoc,
siblingFields: fields,
triggerAccessControl,
triggerHooks,
}),

View File

@@ -39,6 +39,7 @@ type Args = {
siblingData: JsonObject
siblingDoc: JsonObject
siblingDocWithLocales?: JsonObject
siblingFields?: (Field | TabAsField)[]
skipValidation: boolean
}
@@ -71,6 +72,7 @@ export const promise = async ({
siblingData,
siblingDoc,
siblingDocWithLocales,
siblingFields,
skipValidation,
}: Args): Promise<void> => {
const { indexPath, path, schemaPath } = getFieldPaths({
@@ -123,6 +125,7 @@ export const promise = async ({
schemaPath: schemaPathSegments,
siblingData,
siblingDocWithLocales,
siblingFields,
value: siblingData[field.name],
})

View File

@@ -100,6 +100,7 @@ export const traverseFields = async ({
siblingData,
siblingDoc,
siblingDocWithLocales,
siblingFields: fields,
skipValidation,
}),
)

View File

@@ -25,6 +25,7 @@ type Args<T> = {
parentSchemaPath: string
req: PayloadRequest
siblingDoc: JsonObject
siblingFields?: (Field | TabAsField)[]
}
export const promise = async <T>({
@@ -41,6 +42,7 @@ export const promise = async <T>({
parentSchemaPath,
req,
siblingDoc,
siblingFields,
}: Args<T>): Promise<void> => {
const { indexPath, path, schemaPath } = getFieldPaths({
field,
@@ -82,6 +84,7 @@ export const promise = async <T>({
schemaPath: schemaPathSegments,
siblingData: siblingDoc,
siblingDocWithLocales: siblingDoc,
siblingFields,
value: siblingDoc[field.name]?.[locale],
}
@@ -116,6 +119,7 @@ export const promise = async <T>({
schemaPath: schemaPathSegments,
siblingData: siblingDoc,
siblingDocWithLocales: siblingDoc,
siblingFields,
value: siblingDoc[field.name],
}

View File

@@ -55,6 +55,7 @@ export const traverseFields = async <T>({
parentSchemaPath,
req,
siblingDoc,
siblingFields: fields,
}),
)
})

View File

@@ -40,6 +40,7 @@ type Args<T> = {
* The original siblingData (not modified by any hooks)
*/
siblingDoc: JsonObject
siblingFields?: (Field | TabAsField)[]
}
// This function is responsible for the following actions, in order:
@@ -67,6 +68,7 @@ export const promise = async <T>({
req,
siblingData,
siblingDoc,
siblingFields,
}: Args<T>): Promise<void> => {
const { indexPath, path, schemaPath } = getFieldPaths({
field,
@@ -291,6 +293,7 @@ export const promise = async <T>({
req,
schemaPath: schemaPathSegments,
siblingData,
siblingFields,
value: siblingData[field.name],
})

View File

@@ -74,6 +74,7 @@ export const traverseFields = async <T>({
req,
siblingData,
siblingDoc,
siblingFields: fields,
}),
)
})

View File

@@ -83,62 +83,6 @@ export const consolidateHTMLConverters = ({
return filteredConverters
}
// find the path of this field, as well as its sibling fields, by looking for this `field` in fields and traversing it recursively
function findFieldPathAndSiblingFields(
fields: Field[],
path: string[],
field: FieldAffectingData,
): {
path: string[]
siblingFields: Field[]
} | null {
for (const curField of fields) {
if (curField === field) {
return {
path: [...path, curField.name],
siblingFields: fields,
}
}
if ('fields' in curField) {
const result = findFieldPathAndSiblingFields(
curField.fields,
'name' in curField ? [...path, curField.name] : [...path],
field,
)
if (result) {
return result
}
} else if ('tabs' in curField) {
for (const tab of curField.tabs) {
const result = findFieldPathAndSiblingFields(
tab.fields,
'name' in tab ? [...path, tab.name] : [...path],
field,
)
if (result) {
return result
}
}
} else if ('blocks' in curField) {
for (const block of curField.blocks) {
if (block?.fields?.length) {
const result = findFieldPathAndSiblingFields(
block.fields,
[...path, curField.name, block.slug],
field,
)
if (result) {
return result
}
}
}
}
}
return null
}
export const lexicalHTML: (
/**
* A string which matches the lexical field name you want to convert to HTML.
@@ -161,28 +105,22 @@ export const lexicalHTML: (
hooks: {
afterRead: [
async ({
collection,
currentDepth,
depth,
draft,
field,
global,
overrideAccess,
req,
showHiddenFields,
siblingData,
siblingFields,
}) => {
const fields = collection ? collection.fields : global!.fields
const foundSiblingFields = findFieldPathAndSiblingFields(fields, [], field)
if (!foundSiblingFields) {
if (!siblingFields) {
throw new Error(
`Could not find sibling fields of current lexicalHTML field with name ${field?.name}`,
)
}
const { siblingFields } = foundSiblingFields
const lexicalField: RichTextField<SerializedEditorState, AdapterProps> =
siblingFields.find(
(field) => 'name' in field && field.name === lexicalFieldName,