Skip to content

Commit e5bc6be

Browse files
authored
fix(ui): bulk edit ignores fields in named tabs and shows incorrect labels for unlabeled containers (#16340)
## Overview Fixes two bugs in `reduceFieldOptions` affecting the bulk edit field selector. ### Key Changes Fields inside named tabs were not updating on bulk edit - the tab namespace was being stripped from the field path, so the API received `{ status: "draft" }` instead of `{ myTab: { status: "draft" } }` and silently skipped the update. Fields nested inside unlabeled containers (`row`, groups with `label: false`) were showing spurious `> >` prefixes in the dropdown. Fixes #15995
1 parent f6e9073 commit e5bc6be

4 files changed

Lines changed: 146 additions & 2 deletions

File tree

packages/ui/src/elements/FieldSelect/reduceFieldOptions.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,14 @@ export const reduceFieldOptions = ({
8383
}
8484

8585
if (!(field.type === 'array' || field.type === 'blocks') && fieldHasSubFields(field)) {
86+
const fieldHasLabel = 'label' in field && field.label
8687
return [
8788
...fieldsToUse,
8889
...reduceFieldOptions({
8990
fields: field.fields,
90-
labelPrefix: combineFieldLabel({ CustomLabel, field, prefix: labelPrefix }),
91+
labelPrefix: fieldHasLabel
92+
? combineFieldLabel({ CustomLabel, field, prefix: labelPrefix })
93+
: labelPrefix,
9194
parentPath: path,
9295
path: createNestedClientFieldPath(path, field),
9396
permissions: fieldPermissions,
@@ -107,7 +110,7 @@ export const reduceFieldOptions = ({
107110
fields: tab.fields,
108111
labelPrefix,
109112
parentPath: path,
110-
path: isNamedTab ? createNestedClientFieldPath(path, field) : path,
113+
path: isNamedTab ? createNestedClientFieldPath(path, tab as ClientField) : path,
111114
permissions: fieldPermissions,
112115
}),
113116
]

test/bulk-edit/collections/Tabs/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ export const TabsCollection: CollectionConfig = {
2424
{
2525
name: 'tabTab',
2626
fields: [
27+
{
28+
name: 'tabText',
29+
type: 'text',
30+
},
2731
{
2832
name: 'tabTabArray',
2933
type: 'array',
@@ -38,6 +42,24 @@ export const TabsCollection: CollectionConfig = {
3842
},
3943
],
4044
},
45+
{
46+
name: 'noLabelGroup',
47+
type: 'group',
48+
label: false,
49+
fields: [
50+
{
51+
type: 'row',
52+
fields: [
53+
{
54+
name: 'rowText',
55+
type: 'text',
56+
label: 'Row Text',
57+
admin: { width: '50%' },
58+
},
59+
],
60+
},
61+
],
62+
},
4163
],
4264
},
4365
],

test/bulk-edit/e2e.spec.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,102 @@ test.describe('Bulk Edit', () => {
745745
.toEqual('nestedText')
746746
})
747747

748+
test('should bulk edit a field inside a named tab', async () => {
749+
const originalDoc = await payload.create({
750+
collection: tabsSlug,
751+
data: {
752+
title: 'Tab Doc',
753+
tabTab: {
754+
tabText: 'original value',
755+
},
756+
},
757+
})
758+
759+
await page.goto(tabsUrl.list)
760+
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('limit=')
761+
762+
await addListFilter({
763+
page,
764+
fieldLabel: 'ID',
765+
operatorLabel: 'equals',
766+
value: originalDoc.id,
767+
})
768+
769+
await page.locator('table tbody tr.row-1 input[type="checkbox"]').check()
770+
await page
771+
.locator('.list-selection__actions .btn', {
772+
hasText: 'Edit',
773+
})
774+
.click()
775+
776+
const bulkEditForm = page.locator('form.edit-many__form')
777+
await expect(bulkEditForm).toBeVisible()
778+
779+
await selectInput({
780+
selectLocator: bulkEditForm.locator('.react-select'),
781+
options: ['Tab Text'],
782+
multiSelect: true,
783+
})
784+
785+
await bulkEditForm.getByLabel('Tab Text').fill('updated value')
786+
await bulkEditForm.locator('button[type="submit"]').click()
787+
788+
await expect(bulkEditForm).toBeHidden()
789+
790+
const updatedDocQuery = await payload.find({
791+
collection: tabsSlug,
792+
where: {
793+
id: {
794+
equals: originalDoc.id,
795+
},
796+
},
797+
})
798+
const updatedDoc = updatedDocQuery.docs[0]
799+
800+
await expect
801+
.poll(() => updatedDoc?.tabTab?.tabText, { timeout: POLL_TOPASS_TIMEOUT })
802+
.toEqual('updated value')
803+
804+
await payload.delete({ collection: tabsSlug, id: originalDoc.id })
805+
})
806+
807+
test('should show clean labels for fields inside label-false groups and rows', async () => {
808+
const doc = await payload.create({
809+
collection: tabsSlug,
810+
data: { title: 'Label Test Doc' },
811+
})
812+
813+
await page.goto(tabsUrl.list)
814+
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('limit=')
815+
816+
await addListFilter({
817+
page,
818+
fieldLabel: 'ID',
819+
operatorLabel: 'equals',
820+
value: doc.id,
821+
})
822+
823+
await page.locator('table tbody tr.row-1 input[type="checkbox"]').check()
824+
await page
825+
.locator('.list-selection__actions .btn', {
826+
hasText: 'Edit',
827+
})
828+
.click()
829+
830+
const bulkEditForm = page.locator('form.edit-many__form')
831+
await expect(bulkEditForm).toBeVisible()
832+
833+
await bulkEditForm.locator('.field-select .rs__control').click()
834+
835+
// The option must match exactly — no spurious "> >" prefix
836+
const option = bulkEditForm.locator('.field-select .rs__option', {
837+
hasText: exactText('Row Text'),
838+
})
839+
await expect(option).toBeVisible()
840+
841+
await payload.delete({ collection: tabsSlug, id: doc.id })
842+
})
843+
748844
test('should preserve beforeInput components when selecting multiple fields', async () => {
749845
await deleteAllPosts()
750846
await createPost({ title: 'Post 1' })

test/bulk-edit/payload-types.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ export interface Config {
9292
globals: {};
9393
globalsSelect: {};
9494
locale: null;
95+
widgets: {
96+
collections: CollectionsWidget;
97+
};
9598
user: User;
9699
jobs: {
97100
tasks: unknown;
@@ -168,13 +171,17 @@ export interface Tab {
168171
id: string;
169172
title?: string | null;
170173
tabTab?: {
174+
tabText?: string | null;
171175
tabTabArray?:
172176
| {
173177
tabTabArrayText?: string | null;
174178
id?: string | null;
175179
}[]
176180
| null;
177181
};
182+
noLabelGroup?: {
183+
rowText?: string | null;
184+
};
178185
updatedAt: string;
179186
createdAt: string;
180187
}
@@ -338,13 +345,19 @@ export interface TabsSelect<T extends boolean = true> {
338345
tabTab?:
339346
| T
340347
| {
348+
tabText?: T;
341349
tabTabArray?:
342350
| T
343351
| {
344352
tabTabArrayText?: T;
345353
id?: T;
346354
};
347355
};
356+
noLabelGroup?:
357+
| T
358+
| {
359+
rowText?: T;
360+
};
348361
updatedAt?: T;
349362
createdAt?: T;
350363
}
@@ -410,6 +423,16 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
410423
updatedAt?: T;
411424
createdAt?: T;
412425
}
426+
/**
427+
* This interface was referenced by `Config`'s JSON-Schema
428+
* via the `definition` "collections_widget".
429+
*/
430+
export interface CollectionsWidget {
431+
data?: {
432+
[k: string]: unknown;
433+
};
434+
width: 'full';
435+
}
413436
/**
414437
* This interface was referenced by `Config`'s JSON-Schema
415438
* via the `definition` "auth".

0 commit comments

Comments
 (0)