fix: presentational-field types incorrectly exposing hooks (#11514)
### What? Presentational Fields such as [Row](https://payloadcms.com/docs/fields/row) are described as only effecting the admin panel. If they do not impact data, their types should not include hooks in the fields config. ### Why? Developers can currently assign hooks to these fields, expecting them to work, when in reality they are not called. ### How? Omit `hooks` from `FieldBase` Fixes #11507 --------- Co-authored-by: German Jablonski <43938777+GermanJablo@users.noreply.github.com>
This commit is contained in:
@@ -95,7 +95,8 @@ Here are the available Presentational Fields:
|
||||
|
||||
- [Collapsible](../fields/collapsible) - nests fields within a collapsible component
|
||||
- [Row](../fields/row) - aligns fields horizontally
|
||||
- [Tabs (Unnamed)](../fields/tabs) - nests fields within a tabbed layout
|
||||
- [Tabs (Unnamed)](../fields/tabs) - nests fields within a tabbed layout. It is not presentational if the tab has a name.
|
||||
- [Group (Unnamed)](../fields/group) - nests fields within a keyed object It is not presentational if the group has a name.
|
||||
- [UI](../fields/ui) - blank field for custom UI components
|
||||
|
||||
### Virtual Fields
|
||||
|
||||
@@ -753,7 +753,7 @@ export type NamedGroupField = {
|
||||
export type UnnamedGroupField = {
|
||||
interfaceName?: never
|
||||
localized?: never
|
||||
} & Omit<GroupBase, 'name' | 'virtual'>
|
||||
} & Omit<GroupBase, 'hooks' | 'name' | 'virtual'>
|
||||
|
||||
export type GroupField = NamedGroupField | UnnamedGroupField
|
||||
|
||||
@@ -777,7 +777,7 @@ export type RowField = {
|
||||
admin?: Omit<Admin, 'description'>
|
||||
fields: Field[]
|
||||
type: 'row'
|
||||
} & Omit<FieldBase, 'admin' | 'label' | 'localized' | 'name' | 'validate' | 'virtual'>
|
||||
} & Omit<FieldBase, 'admin' | 'hooks' | 'label' | 'localized' | 'name' | 'validate' | 'virtual'>
|
||||
|
||||
export type RowFieldClient = {
|
||||
admin?: Omit<AdminClient, 'description'>
|
||||
@@ -816,7 +816,7 @@ export type CollapsibleField = {
|
||||
label: Required<FieldBase['label']>
|
||||
}
|
||||
) &
|
||||
Omit<FieldBase, 'label' | 'localized' | 'name' | 'validate' | 'virtual'>
|
||||
Omit<FieldBase, 'hooks' | 'label' | 'localized' | 'name' | 'validate' | 'virtual'>
|
||||
|
||||
export type CollapsibleFieldClient = {
|
||||
admin?: {
|
||||
@@ -863,7 +863,7 @@ export type UnnamedTab = {
|
||||
| LabelFunction
|
||||
| string
|
||||
localized?: never
|
||||
} & Omit<TabBase, 'name' | 'virtual'>
|
||||
} & Omit<TabBase, 'hooks' | 'name' | 'virtual'>
|
||||
|
||||
export type Tab = NamedTab | UnnamedTab
|
||||
export type TabsField = {
|
||||
|
||||
@@ -73,7 +73,7 @@ export const promise = async ({
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
// Execute hooks
|
||||
if (field.hooks?.afterChange) {
|
||||
if ('hooks' in field && field.hooks?.afterChange) {
|
||||
for (const hook of field.hooks.afterChange) {
|
||||
const hookedValue = await hook({
|
||||
blockData,
|
||||
@@ -88,16 +88,16 @@ export const promise = async ({
|
||||
path: pathSegments,
|
||||
previousDoc,
|
||||
previousSiblingDoc,
|
||||
previousValue: previousDoc?.[field.name!],
|
||||
previousValue: previousDoc?.[field.name],
|
||||
req,
|
||||
schemaPath: schemaPathSegments,
|
||||
siblingData,
|
||||
siblingFields: siblingFields!,
|
||||
value: siblingDoc?.[field.name!],
|
||||
value: siblingDoc?.[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingDoc[field.name!] = hookedValue
|
||||
siblingDoc[field.name] = hookedValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,15 +238,15 @@ export const promise = async ({
|
||||
|
||||
if (fieldAffectsDataResult) {
|
||||
// Execute hooks
|
||||
if (triggerHooks && field.hooks?.afterRead) {
|
||||
if (triggerHooks && 'hooks' in field && field.hooks?.afterRead) {
|
||||
for (const hook of field.hooks.afterRead) {
|
||||
const shouldRunHookOnAllLocales =
|
||||
fieldShouldBeLocalized({ field, parentIsLocalized: parentIsLocalized! }) &&
|
||||
(locale === 'all' || !flattenLocales) &&
|
||||
typeof siblingDoc[field.name!] === 'object'
|
||||
typeof siblingDoc[field.name] === 'object'
|
||||
|
||||
if (shouldRunHookOnAllLocales) {
|
||||
const localesAndValues = Object.entries(siblingDoc[field.name!])
|
||||
const localesAndValues = Object.entries(siblingDoc[field.name])
|
||||
await Promise.all(
|
||||
localesAndValues.map(async ([localeKey, value]) => {
|
||||
const hookedValue = await hook({
|
||||
@@ -274,7 +274,7 @@ export const promise = async ({
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingDoc[field.name!][localeKey] = hookedValue
|
||||
siblingDoc[field.name][localeKey] = hookedValue
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -300,11 +300,11 @@ export const promise = async ({
|
||||
showHiddenFields,
|
||||
siblingData: siblingDoc,
|
||||
siblingFields: siblingFields!,
|
||||
value: siblingDoc[field.name!],
|
||||
value: siblingDoc[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingDoc[field.name!] = hookedValue
|
||||
siblingDoc[field.name] = hookedValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ export const promise = async ({
|
||||
}
|
||||
|
||||
// Execute hooks
|
||||
if (field.hooks?.beforeChange) {
|
||||
if ('hooks' in field && field.hooks?.beforeChange) {
|
||||
for (const hook of field.hooks.beforeChange) {
|
||||
const hookedValue = await hook({
|
||||
blockData,
|
||||
@@ -143,17 +143,17 @@ export const promise = async ({
|
||||
originalDoc: doc,
|
||||
path: pathSegments,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingDoc[field.name!],
|
||||
previousValue: siblingDoc[field.name],
|
||||
req,
|
||||
schemaPath: schemaPathSegments,
|
||||
siblingData,
|
||||
siblingDocWithLocales,
|
||||
siblingFields: siblingFields!,
|
||||
value: siblingData[field.name!],
|
||||
value: siblingData[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingData[field.name!] = hookedValue
|
||||
siblingData[field.name] = hookedValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export const promise = async <T>({
|
||||
|
||||
// Run field beforeDuplicate hooks.
|
||||
// These hooks are responsible for resetting the `id` field values of array and block rows. See `baseIDField`.
|
||||
if (Array.isArray(field.hooks?.beforeDuplicate)) {
|
||||
if (Array.isArray('hooks' in field && field.hooks?.beforeDuplicate)) {
|
||||
if (fieldIsLocalized) {
|
||||
const localeData: JsonObject = {}
|
||||
|
||||
@@ -90,8 +90,10 @@ export const promise = async <T>({
|
||||
}
|
||||
|
||||
let hookResult
|
||||
for (const hook of field.hooks.beforeDuplicate) {
|
||||
hookResult = await hook(beforeDuplicateArgs)
|
||||
if ('hooks' in field && field.hooks?.beforeDuplicate) {
|
||||
for (const hook of field.hooks.beforeDuplicate) {
|
||||
hookResult = await hook(beforeDuplicateArgs)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof hookResult !== 'undefined') {
|
||||
@@ -121,8 +123,10 @@ export const promise = async <T>({
|
||||
}
|
||||
|
||||
let hookResult
|
||||
for (const hook of field.hooks.beforeDuplicate) {
|
||||
hookResult = await hook(beforeDuplicateArgs)
|
||||
if ('hooks' in field && field.hooks?.beforeDuplicate) {
|
||||
for (const hook of field.hooks.beforeDuplicate) {
|
||||
hookResult = await hook(beforeDuplicateArgs)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof hookResult !== 'undefined') {
|
||||
|
||||
@@ -284,7 +284,7 @@ export const promise = async <T>({
|
||||
}
|
||||
|
||||
// Execute hooks
|
||||
if (field.hooks?.beforeValidate) {
|
||||
if ('hooks' in field && field.hooks?.beforeValidate) {
|
||||
for (const hook of field.hooks.beforeValidate) {
|
||||
const hookedValue = await hook({
|
||||
blockData,
|
||||
@@ -299,19 +299,19 @@ export const promise = async <T>({
|
||||
overrideAccess,
|
||||
path: pathSegments,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingDoc[field.name!],
|
||||
previousValue: siblingDoc[field.name],
|
||||
req,
|
||||
schemaPath: schemaPathSegments,
|
||||
siblingData,
|
||||
siblingFields: siblingFields!,
|
||||
value:
|
||||
typeof siblingData[field.name!] === 'undefined'
|
||||
typeof siblingData[field.name] === 'undefined'
|
||||
? fallbackResult.value
|
||||
: siblingData[field.name!],
|
||||
: siblingData[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingData[field.name!] = hookedValue
|
||||
siblingData[field.name] = hookedValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export const setDefaultBeforeDuplicate = (
|
||||
) => {
|
||||
if (
|
||||
(('required' in field && field.required) || field.unique) &&
|
||||
'hooks' in field &&
|
||||
(!field.hooks?.beforeDuplicate ||
|
||||
(Array.isArray(field.hooks.beforeDuplicate) && field.hooks.beforeDuplicate.length === 0))
|
||||
) {
|
||||
|
||||
@@ -119,7 +119,10 @@ export const getFields = ({
|
||||
generateFileURL,
|
||||
size,
|
||||
}),
|
||||
...(existingSizeURLField?.hooks?.afterRead || []),
|
||||
...((typeof existingSizeURLField === 'object' &&
|
||||
'hooks' in existingSizeURLField &&
|
||||
existingSizeURLField?.hooks?.afterRead) ||
|
||||
[]),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user