fix: field inside an unnamed group field erroring when used as a title (#12771)

Fixes https://github.com/payloadcms/payload/issues/12632

Config sanitisation will error without this PR when attempting to
useAsTitle a field inside an unnamed group field.
This commit is contained in:
Paul
2025-06-12 05:57:37 -07:00
committed by GitHub
parent cf43c5cd08
commit 143aff57ae
5 changed files with 36 additions and 5 deletions

View File

@@ -152,7 +152,7 @@ describe('flattenFields', () => {
// Should return the group as a top-level item, not the inner field // Should return the group as a top-level item, not the inner field
expect(withoutExtract).toHaveLength(1) expect(withoutExtract).toHaveLength(1)
expect(withoutExtract[0].type).toBe('group') expect(withoutExtract[0].type).toBe('text')
expect(withoutExtract[0].accessor).toBeUndefined() expect(withoutExtract[0].accessor).toBeUndefined()
expect(withoutExtract[0].labelWithPrefix).toBeUndefined() expect(withoutExtract[0].labelWithPrefix).toBeUndefined()
}) })
@@ -635,8 +635,8 @@ describe('flattenFields', () => {
const defaultResult = flattenTopLevelFields(unnamedTabWithUnnamedGroup) const defaultResult = flattenTopLevelFields(unnamedTabWithUnnamedGroup)
expect(defaultResult).toHaveLength(1) expect(defaultResult).toHaveLength(1)
expect(defaultResult[0].type).toBe('group') expect(defaultResult[0].type).toBe('text')
expect(defaultResult[0].label).toBe('Unnamed Group In Tab') expect(defaultResult[0].label).toBe('Nested In Unnamed Group')
expect('accessor' in defaultResult[0]).toBe(false) expect('accessor' in defaultResult[0]).toBe(false)
expect('labelWithPrefix' in defaultResult[0]).toBe(false) expect('labelWithPrefix' in defaultResult[0]).toBe(false)

View File

@@ -84,6 +84,7 @@ export function flattenTopLevelFields<TField extends ClientField | Field>(
} = normalizedOptions } = normalizedOptions
return fields.reduce<FlattenedField<TField>[]>((acc, field) => { return fields.reduce<FlattenedField<TField>[]>((acc, field) => {
// If a group field has subfields and has a name, otherwise we catch it below along with collapsible and row fields
if (field.type === 'group' && 'fields' in field) { if (field.type === 'group' && 'fields' in field) {
if (moveSubFieldsToTop) { if (moveSubFieldsToTop) {
const isNamedGroup = 'name' in field && typeof field.name === 'string' && !!field.name const isNamedGroup = 'name' in field && typeof field.name === 'string' && !!field.name
@@ -120,8 +121,12 @@ export function flattenTopLevelFields<TField extends ClientField | Field>(
}), }),
) )
} else { } else {
// Hoisting diabled - keep as top level field if (fieldAffectsData(field)) {
acc.push(field as FlattenedField<TField>) // Hoisting diabled - keep as top level field
acc.push(field as FlattenedField<TField>)
} else {
acc.push(...flattenTopLevelFields(field.fields as TField[], options))
}
} }
} else if (field.type === 'tabs' && 'tabs' in field) { } else if (field.type === 'tabs' && 'tabs' in field) {
return [ return [

View File

@@ -0,0 +1,22 @@
import type { CollectionConfig } from 'payload'
import { useAsTitleGroupFieldSlug } from '../slugs.js'
export const UseAsTitleGroupField: CollectionConfig = {
slug: useAsTitleGroupFieldSlug,
admin: {
useAsTitle: 'name',
},
fields: [
{
type: 'group',
label: 'unnamed group',
fields: [
{
name: 'name',
type: 'text',
},
],
},
],
}

View File

@@ -23,6 +23,7 @@ import { Placeholder } from './collections/Placeholder.js'
import { Posts } from './collections/Posts.js' import { Posts } from './collections/Posts.js'
import { UploadCollection } from './collections/Upload.js' import { UploadCollection } from './collections/Upload.js'
import { UploadTwoCollection } from './collections/UploadTwo.js' import { UploadTwoCollection } from './collections/UploadTwo.js'
import { UseAsTitleGroupField } from './collections/UseAsTitleGroupField.js'
import { Users } from './collections/Users.js' import { Users } from './collections/Users.js'
import { with300Documents } from './collections/With300Documents.js' import { with300Documents } from './collections/With300Documents.js'
import { CustomGlobalViews1 } from './globals/CustomViews1.js' import { CustomGlobalViews1 } from './globals/CustomViews1.js'
@@ -174,6 +175,7 @@ export default buildConfigWithDefaults({
with300Documents, with300Documents,
ListDrawer, ListDrawer,
Placeholder, Placeholder,
UseAsTitleGroupField,
], ],
globals: [ globals: [
GlobalHidden, GlobalHidden,

View File

@@ -8,6 +8,8 @@ export const group1Collection1Slug = 'group-one-collection-ones'
export const group1Collection2Slug = 'group-one-collection-twos' export const group1Collection2Slug = 'group-one-collection-twos'
export const group2Collection1Slug = 'group-two-collection-ones' export const group2Collection1Slug = 'group-two-collection-ones'
export const group2Collection2Slug = 'group-two-collection-twos' export const group2Collection2Slug = 'group-two-collection-twos'
export const useAsTitleGroupFieldSlug = 'use-as-title-group-field'
export const hiddenCollectionSlug = 'hidden-collection' export const hiddenCollectionSlug = 'hidden-collection'
export const notInViewCollectionSlug = 'not-in-view-collection' export const notInViewCollectionSlug = 'not-in-view-collection'
export const noApiViewCollectionSlug = 'collection-no-api-view' export const noApiViewCollectionSlug = 'collection-no-api-view'