Skip to content

Commit f7172b5

Browse files
authored
fix(ui): refreshes column state during hmr and respects admin.disableListColumn despite preferences (#9846)
Partial fix for #9774. When `admin.disableListColumn` is set retroactively, it continues to appear in column state, but shouldn't. This was because the table column context was not refreshing after HMR runs, and would instead hold onto these stale columns until the page itself refreshes. Similarly, this was also a problem when the user had saved any of these columns to their list preferences, where those prefs would take precedence despite these properties being set on the underlying fields. The fix is to filter these columns from all requests that send them, and ensure local component state properly refreshes itself.
1 parent 563694d commit f7172b5

File tree

19 files changed

+194
-120
lines changed

19 files changed

+194
-120
lines changed

packages/next/src/views/List/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,13 +171,13 @@ export const renderListView = async (
171171
const clientCollectionConfig = clientConfig.collections.find((c) => c.slug === collectionSlug)
172172

173173
const { columnState, Table } = renderTable({
174-
collectionConfig: clientCollectionConfig,
174+
clientCollectionConfig,
175+
collectionConfig,
175176
columnPreferences: listPreferences?.columns,
176177
customCellProps,
177178
docs: data.docs,
178179
drawerSlug,
179180
enableRowSelections,
180-
fields,
181181
i18n: req.i18n,
182182
payload,
183183
useAsTitle,

packages/payload/src/exports/shared.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ export {
5757
} from '../utilities/deepMerge.js'
5858

5959
export { fieldSchemaToJSON } from '../utilities/fieldSchemaToJSON.js'
60+
export { flattenAllFields } from '../utilities/flattenAllFields.js'
61+
export { default as flattenTopLevelFields } from '../utilities/flattenTopLevelFields.js'
6062

6163
export { getDataByPath } from '../utilities/getDataByPath.js'
6264
export { getSelectMode } from '../utilities/getSelectMode.js'

packages/payload/src/utilities/flattenTopLevelFields.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,14 @@ function flattenFields<TField extends ClientField | Field>(
3333
fields: TField[],
3434
keepPresentationalFields?: boolean,
3535
): FlattenedField<TField>[] {
36-
return fields.reduce<FlattenedField<TField>[]>((fieldsToUse, field) => {
36+
return fields.reduce<FlattenedField<TField>[]>((acc, field) => {
3737
if (fieldAffectsData(field) || (keepPresentationalFields && fieldIsPresentationalOnly(field))) {
38-
return [...fieldsToUse, field as FlattenedField<TField>]
39-
}
40-
41-
if (fieldHasSubFields(field)) {
42-
return [...fieldsToUse, ...flattenFields(field.fields as TField[], keepPresentationalFields)]
43-
}
44-
45-
if (field.type === 'tabs' && 'tabs' in field) {
38+
acc.push(field as FlattenedField<TField>)
39+
} else if (fieldHasSubFields(field)) {
40+
acc.push(...flattenFields(field.fields as TField[], keepPresentationalFields))
41+
} else if (field.type === 'tabs' && 'tabs' in field) {
4642
return [
47-
...fieldsToUse,
43+
...acc,
4844
...field.tabs.reduce<FlattenedField<TField>[]>((tabFields, tab: TabType<TField>) => {
4945
if (tabHasName(tab)) {
5046
return [...tabFields, { ...tab, type: 'tab' } as unknown as FlattenedField<TField>]
@@ -58,7 +54,7 @@ function flattenFields<TField extends ClientField | Field>(
5854
]
5955
}
6056

61-
return fieldsToUse
57+
return acc
6258
}, [])
6359
}
6460

packages/ui/src/elements/ListControls/getTextFieldsToBeSearched.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
'use client'
22
import type { ClientField } from 'payload'
33

4-
import { fieldAffectsData } from 'payload/shared'
5-
6-
import { flattenFieldMap } from '../../utilities/flattenFieldMap.js'
4+
import { fieldAffectsData, flattenTopLevelFields } from 'payload/shared'
75

86
export const getTextFieldsToBeSearched = (
97
listSearchableFields: string[],
108
fields: ClientField[],
119
): ClientField[] => {
1210
if (listSearchableFields) {
13-
const flattenedFields = flattenFieldMap(fields)
11+
const flattenedFields = flattenTopLevelFields(fields) as ClientField[]
12+
1413
return flattenedFields.filter(
1514
(field) => fieldAffectsData(field) && listSearchableFields.includes(field.name),
1615
)

packages/ui/src/elements/TableColumns/buildColumnState.tsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { I18nClient } from '@payloadcms/translations'
22
import type {
33
ClientCollectionConfig,
4+
ClientField,
45
DefaultCellComponentProps,
56
DefaultServerCellComponentProps,
67
Field,
@@ -16,6 +17,7 @@ import {
1617
fieldIsHiddenOrDisabled,
1718
fieldIsID,
1819
fieldIsPresentationalOnly,
20+
flattenTopLevelFields,
1921
} from 'payload/shared'
2022
import React from 'react'
2123

@@ -31,19 +33,19 @@ import {
3133
SortColumn,
3234
// eslint-disable-next-line payload/no-imports-from-exports-dir
3335
} from '../../exports/client/index.js'
34-
import { flattenFieldMap } from '../../utilities/flattenFieldMap.js'
3536
import { RenderServerComponent } from '../RenderServerComponent/index.js'
37+
import { filterFields } from './filterFields.js'
3638

3739
type Args = {
3840
beforeRows?: Column[]
39-
collectionConfig: ClientCollectionConfig
41+
clientCollectionConfig: ClientCollectionConfig
42+
collectionConfig: SanitizedCollectionConfig
4043
columnPreferences: ColumnPreferences
4144
columns?: ColumnPreferences
4245
customCellProps: DefaultCellComponentProps['customCellProps']
4346
docs: PaginatedDocs['docs']
4447
enableRowSelections: boolean
4548
enableRowTypes?: boolean
46-
fields: Field[]
4749
i18n: I18nClient
4850
payload: Payload
4951
sortColumnProps?: Partial<SortColumnProps>
@@ -53,24 +55,29 @@ type Args = {
5355
export const buildColumnState = (args: Args): Column[] => {
5456
const {
5557
beforeRows,
58+
clientCollectionConfig,
5659
collectionConfig,
5760
columnPreferences,
5861
columns,
5962
customCellProps,
6063
docs,
6164
enableRowSelections,
62-
fields,
6365
i18n,
6466
payload,
6567
sortColumnProps,
6668
useAsTitle,
6769
} = args
6870

69-
const clientFields = collectionConfig.fields
70-
7171
// clientFields contains the fake `id` column
72-
let sortedFieldMap = flattenFieldMap(clientFields)
73-
let _sortedFieldMap = flattenFieldMap(fields) // TODO: think of a way to avoid this additional flatten
72+
let sortedFieldMap = flattenTopLevelFields(
73+
filterFields(clientCollectionConfig.fields),
74+
true,
75+
) as ClientField[]
76+
77+
let _sortedFieldMap = flattenTopLevelFields(
78+
filterFields(collectionConfig.fields),
79+
true,
80+
) as Field[] // TODO: think of a way to avoid this additional flatten
7481

7582
// place the `ID` field first, if it exists
7683
// do the same for the `useAsTitle` field with precedence over the `ID` field
@@ -180,7 +187,7 @@ export const buildColumnState = (args: Args): Column[] => {
180187

181188
const baseCellClientProps: DefaultCellComponentProps = {
182189
cellData: undefined,
183-
collectionConfig: deepCopyObjectSimple(collectionConfig),
190+
collectionConfig: deepCopyObjectSimple(clientCollectionConfig),
184191
customCellProps,
185192
field,
186193
rowData: undefined,

packages/ui/src/elements/TableColumns/filterFields.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
import type { ClientField, Field } from 'payload'
22

3-
// 1. Skips fields that are hidden, disabled, or presentational-only (i.e. `ui` fields)
4-
// 2. Maps through top-level `tabs` fields and filters out the same
3+
import { fieldIsHiddenOrDisabled, fieldIsID } from 'payload/shared'
4+
5+
/**
6+
* Filters fields that are hidden, disabled, or have `disableListColumn` set to `true`
7+
* Does so recursively for `tabs` fields.
8+
*/
59
export const filterFields = <T extends ClientField | Field>(incomingFields: T[]): T[] => {
610
const shouldSkipField = (field: T): boolean =>
7-
(field.type !== 'ui' && field.admin?.disabled === true) ||
11+
(field.type !== 'ui' && fieldIsHiddenOrDisabled(field) && !fieldIsID(field)) ||
812
field?.admin?.disableListColumn === true
913

10-
const fields: T[] = incomingFields?.reduce((formatted, field) => {
14+
const fields: T[] = incomingFields?.reduce((acc, field) => {
1115
if (shouldSkipField(field)) {
12-
return formatted
16+
return acc
1317
}
1418

19+
// extract top-level `tabs` fields and filter out the same
1520
const formattedField: T =
1621
field.type === 'tabs' && 'tabs' in field
1722
? {
@@ -23,7 +28,9 @@ export const filterFields = <T extends ClientField | Field>(incomingFields: T[])
2328
}
2429
: field
2530

26-
return [...formatted, formattedField]
31+
acc.push(formattedField)
32+
33+
return acc
2734
}, [])
2835

2936
return fields

packages/ui/src/elements/TableColumns/getInitialColumns.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ClientField, Field } from 'payload'
1+
import type { ClientField, CollectionConfig, Field } from 'payload'
22

33
import { fieldAffectsData } from 'payload/shared'
44

@@ -33,10 +33,15 @@ const getRemainingColumns = <T extends ClientField[] | Field[]>(
3333
return [...remaining, field.name]
3434
}, [])
3535

36+
/**
37+
* Returns the initial columns to display in the table based on the following criteria:
38+
* 1. If `defaultColumns` is set in the collection config, use those columns
39+
* 2. Otherwise take `useAtTitle, if set, and the next 3 fields that are not hidden or disabled
40+
*/
3641
export const getInitialColumns = <T extends ClientField[] | Field[]>(
3742
fields: T,
38-
useAsTitle: string,
39-
defaultColumns: string[],
43+
useAsTitle: CollectionConfig['admin']['useAsTitle'],
44+
defaultColumns: CollectionConfig['admin']['defaultColumns'],
4045
): ColumnPreferences => {
4146
let initialColumns = []
4247

packages/ui/src/elements/TableColumns/index.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22
import type { ClientCollectionConfig, SanitizedCollectionConfig } from 'payload'
33

4-
import React, { createContext, useCallback, useContext } from 'react'
4+
import React, { createContext, useCallback, useContext, useEffect } from 'react'
55

66
import type { ColumnPreferences } from '../../providers/ListQuery/index.js'
77
import type { SortColumnProps } from '../SortColumn/index.js'
@@ -204,6 +204,7 @@ export const TableColumnsProvider: React.FC<Props> = ({
204204

205205
return indexOfFirst > indexOfSecond ? 1 : -1
206206
})
207+
207208
const { state: columnState, Table } = await getTableState({
208209
collectionSlug,
209210
columns: activeColumns,
@@ -275,7 +276,11 @@ export const TableColumnsProvider: React.FC<Props> = ({
275276
sortColumnProps,
276277
])
277278

278-
React.useEffect(() => {
279+
useEffect(() => {
280+
setTableColumns(columnState)
281+
}, [columnState])
282+
283+
useEffect(() => {
279284
return () => {
280285
abortAndIgnore(tableStateControllerRef.current)
281286
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
'use client'
22
import type { ClientCollectionConfig, ClientField } from 'payload'
33

4-
import { flattenFieldMap } from '../utilities/flattenFieldMap.js'
4+
import { flattenTopLevelFields } from 'payload/shared'
55

66
export const useUseTitleField = (collection: ClientCollectionConfig): ClientField => {
77
const {
88
admin: { useAsTitle },
99
fields,
1010
} = collection
1111

12-
const topLevelFields = flattenFieldMap(fields)
12+
const topLevelFields = flattenTopLevelFields(fields) as ClientField[]
13+
1314
return topLevelFields?.find((field) => 'name' in field && field.name === useAsTitle)
1415
}

packages/ui/src/utilities/buildTableState.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,6 @@ export const buildTableState = async (
198198
}
199199
}
200200

201-
const fields = collectionConfig.fields
202-
203201
let docs = docsFromArgs
204202
let data: PaginatedDocs
205203

@@ -219,20 +217,20 @@ export const buildTableState = async (
219217
}
220218

221219
const { columnState, Table } = renderTable({
222-
collectionConfig: clientCollectionConfig,
220+
clientCollectionConfig,
221+
collectionConfig,
223222
columnPreferences: undefined, // TODO, might not be needed
224223
columns,
225224
docs,
226225
enableRowSelections,
227-
fields,
228226
i18n: req.i18n,
229227
payload,
230228
renderRowTypes,
231229
tableAppearance,
232230
useAsTitle: collectionConfig.admin.useAsTitle,
233231
})
234232

235-
const renderedFilters = renderFilters(fields, req.payload.importMap)
233+
const renderedFilters = renderFilters(collectionConfig.fields, req.payload.importMap)
236234

237235
return {
238236
data,

0 commit comments

Comments
 (0)