feat: show nested fields in named tabs as separate columns in the list view (#12530)
### What Continuation of #7355 by extending the functionality to named tabs. Updates `flattenFields` to hoist nested fields inside named tabs to the top-level field array when `moveSubFieldsToTop` is enabled. Also fixes an issue where group fields with custom cells were being flattened out. Now, group fields with a custom cell components remain available as top-level columns. Fixes #12563
This commit is contained in:
@@ -49,10 +49,10 @@ describe('flattenFields', () => {
|
||||
i18n,
|
||||
})
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].name).toBe('slug')
|
||||
expect(result[0].accessor).toBe('meta-slug')
|
||||
expect(result[0].labelWithPrefix).toBe('Meta Info > Slug')
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[1].name).toBe('slug')
|
||||
expect(result[1].accessor).toBe('meta-slug')
|
||||
expect(result[1].labelWithPrefix).toBe('Meta Info > Slug')
|
||||
})
|
||||
|
||||
it('should NOT flatten fields inside group without moveSubFieldsToTop', () => {
|
||||
@@ -109,10 +109,10 @@ describe('flattenFields', () => {
|
||||
i18n,
|
||||
})
|
||||
|
||||
expect(hoisted).toHaveLength(1)
|
||||
expect(hoisted[0].name).toBe('deep')
|
||||
expect(hoisted[0].accessor).toBe('outer-inner-deep')
|
||||
expect(hoisted[0].labelWithPrefix).toBe('Outer > Inner > Deep Field')
|
||||
expect(hoisted).toHaveLength(3)
|
||||
expect(hoisted[2].name).toBe('deep')
|
||||
expect(hoisted[2].accessor).toBe('outer-inner-deep')
|
||||
expect(hoisted[2].labelWithPrefix).toBe('Outer > Inner > Deep Field')
|
||||
|
||||
const nonHoisted = flattenFields(fields)
|
||||
|
||||
@@ -142,14 +142,15 @@ describe('flattenFields', () => {
|
||||
i18n,
|
||||
})
|
||||
|
||||
// Should keep the group as a single top-level field
|
||||
expect(withExtract).toHaveLength(1)
|
||||
expect(withExtract[0].type).toBe('text')
|
||||
expect(withExtract[0].accessor).toBeUndefined()
|
||||
expect(withExtract[0].labelWithPrefix).toBeUndefined()
|
||||
// Should include top level group and its nested field as a top-level field
|
||||
expect(withExtract).toHaveLength(2)
|
||||
expect(withExtract[1].type).toBe('text')
|
||||
expect(withExtract[1].accessor).toBeUndefined()
|
||||
expect(withExtract[1].labelWithPrefix).toBeUndefined()
|
||||
|
||||
const withoutExtract = flattenFields(fields)
|
||||
|
||||
// Should return the group as a top-level item, not the inner field
|
||||
expect(withoutExtract).toHaveLength(1)
|
||||
expect(withoutExtract[0].type).toBe('group')
|
||||
expect(withoutExtract[0].accessor).toBeUndefined()
|
||||
@@ -195,10 +196,10 @@ describe('flattenFields', () => {
|
||||
i18n,
|
||||
})
|
||||
|
||||
expect(hoistedResult).toHaveLength(1)
|
||||
expect(hoistedResult[0].name).toBe('nestedField')
|
||||
expect(hoistedResult[0].accessor).toBe('namedGroup-nestedField')
|
||||
expect(hoistedResult[0].labelWithPrefix).toBe('Named Group > Nested Field')
|
||||
expect(hoistedResult).toHaveLength(5)
|
||||
expect(hoistedResult[4].name).toBe('nestedField')
|
||||
expect(hoistedResult[4].accessor).toBe('namedGroup-nestedField')
|
||||
expect(hoistedResult[4].labelWithPrefix).toBe('Named Group > Nested Field')
|
||||
|
||||
const nonHoistedResult = flattenFields(fields)
|
||||
|
||||
@@ -432,9 +433,9 @@ describe('flattenFields', () => {
|
||||
i18n,
|
||||
})
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].accessor).toBe('groupInRow-nestedInRowGroup')
|
||||
expect(result[0].labelWithPrefix).toBe('Group In Row > Nested In Row Group')
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[1].accessor).toBe('groupInRow-nestedInRowGroup')
|
||||
expect(result[1].labelWithPrefix).toBe('Group In Row > Nested In Row Group')
|
||||
})
|
||||
|
||||
it('should hoist named group fields inside collapsibles', () => {
|
||||
@@ -464,15 +465,114 @@ describe('flattenFields', () => {
|
||||
i18n,
|
||||
})
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].accessor).toBe('groupInCollapsible-nestedInCollapsibleGroup')
|
||||
expect(result[0].labelWithPrefix).toBe('Group In Collapsible > Nested In Collapsible Group')
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[1].accessor).toBe('groupInCollapsible-nestedInCollapsibleGroup')
|
||||
expect(result[1].labelWithPrefix).toBe('Group In Collapsible > Nested In Collapsible Group')
|
||||
})
|
||||
})
|
||||
|
||||
describe('tab integration', () => {
|
||||
it('should hoist named group fields inside tabs when moveSubFieldsToTop is true', () => {
|
||||
const fields: ClientField[] = [
|
||||
const namedTabFields: ClientField[] = [
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
label: 'Tab One',
|
||||
name: 'tabOne',
|
||||
fields: [
|
||||
{
|
||||
type: 'array',
|
||||
name: 'array',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'arrayInRow',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'textInArrayInRow',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'textInTab',
|
||||
label: 'Text In Tab',
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
name: 'groupInTab',
|
||||
label: 'Group In Tab',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'nestedTextInTabGroup',
|
||||
label: 'Nested Text In Tab Group',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const unnamedTabFields: ClientField[] = [
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
label: 'Tab One',
|
||||
fields: [
|
||||
{
|
||||
type: 'array',
|
||||
name: 'array',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'arrayInRow',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'textInArrayInRow',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'textInTab',
|
||||
label: 'Text In Tab',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
it('should hoist named group fields inside unamed tabs when moveSubFieldsToTop is true', () => {
|
||||
const unnamedTabWithNamedGroup: ClientField[] = [
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
@@ -497,14 +597,107 @@ describe('flattenFields', () => {
|
||||
},
|
||||
]
|
||||
|
||||
const result = flattenFields(fields, {
|
||||
const result = flattenFields(unnamedTabWithNamedGroup, {
|
||||
moveSubFieldsToTop: true,
|
||||
i18n,
|
||||
})
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[1].accessor).toBe('groupInTab-nestedInTabGroup')
|
||||
expect(result[1].labelWithPrefix).toBe('Group In Tab > Nested In Tab Group')
|
||||
})
|
||||
|
||||
it('should hoist fields inside unnamed groups inside unnamed tabs when moveSubFieldsToTop is true', () => {
|
||||
const unnamedTabWithUnnamedGroup: ClientField[] = [
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
label: 'Tab One',
|
||||
fields: [
|
||||
{
|
||||
type: 'group',
|
||||
label: 'Unnamed Group In Tab',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'nestedInUnnamedGroup',
|
||||
label: 'Nested In Unnamed Group',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const defaultResult = flattenFields(unnamedTabWithUnnamedGroup)
|
||||
|
||||
expect(defaultResult).toHaveLength(1)
|
||||
expect(defaultResult[0].type).toBe('group')
|
||||
expect(defaultResult[0].label).toBe('Unnamed Group In Tab')
|
||||
expect('accessor' in defaultResult[0]).toBe(false)
|
||||
expect('labelWithPrefix' in defaultResult[0]).toBe(false)
|
||||
|
||||
const hoistedResult = flattenFields(unnamedTabWithUnnamedGroup, {
|
||||
moveSubFieldsToTop: true,
|
||||
i18n,
|
||||
})
|
||||
|
||||
expect(hoistedResult).toHaveLength(2)
|
||||
const hoistedField = hoistedResult[1]
|
||||
expect(hoistedField.name).toBe('nestedInUnnamedGroup')
|
||||
expect(hoistedField.accessor).toBeUndefined()
|
||||
expect(hoistedField.labelWithPrefix).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should properly hoist fields inside named tabs when moveSubFieldsToTop is true', () => {
|
||||
const result = flattenFields(namedTabFields, {
|
||||
moveSubFieldsToTop: true,
|
||||
i18n,
|
||||
})
|
||||
|
||||
expect(result).toHaveLength(5)
|
||||
expect(result[0].accessor).toBe('tabOne-array')
|
||||
expect(result[0].labelWithPrefix).toBe('Tab One > array')
|
||||
expect(result[1].accessor).toBe('tabOne-arrayInRow')
|
||||
expect(result[1].labelWithPrefix).toBe('Tab One > arrayInRow')
|
||||
expect(result[2].accessor).toBe('tabOne-textInTab')
|
||||
expect(result[2].labelWithPrefix).toBe('Tab One > Text In Tab')
|
||||
expect(result[4].accessor).toBe('tabOne-groupInTab-nestedTextInTabGroup')
|
||||
expect(result[4].labelWithPrefix).toBe('Tab One > Group In Tab > Nested Text In Tab Group')
|
||||
})
|
||||
|
||||
it('should NOT hoist fields inside named tabs when moveSubFieldsToTop is false', () => {
|
||||
const result = flattenFields(namedTabFields)
|
||||
|
||||
// We expect one top-level field: the tabs container itself is *not* hoisted
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].accessor).toBe('groupInTab-nestedInTabGroup')
|
||||
expect(result[0].labelWithPrefix).toBe('Group In Tab > Nested In Tab Group')
|
||||
|
||||
const tabField = result[0]
|
||||
expect(tabField.type).toBe('tab')
|
||||
|
||||
// Confirm nested fields are NOT hoisted: no accessors or labelWithPrefix at the top level
|
||||
expect('accessor' in tabField).toBe(false)
|
||||
expect('labelWithPrefix' in tabField).toBe(false)
|
||||
})
|
||||
|
||||
it('should hoist fields inside unnamed tabs regardless of moveSubFieldsToTop', () => {
|
||||
const resultDefault = flattenFields(unnamedTabFields)
|
||||
const resultHoisted = flattenFields(unnamedTabFields, {
|
||||
moveSubFieldsToTop: true,
|
||||
i18n,
|
||||
})
|
||||
|
||||
expect(resultDefault).toHaveLength(3)
|
||||
expect(resultHoisted).toHaveLength(3)
|
||||
expect(resultDefault).toEqual(resultHoisted)
|
||||
|
||||
for (const field of resultDefault) {
|
||||
expect(field.accessor).toBeUndefined()
|
||||
expect(field.labelWithPrefix).toBeUndefined()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -50,7 +50,7 @@ type FlattenFieldsOptions = {
|
||||
*/
|
||||
labelPrefix?: string
|
||||
/**
|
||||
* If true, nested fields inside `group` fields will be lifted to the top level
|
||||
* If true, nested fields inside `group` & `tabs` fields will be lifted to the top level
|
||||
* and given contextual `accessor` and `labelWithPrefix` values.
|
||||
* Default: false.
|
||||
*/
|
||||
@@ -85,48 +85,93 @@ function flattenFields<TField extends ClientField | Field>(
|
||||
} = normalizedOptions
|
||||
|
||||
return fields.reduce<FlattenedField<TField>[]>((acc, field) => {
|
||||
if (fieldHasSubFields(field)) {
|
||||
if (field.type === 'group') {
|
||||
if (moveSubFieldsToTop && 'fields' in field) {
|
||||
const isNamedGroup = 'name' in field && typeof field.name === 'string' && !!field.name
|
||||
if (field.type === 'group' && 'fields' in field) {
|
||||
if (moveSubFieldsToTop) {
|
||||
const isNamedGroup = 'name' in field && typeof field.name === 'string' && !!field.name
|
||||
const groupName = 'name' in field ? field.name : undefined
|
||||
|
||||
const translatedLabel =
|
||||
'label' in field && field.label && i18n
|
||||
? getTranslation(field.label as string, i18n)
|
||||
: undefined
|
||||
const translatedLabel =
|
||||
'label' in field && field.label && i18n
|
||||
? getTranslation(field.label as string, i18n)
|
||||
: undefined
|
||||
|
||||
const labelWithPrefix =
|
||||
isNamedGroup && labelPrefix && translatedLabel
|
||||
? `${labelPrefix} > ${translatedLabel}`
|
||||
: (labelPrefix ?? translatedLabel)
|
||||
const labelWithPrefix = labelPrefix
|
||||
? `${labelPrefix} > ${translatedLabel ?? groupName}`
|
||||
: (translatedLabel ?? groupName)
|
||||
|
||||
const nameWithPrefix =
|
||||
isNamedGroup && field.name
|
||||
? pathPrefix
|
||||
? `${pathPrefix}-${field.name as string}`
|
||||
: (field.name as string)
|
||||
: pathPrefix
|
||||
const nameWithPrefix =
|
||||
'name' in field && field.name
|
||||
? pathPrefix
|
||||
? `${pathPrefix}-${field.name}`
|
||||
: field.name
|
||||
: pathPrefix
|
||||
|
||||
acc.push(
|
||||
...flattenFields(field.fields as TField[], {
|
||||
i18n,
|
||||
keepPresentationalFields,
|
||||
labelPrefix: isNamedGroup ? labelWithPrefix : labelPrefix,
|
||||
moveSubFieldsToTop,
|
||||
pathPrefix: isNamedGroup ? nameWithPrefix : pathPrefix,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
// Just keep the group as-is
|
||||
acc.push(field as FlattenedField<TField>)
|
||||
}
|
||||
} else if (['collapsible', 'row'].includes(field.type)) {
|
||||
// Recurse into row and collapsible
|
||||
acc.push(...flattenFields(field.fields as TField[], options))
|
||||
acc.push(
|
||||
// Need to include the top-level group field when hoisting its subfields,
|
||||
// so that `buildColumnState` can detect and render a column if the group
|
||||
// has a custom admin Cell component defined in its configuration.
|
||||
// See: packages/ui/src/providers/TableColumns/buildColumnState/index.tsx
|
||||
field as FlattenedField<TField>,
|
||||
...flattenFields(field.fields as TField[], {
|
||||
i18n,
|
||||
keepPresentationalFields,
|
||||
labelPrefix: isNamedGroup ? labelWithPrefix : labelPrefix,
|
||||
moveSubFieldsToTop,
|
||||
pathPrefix: isNamedGroup ? nameWithPrefix : pathPrefix,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
// Do not hoist fields from arrays & blocks
|
||||
// Hoisting diabled - keep as top level field
|
||||
acc.push(field as FlattenedField<TField>)
|
||||
}
|
||||
} else if (field.type === 'tabs' && 'tabs' in field) {
|
||||
return [
|
||||
...acc,
|
||||
...field.tabs.reduce<FlattenedField<TField>[]>((tabFields, tab: TabType<TField>) => {
|
||||
if (tabHasName(tab)) {
|
||||
if (moveSubFieldsToTop) {
|
||||
const translatedLabel =
|
||||
'label' in tab && tab.label && i18n ? getTranslation(tab.label, i18n) : undefined
|
||||
|
||||
const labelWithPrefixForTab = labelPrefix
|
||||
? `${labelPrefix} > ${translatedLabel ?? tab.name}`
|
||||
: (translatedLabel ?? tab.name)
|
||||
|
||||
const pathPrefixForTab = tab.name
|
||||
? pathPrefix
|
||||
? `${pathPrefix}-${tab.name}`
|
||||
: tab.name
|
||||
: pathPrefix
|
||||
|
||||
return [
|
||||
...tabFields,
|
||||
...flattenFields(tab.fields as TField[], {
|
||||
i18n,
|
||||
keepPresentationalFields,
|
||||
labelPrefix: labelWithPrefixForTab,
|
||||
moveSubFieldsToTop,
|
||||
pathPrefix: pathPrefixForTab,
|
||||
}),
|
||||
]
|
||||
} else {
|
||||
// Named tab, hoisting disabled: keep as top-level field
|
||||
return [
|
||||
...tabFields,
|
||||
{
|
||||
...tab,
|
||||
type: 'tab',
|
||||
} as unknown as FlattenedField<TField>,
|
||||
]
|
||||
}
|
||||
} else {
|
||||
// Unnamed tab: always hoist its fields
|
||||
return [...tabFields, ...flattenFields<TField>(tab.fields as TField[], options)]
|
||||
}
|
||||
}, []),
|
||||
]
|
||||
} else if (fieldHasSubFields(field) && ['collapsible', 'row'].includes(field.type)) {
|
||||
// Recurse into row and collapsible
|
||||
acc.push(...flattenFields(field.fields as TField[], options))
|
||||
} else if (
|
||||
fieldAffectsData(field) ||
|
||||
(keepPresentationalFields && fieldIsPresentationalOnly(field))
|
||||
@@ -148,30 +193,11 @@ function flattenFields<TField extends ClientField | Field>(
|
||||
...(moveSubFieldsToTop &&
|
||||
isHoistingFromGroup && {
|
||||
accessor: pathPrefix && name ? `${pathPrefix}-${name}` : (name ?? ''),
|
||||
labelWithPrefix:
|
||||
labelPrefix && translatedLabel
|
||||
? `${labelPrefix} > ${translatedLabel}`
|
||||
: (labelPrefix ?? translatedLabel),
|
||||
labelWithPrefix: labelPrefix
|
||||
? `${labelPrefix} > ${translatedLabel ?? name}`
|
||||
: (translatedLabel ?? name),
|
||||
}),
|
||||
})
|
||||
} else if (field.type === 'tabs' && 'tabs' in field) {
|
||||
return [
|
||||
...acc,
|
||||
...field.tabs.reduce<FlattenedField<TField>[]>((tabFields, tab: TabType<TField>) => {
|
||||
if (tabHasName(tab)) {
|
||||
return [
|
||||
...tabFields,
|
||||
{
|
||||
...tab,
|
||||
type: 'tab',
|
||||
...(moveSubFieldsToTop && { labelPrefix }),
|
||||
} as unknown as FlattenedField<TField>,
|
||||
]
|
||||
} else {
|
||||
return [...tabFields, ...flattenFields<TField>(tab.fields as TField[], options)]
|
||||
}
|
||||
}, []),
|
||||
]
|
||||
}
|
||||
|
||||
return acc
|
||||
|
||||
@@ -139,6 +139,17 @@ export const buildColumnState = (args: BuildColumnStateArgs): Column[] => {
|
||||
return fAccessor === accessor
|
||||
})
|
||||
|
||||
const hasCustomCell =
|
||||
serverField?.admin &&
|
||||
'components' in serverField.admin &&
|
||||
serverField.admin.components &&
|
||||
'Cell' in serverField.admin.components &&
|
||||
serverField.admin.components.Cell
|
||||
|
||||
if (serverField && serverField.type === 'group' && !hasCustomCell) {
|
||||
return acc // skip any group without a custom cell
|
||||
}
|
||||
|
||||
const columnPreference = columnPreferences?.find(
|
||||
(preference) => clientField && 'name' in clientField && preference.accessor === accessor,
|
||||
)
|
||||
|
||||
@@ -133,7 +133,7 @@ export const Posts: CollectionConfig = {
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'group',
|
||||
name: 'namedGroup',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
@@ -142,6 +142,54 @@ export const Posts: CollectionConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
label: 'Unnamed group',
|
||||
fields: [
|
||||
{
|
||||
name: 'textFieldInUnnamedGroup',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'groupWithCustomCell',
|
||||
type: 'group',
|
||||
admin: {
|
||||
components: {
|
||||
Cell: '/components/CustomGroupCell/index.js#CustomGroupCell',
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'nestedTextFieldInGroupWithCustomCell',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
name: 'namedTab',
|
||||
fields: [
|
||||
{
|
||||
name: 'nestedTextFieldInNamedTab',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'unnamedTab',
|
||||
fields: [
|
||||
{
|
||||
name: 'nestedTextFieldInUnnamedTab',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'relationship',
|
||||
type: 'relationship',
|
||||
|
||||
9
test/admin/components/CustomGroupCell/index.tsx
Normal file
9
test/admin/components/CustomGroupCell/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import type { DefaultCellComponentProps } from 'payload'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
export const CustomGroupCell: React.FC<DefaultCellComponentProps> = (props) => {
|
||||
return <div>{`Custom group cell: ${props?.rowData?.title || 'No data'}`}</div>
|
||||
}
|
||||
@@ -955,24 +955,64 @@ describe('List View', () => {
|
||||
expect(page.url()).not.toMatch(/columns=/)
|
||||
})
|
||||
|
||||
test('should render field in group as column', async () => {
|
||||
await createPost({ group: { someTextField: 'nested group text field' } })
|
||||
test('should render nested field in named group as separate column', async () => {
|
||||
await createPost({ namedGroup: { someTextField: 'nested group text field' } })
|
||||
await page.goto(postsUrl.list)
|
||||
await openColumnControls(page)
|
||||
await page
|
||||
.locator('.column-selector .column-selector__column', {
|
||||
hasText: exactText('Group > Some Text Field'),
|
||||
hasText: exactText('Named Group > Some Text Field'),
|
||||
})
|
||||
.click()
|
||||
await expect(page.locator('.row-1 .cell-group-someTextField')).toHaveText(
|
||||
await expect(page.locator('.row-1 .cell-namedGroup-someTextField')).toHaveText(
|
||||
'nested group text field',
|
||||
)
|
||||
})
|
||||
|
||||
test('should render top-level and group field with same name in separate columns', async () => {
|
||||
test('should render nested field in unnamed group as separate column', async () => {
|
||||
await createPost({ textFieldInUnnamedGroup: 'nested text in unnamed group' })
|
||||
await page.goto(postsUrl.list)
|
||||
await openColumnControls(page)
|
||||
await page
|
||||
.locator('.column-selector .column-selector__column', {
|
||||
hasText: exactText('Text Field In Unnamed Group'),
|
||||
})
|
||||
.click()
|
||||
await expect(page.locator('.row-1 .cell-textFieldInUnnamedGroup')).toHaveText(
|
||||
'nested text in unnamed group',
|
||||
)
|
||||
})
|
||||
|
||||
test('should not render group field as top level column when custom cell is not defined', async () => {
|
||||
await createPost({ namedGroup: { someTextField: 'nested group text field' } })
|
||||
await page.goto(postsUrl.list)
|
||||
await openColumnControls(page)
|
||||
await expect(
|
||||
page.locator('.column-selector .column-selector__column', {
|
||||
hasText: exactText('Named Group'),
|
||||
}),
|
||||
).toBeHidden()
|
||||
})
|
||||
|
||||
test('should render group field as top level column when custom cell is defined', async () => {
|
||||
await createPost({
|
||||
groupWithCustomCell: {
|
||||
nestedTextFieldInGroupWithCustomCell: 'nested group text field in group with custom cell',
|
||||
},
|
||||
})
|
||||
await page.goto(postsUrl.list)
|
||||
await openColumnControls(page)
|
||||
await expect(
|
||||
page.locator('.column-selector .column-selector__column', {
|
||||
hasText: exactText('Group With Custom Cell'),
|
||||
}),
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('should render top-level field and group field with same name in separate columns', async () => {
|
||||
await createPost({
|
||||
someTextField: 'top-level text field',
|
||||
group: { someTextField: 'nested group text field' },
|
||||
namedGroup: { someTextField: 'nested group text field' },
|
||||
})
|
||||
|
||||
await page.goto(postsUrl.list)
|
||||
@@ -988,7 +1028,7 @@ describe('List View', () => {
|
||||
// Enable group column
|
||||
await page
|
||||
.locator('.column-selector .column-selector__column', {
|
||||
hasText: exactText('Group > Some Text Field'),
|
||||
hasText: exactText('Named Group > Some Text Field'),
|
||||
})
|
||||
.click()
|
||||
|
||||
@@ -996,11 +1036,39 @@ describe('List View', () => {
|
||||
await expect(page.locator('.row-1 .cell-someTextField')).toHaveText('top-level text field')
|
||||
|
||||
// Expect nested group cell
|
||||
await expect(page.locator('.row-1 .cell-group-someTextField')).toHaveText(
|
||||
await expect(page.locator('.row-1 .cell-namedGroup-someTextField')).toHaveText(
|
||||
'nested group text field',
|
||||
)
|
||||
})
|
||||
|
||||
test('should render nested field in named tab as separate column', async () => {
|
||||
await createPost({ namedTab: { nestedTextFieldInNamedTab: 'nested text in named tab' } })
|
||||
await page.goto(postsUrl.list)
|
||||
await openColumnControls(page)
|
||||
await page
|
||||
.locator('.column-selector .column-selector__column', {
|
||||
hasText: exactText('Named Tab > Nested Text Field In Named Tab'),
|
||||
})
|
||||
.click()
|
||||
await expect(page.locator('.row-1 .cell-namedTab-nestedTextFieldInNamedTab')).toHaveText(
|
||||
'nested text in named tab',
|
||||
)
|
||||
})
|
||||
|
||||
test('should render nested field in unnamed tab as separate column', async () => {
|
||||
await createPost({ nestedTextFieldInUnnamedTab: 'nested text in unnamed tab' })
|
||||
await page.goto(postsUrl.list)
|
||||
await openColumnControls(page)
|
||||
await page
|
||||
.locator('.column-selector .column-selector__column', {
|
||||
hasText: exactText('Nested Text Field In Unnamed Tab'),
|
||||
})
|
||||
.click()
|
||||
await expect(page.locator('.row-1 .cell-nestedTextFieldInUnnamedTab')).toHaveText(
|
||||
'nested text in unnamed tab',
|
||||
)
|
||||
})
|
||||
|
||||
test('should drag to reorder columns and save to preferences', async () => {
|
||||
await reorderColumns(page, { fromColumn: 'Number', toColumn: 'ID' })
|
||||
|
||||
@@ -1308,7 +1376,11 @@ describe('List View', () => {
|
||||
beforeEach(async () => {
|
||||
// delete all posts created by the seed
|
||||
await deleteAllPosts()
|
||||
await createPost({ number: 1, group: { someTextField: 'nested group text field' } })
|
||||
await createPost({
|
||||
number: 1,
|
||||
namedGroup: { someTextField: 'nested group text field' },
|
||||
namedTab: { nestedTextFieldInNamedTab: 'nested text in named tab' },
|
||||
})
|
||||
await createPost({ number: 2 })
|
||||
})
|
||||
|
||||
@@ -1335,33 +1407,69 @@ describe('List View', () => {
|
||||
await openColumnControls(page)
|
||||
await page
|
||||
.locator('.column-selector .column-selector__column', {
|
||||
hasText: exactText('Group > Some Text Field'),
|
||||
hasText: exactText('Named Group > Some Text Field'),
|
||||
})
|
||||
.click()
|
||||
const upChevron = page.locator('#heading-group-someTextField .sort-column__asc')
|
||||
const downChevron = page.locator('#heading-group-someTextField .sort-column__desc')
|
||||
const upChevron = page.locator('#heading-namedGroup-someTextField .sort-column__asc')
|
||||
const downChevron = page.locator('#heading-namedGroup-someTextField .sort-column__desc')
|
||||
|
||||
await upChevron.click()
|
||||
await page.waitForURL(/sort=group.someTextField/)
|
||||
await page.waitForURL(/sort=namedGroup.someTextField/)
|
||||
|
||||
await expect(page.locator('.row-1 .cell-group-someTextField')).toHaveText(
|
||||
await expect(page.locator('.row-1 .cell-namedGroup-someTextField')).toHaveText(
|
||||
'<No Some Text Field>',
|
||||
)
|
||||
await expect(page.locator('.row-2 .cell-group-someTextField')).toHaveText(
|
||||
await expect(page.locator('.row-2 .cell-namedGroup-someTextField')).toHaveText(
|
||||
'nested group text field',
|
||||
)
|
||||
|
||||
await downChevron.click()
|
||||
await page.waitForURL(/sort=-group.someTextField/)
|
||||
await page.waitForURL(/sort=-namedGroup.someTextField/)
|
||||
|
||||
await expect(page.locator('.row-1 .cell-group-someTextField')).toHaveText(
|
||||
await expect(page.locator('.row-1 .cell-namedGroup-someTextField')).toHaveText(
|
||||
'nested group text field',
|
||||
)
|
||||
await expect(page.locator('.row-2 .cell-group-someTextField')).toHaveText(
|
||||
await expect(page.locator('.row-2 .cell-namedGroup-someTextField')).toHaveText(
|
||||
'<No Some Text Field>',
|
||||
)
|
||||
})
|
||||
|
||||
test('should allow sorting by nested field within tab in separate column', async () => {
|
||||
await page.goto(postsUrl.list)
|
||||
await openColumnControls(page)
|
||||
await page
|
||||
.locator('.column-selector .column-selector__column', {
|
||||
hasText: exactText('Named Tab > Nested Text Field In Named Tab'),
|
||||
})
|
||||
.click()
|
||||
const upChevron = page.locator(
|
||||
'#heading-namedTab-nestedTextFieldInNamedTab .sort-column__asc',
|
||||
)
|
||||
const downChevron = page.locator(
|
||||
'#heading-namedTab-nestedTextFieldInNamedTab .sort-column__desc',
|
||||
)
|
||||
|
||||
await upChevron.click()
|
||||
await page.waitForURL(/sort=namedTab.nestedTextFieldInNamedTab/)
|
||||
|
||||
await expect(page.locator('.row-1 .cell-namedTab-nestedTextFieldInNamedTab')).toHaveText(
|
||||
'<No Nested Text Field In Named Tab>',
|
||||
)
|
||||
await expect(page.locator('.row-2 .cell-namedTab-nestedTextFieldInNamedTab')).toHaveText(
|
||||
'nested text in named tab',
|
||||
)
|
||||
|
||||
await downChevron.click()
|
||||
await page.waitForURL(/sort=-namedTab.nestedTextFieldInNamedTab/)
|
||||
|
||||
await expect(page.locator('.row-1 .cell-namedTab-nestedTextFieldInNamedTab')).toHaveText(
|
||||
'nested text in named tab',
|
||||
)
|
||||
await expect(page.locator('.row-2 .cell-namedTab-nestedTextFieldInNamedTab')).toHaveText(
|
||||
'<No Nested Text Field In Named Tab>',
|
||||
)
|
||||
})
|
||||
|
||||
test('should sort with existing filters', async () => {
|
||||
await page.goto(postsUrl.list)
|
||||
|
||||
|
||||
@@ -238,9 +238,17 @@ export interface Post {
|
||||
}[]
|
||||
| null;
|
||||
someTextField?: string | null;
|
||||
group?: {
|
||||
namedGroup?: {
|
||||
someTextField?: string | null;
|
||||
};
|
||||
textFieldInUnnamedGroup?: string | null;
|
||||
groupWithCustomCell?: {
|
||||
nestedTextFieldInGroupWithCustomCell?: string | null;
|
||||
};
|
||||
namedTab?: {
|
||||
nestedTextFieldInNamedTab?: string | null;
|
||||
};
|
||||
nestedTextFieldInUnnamedTab?: string | null;
|
||||
relationship?: (string | null) | Post;
|
||||
users?: (string | null) | User;
|
||||
customCell?: string | null;
|
||||
@@ -700,11 +708,23 @@ export interface PostsSelect<T extends boolean = true> {
|
||||
number?: T;
|
||||
richText?: T;
|
||||
someTextField?: T;
|
||||
group?:
|
||||
namedGroup?:
|
||||
| T
|
||||
| {
|
||||
someTextField?: T;
|
||||
};
|
||||
textFieldInUnnamedGroup?: T;
|
||||
groupWithCustomCell?:
|
||||
| T
|
||||
| {
|
||||
nestedTextFieldInGroupWithCustomCell?: T;
|
||||
};
|
||||
namedTab?:
|
||||
| T
|
||||
| {
|
||||
nestedTextFieldInNamedTab?: T;
|
||||
};
|
||||
nestedTextFieldInUnnamedTab?: T;
|
||||
relationship?: T;
|
||||
users?: T;
|
||||
customCell?: T;
|
||||
|
||||
Reference in New Issue
Block a user