Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ui): refreshes column state during hmr and respects admin.disableListColumn despite preferences #9846

Merged
merged 10 commits into from
Dec 10, 2024
4 changes: 2 additions & 2 deletions packages/next/src/views/List/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,13 +171,13 @@ export const renderListView = async (
const clientCollectionConfig = clientConfig.collections.find((c) => c.slug === collectionSlug)

const { columnState, Table } = renderTable({
collectionConfig: clientCollectionConfig,
clientCollectionConfig,
collectionConfig,
columnPreferences: listPreferences?.columns,
customCellProps,
docs: data.docs,
drawerSlug,
enableRowSelections,
fields,
i18n: req.i18n,
payload,
useAsTitle,
Expand Down
2 changes: 2 additions & 0 deletions packages/payload/src/exports/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export {
} from '../utilities/deepMerge.js'

export { fieldSchemaToJSON } from '../utilities/fieldSchemaToJSON.js'
export { flattenAllFields } from '../utilities/flattenAllFields.js'
export { default as flattenTopLevelFields } from '../utilities/flattenTopLevelFields.js'

export { getDataByPath } from '../utilities/getDataByPath.js'
export { getSelectMode } from '../utilities/getSelectMode.js'
Expand Down
18 changes: 7 additions & 11 deletions packages/payload/src/utilities/flattenTopLevelFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,14 @@ function flattenFields<TField extends ClientField | Field>(
fields: TField[],
keepPresentationalFields?: boolean,
): FlattenedField<TField>[] {
return fields.reduce<FlattenedField<TField>[]>((fieldsToUse, field) => {
return fields.reduce<FlattenedField<TField>[]>((acc, field) => {
if (fieldAffectsData(field) || (keepPresentationalFields && fieldIsPresentationalOnly(field))) {
return [...fieldsToUse, field as FlattenedField<TField>]
}

if (fieldHasSubFields(field)) {
return [...fieldsToUse, ...flattenFields(field.fields as TField[], keepPresentationalFields)]
}

if (field.type === 'tabs' && 'tabs' in field) {
acc.push(field as FlattenedField<TField>)
} else if (fieldHasSubFields(field)) {
acc.push(...flattenFields(field.fields as TField[], keepPresentationalFields))
} else if (field.type === 'tabs' && 'tabs' in field) {
return [
...fieldsToUse,
...acc,
...field.tabs.reduce<FlattenedField<TField>[]>((tabFields, tab: TabType<TField>) => {
if (tabHasName(tab)) {
return [...tabFields, { ...tab, type: 'tab' } as unknown as FlattenedField<TField>]
Expand All @@ -58,7 +54,7 @@ function flattenFields<TField extends ClientField | Field>(
]
}

return fieldsToUse
return acc
}, [])
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
'use client'
import type { ClientField } from 'payload'

import { fieldAffectsData } from 'payload/shared'

import { flattenFieldMap } from '../../utilities/flattenFieldMap.js'
import { fieldAffectsData, flattenTopLevelFields } from 'payload/shared'

export const getTextFieldsToBeSearched = (
listSearchableFields: string[],
fields: ClientField[],
): ClientField[] => {
if (listSearchableFields) {
const flattenedFields = flattenFieldMap(fields)
const flattenedFields = flattenTopLevelFields(fields) as ClientField[]

return flattenedFields.filter(
(field) => fieldAffectsData(field) && listSearchableFields.includes(field.name),
)
Expand Down
25 changes: 16 additions & 9 deletions packages/ui/src/elements/TableColumns/buildColumnState.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { I18nClient } from '@payloadcms/translations'
import type {
ClientCollectionConfig,
ClientField,
DefaultCellComponentProps,
DefaultServerCellComponentProps,
Field,
Expand All @@ -16,6 +17,7 @@ import {
fieldIsHiddenOrDisabled,
fieldIsID,
fieldIsPresentationalOnly,
flattenTopLevelFields,
} from 'payload/shared'
import React from 'react'

Expand All @@ -31,19 +33,19 @@ import {
SortColumn,
// eslint-disable-next-line payload/no-imports-from-exports-dir
} from '../../exports/client/index.js'
import { flattenFieldMap } from '../../utilities/flattenFieldMap.js'
import { RenderServerComponent } from '../RenderServerComponent/index.js'
import { filterFields } from './filterFields.js'

type Args = {
beforeRows?: Column[]
collectionConfig: ClientCollectionConfig
clientCollectionConfig: ClientCollectionConfig
collectionConfig: SanitizedCollectionConfig
columnPreferences: ColumnPreferences
columns?: ColumnPreferences
customCellProps: DefaultCellComponentProps['customCellProps']
docs: PaginatedDocs['docs']
enableRowSelections: boolean
enableRowTypes?: boolean
fields: Field[]
i18n: I18nClient
payload: Payload
sortColumnProps?: Partial<SortColumnProps>
Expand All @@ -53,24 +55,29 @@ type Args = {
export const buildColumnState = (args: Args): Column[] => {
const {
beforeRows,
clientCollectionConfig,
collectionConfig,
columnPreferences,
columns,
customCellProps,
docs,
enableRowSelections,
fields,
i18n,
payload,
sortColumnProps,
useAsTitle,
} = args

const clientFields = collectionConfig.fields

// clientFields contains the fake `id` column
let sortedFieldMap = flattenFieldMap(clientFields)
let _sortedFieldMap = flattenFieldMap(fields) // TODO: think of a way to avoid this additional flatten
let sortedFieldMap = flattenTopLevelFields(
filterFields(clientCollectionConfig.fields),
true,
) as ClientField[]

let _sortedFieldMap = flattenTopLevelFields(
filterFields(collectionConfig.fields),
true,
) as Field[] // TODO: think of a way to avoid this additional flatten

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

const baseCellClientProps: DefaultCellComponentProps = {
cellData: undefined,
collectionConfig: deepCopyObjectSimple(collectionConfig),
collectionConfig: deepCopyObjectSimple(clientCollectionConfig),
customCellProps,
field,
rowData: undefined,
Expand Down
19 changes: 13 additions & 6 deletions packages/ui/src/elements/TableColumns/filterFields.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import type { ClientField, Field } from 'payload'

// 1. Skips fields that are hidden, disabled, or presentational-only (i.e. `ui` fields)
// 2. Maps through top-level `tabs` fields and filters out the same
import { fieldIsHiddenOrDisabled, fieldIsID } from 'payload/shared'

/**
* Filters fields that are hidden, disabled, or have `disableListColumn` set to `true`
* Does so recursively for `tabs` fields.
*/
export const filterFields = <T extends ClientField | Field>(incomingFields: T[]): T[] => {
const shouldSkipField = (field: T): boolean =>
(field.type !== 'ui' && field.admin?.disabled === true) ||
(field.type !== 'ui' && fieldIsHiddenOrDisabled(field) && !fieldIsID(field)) ||
field?.admin?.disableListColumn === true

const fields: T[] = incomingFields?.reduce((formatted, field) => {
const fields: T[] = incomingFields?.reduce((acc, field) => {
if (shouldSkipField(field)) {
return formatted
return acc
}

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

return [...formatted, formattedField]
acc.push(formattedField)

return acc
}, [])

return fields
Expand Down
11 changes: 8 additions & 3 deletions packages/ui/src/elements/TableColumns/getInitialColumns.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ClientField, Field } from 'payload'
import type { ClientField, CollectionConfig, Field } from 'payload'

import { fieldAffectsData } from 'payload/shared'

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

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

Expand Down
9 changes: 7 additions & 2 deletions packages/ui/src/elements/TableColumns/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'
import type { ClientCollectionConfig, SanitizedCollectionConfig } from 'payload'

import React, { createContext, useCallback, useContext } from 'react'
import React, { createContext, useCallback, useContext, useEffect } from 'react'

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

return indexOfFirst > indexOfSecond ? 1 : -1
})

const { state: columnState, Table } = await getTableState({
collectionSlug,
columns: activeColumns,
Expand Down Expand Up @@ -275,7 +276,11 @@ export const TableColumnsProvider: React.FC<Props> = ({
sortColumnProps,
])

React.useEffect(() => {
useEffect(() => {
setTableColumns(columnState)
}, [columnState])

useEffect(() => {
return () => {
abortAndIgnore(tableStateControllerRef.current)
}
Expand Down
5 changes: 3 additions & 2 deletions packages/ui/src/hooks/useUseAsTitle.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
'use client'
import type { ClientCollectionConfig, ClientField } from 'payload'

import { flattenFieldMap } from '../utilities/flattenFieldMap.js'
import { flattenTopLevelFields } from 'payload/shared'

export const useUseTitleField = (collection: ClientCollectionConfig): ClientField => {
const {
admin: { useAsTitle },
fields,
} = collection

const topLevelFields = flattenFieldMap(fields)
const topLevelFields = flattenTopLevelFields(fields) as ClientField[]

return topLevelFields?.find((field) => 'name' in field && field.name === useAsTitle)
}
8 changes: 3 additions & 5 deletions packages/ui/src/utilities/buildTableState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,6 @@ export const buildTableState = async (
}
}

const fields = collectionConfig.fields

let docs = docsFromArgs
let data: PaginatedDocs

Expand All @@ -219,20 +217,20 @@ export const buildTableState = async (
}

const { columnState, Table } = renderTable({
collectionConfig: clientCollectionConfig,
clientCollectionConfig,
collectionConfig,
columnPreferences: undefined, // TODO, might not be needed
columns,
docs,
enableRowSelections,
fields,
i18n: req.i18n,
payload,
renderRowTypes,
tableAppearance,
useAsTitle: collectionConfig.admin.useAsTitle,
})

const renderedFilters = renderFilters(fields, req.payload.importMap)
const renderedFilters = renderFilters(collectionConfig.fields, req.payload.importMap)

return {
data,
Expand Down
38 changes: 0 additions & 38 deletions packages/ui/src/utilities/flattenFieldMap.ts

This file was deleted.

Loading
Loading