fix(ui): populate nested fields for enableListViewSelectAPI (#13827)

Fixes an issue with the new experimental `enableListViewSelectAPI`
config option.

Group fields were not populating properly in the list view

### Before (incorrect)
```ts
{
  group.field: true
}
```

### After (correct)
```ts
{
  group: {
    field: true
  }
}
```
This commit is contained in:
Jarrod Flesch
2025-09-16 17:23:08 -04:00
committed by GitHub
parent a9553925f6
commit dc732b8f52
7 changed files with 142 additions and 54 deletions

View File

@@ -1,9 +1,14 @@
import type { ColumnPreference, SelectType } from 'payload' import type { ColumnPreference, SelectType } from 'payload'
export const transformColumnsToSelect = (columns: ColumnPreference[]): SelectType => import { unflatten } from 'payload/shared'
columns.reduce((acc, column) => {
export const transformColumnsToSelect = (columns: ColumnPreference[]): SelectType => {
const columnsSelect = columns.reduce((acc, column) => {
if (column.active) { if (column.active) {
acc[column.accessor] = true acc[column.accessor] = true
} }
return acc return acc
}, {} as SelectType) }, {} as SelectType)
return unflatten(columnsSelect)
}

View File

@@ -21,5 +21,15 @@ export const ListViewSelectAPI: CollectionConfig = {
name: 'description', name: 'description',
type: 'text', type: 'text',
}, },
{
name: 'group',
type: 'group',
fields: [
{
name: 'groupNameField',
type: 'text',
},
],
},
], ],
} }

View File

@@ -36,6 +36,7 @@ let payload: PayloadTestSDK<Config>
import { listViewSelectAPISlug } from 'admin/collections/ListViewSelectAPI/index.js' import { listViewSelectAPISlug } from 'admin/collections/ListViewSelectAPI/index.js'
import { devUser } from 'credentials.js' import { devUser } from 'credentials.js'
import { getRowByCellValueAndAssert } from 'helpers/e2e/getRowByCellValueAndAssert.js'
import { import {
openListColumns, openListColumns,
reorderColumns, reorderColumns,
@@ -993,58 +994,101 @@ describe('List View', () => {
await toggleColumn(page, { columnLabel: 'ID', columnName: 'id', targetState: 'off' }) await toggleColumn(page, { columnLabel: 'ID', columnName: 'id', targetState: 'off' })
}) })
test('should use select API in the list view when `enableListViewSelectAPI` is true', async () => { describe('enableListViewSelectAPI', () => {
const doc = await payload.create({ test('`id` should always be selected even when toggled off', async () => {
collection: listViewSelectAPISlug, const doc = await payload.create({
data: { collection: listViewSelectAPISlug,
title: 'This is a test title', data: {
description: 'This is a test description', title: 'This is a test title',
}, description: 'This is a test description',
},
})
const selectAPIUrl = new AdminUrlUtil(serverURL, listViewSelectAPISlug)
await page.goto(selectAPIUrl.list)
const printedResults = page.locator('#table-state')
await expect
.poll(
async () => {
const resultText = await printedResults.innerText()
const parsedResult = JSON.parse(resultText)
return Boolean(parsedResult[0].id && parsedResult[0].description)
},
{
timeout: 3000,
intervals: [100, 250, 500, 1000],
},
)
.toBeTruthy()
await toggleColumn(page, { columnLabel: 'ID', columnName: 'id', targetState: 'off' })
await toggleColumn(page, {
columnLabel: 'Description',
columnName: 'description',
targetState: 'off',
})
// Poll until the "description" field is removed from the response BUT `id` is still present
// The `id` field will remain selected despite it being inactive
await expect
.poll(
async () => {
const resultText = await printedResults.innerText()
const parsedResult = JSON.parse(resultText)
return Boolean(parsedResult[0].description === undefined && parsedResult[0].id)
},
{
timeout: 3000,
intervals: [100, 250, 500, 1000],
},
)
.toBeTruthy()
}) })
const selectAPIUrl = new AdminUrlUtil(serverURL, listViewSelectAPISlug) test('group fields should populate with the select API', async () => {
const doc = await payload.create({
await page.goto(selectAPIUrl.list) collection: listViewSelectAPISlug,
data: {
const printedResults = page.locator('#table-state') title: 'This is a test title',
description: 'This is a test description',
await expect group: {
.poll( groupNameField: 'Select Nested Field',
async () => { },
const resultText = await printedResults.innerText()
const parsedResult = JSON.parse(resultText)
return Boolean(parsedResult[0].id && parsedResult[0].description)
}, },
{ })
timeout: 3000,
intervals: [100, 250, 500, 1000],
},
)
.toBeTruthy()
await toggleColumn(page, { columnLabel: 'ID', columnName: 'id', targetState: 'off' }) const selectAPIUrl = new AdminUrlUtil(serverURL, listViewSelectAPISlug)
await toggleColumn(page, { await page.goto(selectAPIUrl.list)
columnLabel: 'Description',
columnName: 'description', await toggleColumn(page, {
targetState: 'off', columnLabel: 'Group > Group Name Field',
columnName: 'group.groupNameField',
targetState: 'off',
})
await toggleColumn(page, {
columnLabel: 'Group > Group Name Field',
columnName: 'group.groupNameField',
targetState: 'on',
})
await getRowByCellValueAndAssert({
cellClass: `.cell-group__groupNameField`,
page,
textToMatch: 'Select Nested Field',
})
// cleanup after run
await payload.delete({
id: doc.id,
collection: listViewSelectAPISlug,
})
}) })
// Poll until the "description" field is removed from the response BUT `id` is still present
// The `id` field will remain selected despite it being inactive
await expect
.poll(
async () => {
const resultText = await printedResults.innerText()
const parsedResult = JSON.parse(resultText)
return Boolean(parsedResult[0].description === undefined && parsedResult[0].id)
},
{
timeout: 3000,
intervals: [100, 250, 500, 1000],
},
)
.toBeTruthy()
}) })
test('should toggle columns and save to preferences', async () => { test('should toggle columns and save to preferences', async () => {

View File

@@ -594,6 +594,9 @@ export interface ListViewSelectApi {
id: string; id: string;
title?: string | null; title?: string | null;
description?: string | null; description?: string | null;
group?: {
groupNameField?: string | null;
};
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
@@ -1153,6 +1156,11 @@ export interface CustomListDrawerSelect<T extends boolean = true> {
export interface ListViewSelectApiSelect<T extends boolean = true> { export interface ListViewSelectApiSelect<T extends boolean = true> {
title?: T; title?: T;
description?: T; description?: T;
group?:
| T
| {
groupNameField?: T;
};
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
} }

View File

@@ -76,6 +76,7 @@ export const testEslintConfig = [
'createFolderFromDoc', 'createFolderFromDoc',
'assertURLParams', 'assertURLParams',
'uploadImage', 'uploadImage',
'getRowByCellValueAndAssert',
], ],
}, },
], ],

View File

@@ -0,0 +1,23 @@
import type { Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
export async function getRowByCellValueAndAssert({
page,
textToMatch,
cellClass,
}: {
cellClass: `.cell-${string}`
page: Page
textToMatch: string
}): Promise<Locator> {
const row = page
.locator(`.collection-list .table tr`)
.filter({
has: page.locator(`${cellClass}`, { hasText: textToMatch }),
})
.first()
await expect(row).toBeVisible()
return row
}

View File

@@ -2,6 +2,8 @@ import type { Page } from '@playwright/test'
import type { AdminUrlUtil } from '../../helpers/adminUrlUtil.js' import type { AdminUrlUtil } from '../../helpers/adminUrlUtil.js'
import { getRowByCellValueAndAssert } from './getRowByCellValueAndAssert.js'
export async function goToListDoc({ export async function goToListDoc({
page, page,
cellClass, cellClass,
@@ -14,12 +16,7 @@ export async function goToListDoc({
urlUtil: AdminUrlUtil urlUtil: AdminUrlUtil
}) { }) {
await page.goto(urlUtil.list) await page.goto(urlUtil.list)
const row = page const row = await getRowByCellValueAndAssert({ page, textToMatch, cellClass })
.locator(`.collection-list .table tr`)
.filter({
has: page.locator(`${cellClass}`, { hasText: textToMatch }),
})
.first()
const cellLink = row.locator(`td a`).first() const cellLink = row.locator(`td a`).first()
const linkURL = await cellLink.getAttribute('href') const linkURL = await cellLink.getAttribute('href')
await page.goto(`${urlUtil.serverURL}${linkURL}`) await page.goto(`${urlUtil.serverURL}${linkURL}`)