fix: allow unnamed group fields to not set a label at all (#12580)

Technically you could already set `label: undefined` and it would be
supported by group fields but the types didn't reflect this.

So now you can create an unnamed group field like this:

```ts
{
      type: 'group',
      fields: [
        {
          type: 'text',
          name: 'insideGroupWithNoLabel',
        },
      ],
    },
```

This will remove the label while still visually grouping the fields.

![image](https://github.com/user-attachments/assets/ecb0b364-9cff-4d71-bf9f-86961915aecd)

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
This commit is contained in:
Paul
2025-05-29 15:03:39 -07:00
committed by GitHub
parent 71df378fb0
commit d5611953a7
5 changed files with 34 additions and 14 deletions

View File

@@ -37,7 +37,7 @@ export const MyGroupField: Field = {
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) | | **`name`** | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`fields`** \* | Array of field types to nest within this Group. | | **`fields`** \* | Array of field types to nest within this Group. |
| **`label`** | Used as a heading in the Admin Panel and to name the generated GraphQL type. Required when name is undefined, defaults to name converted to words. | | **`label`** | Used as a heading in the Admin Panel and to name the generated GraphQL type. Defaults to the field name, if defined. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) | | **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
@@ -113,8 +113,7 @@ export const ExampleCollection: CollectionConfig = {
## Presentational group fields ## Presentational group fields
You can also use the Group field to create a presentational group of fields. This is useful when you want to group fields together visually without affecting the data structure. You can also use the Group field to only visually group fields without affecting the data structure. Not defining a label will render just the grouped fields.
The label will be required when a `name` is not provided.
```ts ```ts
import type { CollectionConfig } from 'payload' import type { CollectionConfig } from 'payload'

View File

@@ -746,17 +746,6 @@ export type NamedGroupField = {
export type UnnamedGroupField = { export type UnnamedGroupField = {
interfaceName?: never interfaceName?: never
/**
* Can be either:
* - A string, which will be used as the tab's label.
* - An object, where the key is the language code and the value is the label.
*/
label:
| {
[selectedLanguage: string]: string
}
| LabelFunction
| string
localized?: never localized?: never
} & Omit<GroupBase, 'name' | 'virtual'> } & Omit<GroupBase, 'name' | 'virtual'>

View File

@@ -119,5 +119,26 @@ describe('Group', () => {
const unnamedNestedGroupField = page.locator(unnamedNestedGroupSelector) const unnamedNestedGroupField = page.locator(unnamedNestedGroupSelector)
await expect(unnamedNestedGroupField).toBeVisible() await expect(unnamedNestedGroupField).toBeVisible()
}) })
test('should display with no label when label is undefined', async () => {
await page.goto(url.create)
// Makes sure the fields are rendered
await page.mouse.wheel(0, 2000)
const nolabelGroupSelector = `.field-type.group-field#field-_index-14 .group-field__header`
const nolabelGroupField = page.locator(nolabelGroupSelector)
await expect(nolabelGroupField).toBeHidden()
// Makes sure the fields are rendered
await page.mouse.wheel(0, 2000)
// Children should render even if the group has no label
const nolabelGroupChildSelector = `.field-type.group-field#field-_index-14 #field-insideGroupWithNoLabel`
const nolabelGroupChildField = page.locator(nolabelGroupChildSelector)
await expect(nolabelGroupChildField).toBeVisible()
})
}) })
}) })

View File

@@ -314,6 +314,15 @@ const GroupFields: CollectionConfig = {
}, },
], ],
}, },
{
type: 'group',
fields: [
{
type: 'text',
name: 'insideGroupWithNoLabel',
},
],
},
{ {
type: 'group', type: 'group',
label: 'Deeply nested group', label: 'Deeply nested group',

View File

@@ -1082,6 +1082,7 @@ export interface GroupField {
| null; | null;
}; };
insideUnnamedGroup?: string | null; insideUnnamedGroup?: string | null;
insideGroupWithNoLabel?: string | null;
deeplyNestedGroup?: { deeplyNestedGroup?: {
insideNestedUnnamedGroup?: string | null; insideNestedUnnamedGroup?: string | null;
}; };
@@ -2685,6 +2686,7 @@ export interface GroupFieldsSelect<T extends boolean = true> {
email?: T; email?: T;
}; };
insideUnnamedGroup?: T; insideUnnamedGroup?: T;
insideGroupWithNoLabel?: T;
deeplyNestedGroup?: deeplyNestedGroup?:
| T | T
| { | {