feat: simplify column prefs (#11390)

Transforms how column prefs are stored in the database. This change
reduces the complexity of the `columns` property by removing the
unnecessary `accessor` and `active` keys.

This change is necessary in order to [maintain column state in the
URL](https://github.com/payloadcms/payload/pull/11387), where the state
itself needs to be as concise as possible. Does so in a non-breaking
way, where the old column shape is transformed as needed.

Here's an example:

Before:

```ts
[
  {
    accessor: "title",
    active: true
  }
]
```

After:

```ts
[
  {
    title: true
  }
]
```
This commit is contained in:
Jacob Fletcher
2025-02-25 15:18:14 -05:00
committed by GitHub
parent 48f183bd42
commit 69c0d09437
14 changed files with 84 additions and 79 deletions

View File

@@ -1,7 +1,7 @@
import type { ImportMap } from '../../bin/generateImportMap/index.js'
import type { SanitizedConfig } from '../../config/types.js'
import type { PaginatedDocs } from '../../database/types.js'
import type { CollectionSlug } from '../../index.js'
import type { CollectionSlug, ColumnPreference } from '../../index.js'
import type { PayloadRequest, Sort, Where } from '../../types/index.js'
export type DefaultServerFunctionArgs = {
@@ -50,7 +50,7 @@ export type ListQuery = {
export type BuildTableStateArgs = {
collectionSlug: string | string[]
columns?: { accessor: string; active: boolean }[]
columns?: ColumnPreference[]
docs?: PaginatedDocs['docs']
enableRowSelections?: boolean
parent?: {

View File

@@ -1374,6 +1374,7 @@ export { restoreVersionOperation as restoreVersionOperationGlobal } from './glob
export { updateOperation as updateOperationGlobal } from './globals/operations/update.js'
export type {
CollapsedPreferences,
ColumnPreference,
DocumentPreferences,
FieldsPreferences,
InsideFieldsPreferences,

View File

@@ -0,0 +1,19 @@
/**
* @todo remove this function and subsequent hooks in v4
* They are used to transform the old shape of `columnPreferences` to new shape
* i.e. ({ accessor: string, active: boolean })[] to ({ [accessor: string]: boolean })[]
* In v4 can we use the new shape directly
*/
export const migrateColumns = (value: Record<string, any>) => {
if (value && typeof value === 'object' && 'columns' in value && Array.isArray(value.columns)) {
value.columns = value.columns.map((col) => {
if ('accessor' in col) {
return { [col.accessor]: col.active }
}
return col
})
}
return value
}

View File

@@ -2,6 +2,7 @@
import type { CollectionConfig } from '../collections/config/types.js'
import type { Access, Config } from '../config/types.js'
import { migrateColumns } from './migrateColumns.js'
import { deleteHandler } from './requestHandlers/delete.js'
import { findByIDHandler } from './requestHandlers/findOne.js'
import { updateHandler } from './requestHandlers/update.js'
@@ -76,6 +77,14 @@ const getPreferencesCollection = (config: Config): CollectionConfig => ({
{
name: 'value',
type: 'json',
/**
* @todo remove these hooks in v4
* See `migrateColumns` for more information
*/
hooks: {
afterRead: [({ value }) => migrateColumns(value)],
beforeValidate: [({ value }) => migrateColumns(value)],
},
validate: (value) => {
if (value) {
try {

View File

@@ -28,8 +28,12 @@ export type DocumentPreferences = {
fields: FieldsPreferences
}
export type ColumnPreference = {
[key: string]: boolean
}
export type ListPreferences = {
columns?: { accessor: string; active: boolean }[]
columns?: ColumnPreference[]
limit?: number
sort?: string
}

View File

@@ -2,6 +2,7 @@
import type {
CollectionSlug,
Column,
ColumnPreference,
JoinFieldClient,
ListQuery,
PaginatedDocs,
@@ -25,7 +26,6 @@ import { useServerFunctions } from '../../providers/ServerFunctions/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { hoistQueryParamsToAnd } from '../../utilities/mergeListSearchAndWhere.js'
import { AnimateHeight } from '../AnimateHeight/index.js'
import './index.scss'
import { ColumnSelector } from '../ColumnSelector/index.js'
import { useDocumentDrawer } from '../DocumentDrawer/index.js'
import { Popup, PopupList } from '../Popup/index.js'
@@ -33,6 +33,7 @@ import { RelationshipProvider } from '../Table/RelationshipProvider/index.js'
import { TableColumnsProvider } from '../TableColumns/index.js'
import { DrawerLink } from './cells/DrawerLink/index.js'
import { RelationshipTablePagination } from './Pagination.js'
import './index.scss'
const baseClass = 'relationship-table'
@@ -123,11 +124,10 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
newQuery.where = hoistQueryParamsToAnd(newQuery.where, filterOptions)
}
// map columns from string[] to ListPreferences['columns']
const defaultColumns = field.admin.defaultColumns
// map columns from string[] to ColumnPreference[]
const defaultColumns: ColumnPreference[] = field.admin.defaultColumns
? field.admin.defaultColumns.map((accessor) => ({
accessor,
active: true,
[accessor]: true,
}))
: undefined

View File

@@ -4,10 +4,10 @@ import type {
ClientComponentProps,
ClientField,
Column,
ColumnPreference,
DefaultCellComponentProps,
DefaultServerCellComponentProps,
Field,
ListPreferences,
PaginatedDocs,
Payload,
SanitizedCollectionConfig,
@@ -39,8 +39,8 @@ type Args = {
beforeRows?: Column[]
clientCollectionConfig: ClientCollectionConfig
collectionConfig: SanitizedCollectionConfig
columnPreferences: ListPreferences['columns']
columns?: ListPreferences['columns']
columnPreferences: ColumnPreference[]
columns?: ColumnPreference[]
customCellProps: DefaultCellComponentProps['customCellProps']
docs: PaginatedDocs['docs']
enableRowSelections: boolean
@@ -99,10 +99,10 @@ export const buildColumnState = (args: Args): Column[] => {
const sortTo = columnPreferences || columns
const sortFieldMap = (fieldMap, sortTo) =>
const sortFieldMap = (fieldMap, sortTo: ColumnPreference[]) =>
fieldMap?.sort((a, b) => {
const aIndex = sortTo.findIndex((column) => 'name' in a && column.accessor === a.name)
const bIndex = sortTo.findIndex((column) => 'name' in b && column.accessor === b.name)
const aIndex = sortTo.findIndex((column) => 'name' in a && a.name in column)
const bIndex = sortTo.findIndex((column) => 'name' in b && b.name in column)
if (aIndex === -1 && bIndex === -1) {
return 0
@@ -136,18 +136,12 @@ export const buildColumnState = (args: Args): Column[] => {
(f) => 'name' in field && 'name' in f && f.name === field.name,
)
const columnPreference = columnPreferences?.find(
(preference) => field && 'name' in field && preference.accessor === field.name,
)
let active = false
if (columnPreference) {
active = columnPreference.active
if (columnPreferences) {
active = 'name' in field && columnPreferences?.some((col) => col?.[field.name])
} else if (columns && Array.isArray(columns) && columns.length > 0) {
active = columns.find(
(column) => field && 'name' in field && column.accessor === field.name,
)?.active
active = 'name' in field && columns.some((col) => col?.[field.name])
} else if (activeColumnsIndices.length < 4) {
active = true
}

View File

@@ -4,10 +4,10 @@ import type { I18nClient } from '@payloadcms/translations'
import type {
ClientField,
Column,
ColumnPreference,
DefaultCellComponentProps,
DefaultServerCellComponentProps,
Field,
ListPreferences,
PaginatedDocs,
Payload,
SanitizedCollectionConfig,
@@ -36,8 +36,8 @@ import { filterFields } from './filterFields.js'
type Args = {
beforeRows?: Column[]
columnPreferences: ListPreferences['columns']
columns?: ListPreferences['columns']
columnPreferences: ColumnPreference[]
columns?: ColumnPreference[]
customCellProps: DefaultCellComponentProps['customCellProps']
docs: PaginatedDocs['docs']
enableRowSelections: boolean
@@ -92,8 +92,8 @@ export const buildPolymorphicColumnState = (args: Args): Column[] => {
const sortFieldMap = (fieldMap, sortTo) =>
fieldMap?.sort((a, b) => {
const aIndex = sortTo.findIndex((column) => 'name' in a && column.accessor === a.name)
const bIndex = sortTo.findIndex((column) => 'name' in b && column.accessor === b.name)
const aIndex = sortTo.findIndex((column) => 'name' in a && a.name in column)
const bIndex = sortTo.findIndex((column) => 'name' in b && b.name in column)
if (aIndex === -1 && bIndex === -1) {
return 0
@@ -127,18 +127,12 @@ export const buildPolymorphicColumnState = (args: Args): Column[] => {
(f) => 'name' in field && 'name' in f && f.name === field.name,
)
const columnPreference = columnPreferences?.find(
(preference) => field && 'name' in field && preference.accessor === field.name,
)
let active = false
if (columnPreference) {
active = columnPreference.active
if (columnPreferences) {
active = 'name' in field && columnPreferences?.some((col) => col?.[field.name])
} else if (columns && Array.isArray(columns) && columns.length > 0) {
active = columns.find(
(column) => field && 'name' in field && column.accessor === field.name,
)?.active
active = 'name' in field && columns.some((col) => col?.[field.name])
} else if (activeColumnsIndices.length < 4) {
active = true
}

View File

@@ -1,11 +1,11 @@
import type { ClientField, CollectionConfig, Field, ListPreferences } from 'payload'
import type { ClientField, CollectionConfig, ColumnPreference, Field } from 'payload'
import { fieldAffectsData } from 'payload/shared'
const getRemainingColumns = <T extends ClientField[] | Field[]>(
fields: T,
useAsTitle: string,
): ListPreferences['columns'] =>
): ColumnPreference[] =>
fields?.reduce((remaining, field) => {
if (fieldAffectsData(field) && field.name === useAsTitle) {
return remaining
@@ -40,7 +40,7 @@ export const getInitialColumns = <T extends ClientField[] | Field[]>(
fields: T,
useAsTitle: CollectionConfig['admin']['useAsTitle'],
defaultColumns: CollectionConfig['admin']['defaultColumns'],
): ListPreferences['columns'] => {
): ColumnPreference[] => {
let initialColumns = []
if (Array.isArray(defaultColumns) && defaultColumns.length >= 1) {
@@ -57,7 +57,6 @@ export const getInitialColumns = <T extends ClientField[] | Field[]>(
}
return initialColumns.map((column) => ({
accessor: column,
active: true,
[column]: true,
}))
}

View File

@@ -1,5 +1,5 @@
'use client'
import type { Column, ListPreferences, SanitizedCollectionConfig } from 'payload'
import type { Column, ColumnPreference, ListPreferences, SanitizedCollectionConfig } from 'payload'
import React, { createContext, useCallback, useContext, useEffect } from 'react'
@@ -39,12 +39,10 @@ type Props = {
}
// strip out Heading, Label, and renderedCells properties, they cannot be sent to the server
const sanitizeColumns = (columns: Column[]) => {
return columns.map(({ accessor, active }) => ({
accessor,
active,
const formatColumnPreferences = (columns: Column[]): ColumnPreference[] =>
columns.map(({ accessor, active }) => ({
[accessor]: active,
}))
}
export const TableColumnsProvider: React.FC<Props> = ({
children,
@@ -90,7 +88,7 @@ export const TableColumnsProvider: React.FC<Props> = ({
const result = await getTableState({
collectionSlug,
columns: sanitizeColumns(withMovedColumn),
columns: formatColumnPreferences(withMovedColumn),
docs,
enableRowSelections,
renderRowTypes,
@@ -123,7 +121,7 @@ export const TableColumnsProvider: React.FC<Props> = ({
const { newColumnState, toggledColumns } = tableColumns.reduce<{
newColumnState: Column[]
toggledColumns: Pick<Column, 'accessor' | 'active'>[]
toggledColumns: ColumnPreference[]
}>(
(acc, col) => {
if (col.accessor === column) {
@@ -133,14 +131,12 @@ export const TableColumnsProvider: React.FC<Props> = ({
active: !col.active,
})
acc.toggledColumns.push({
accessor: col.accessor,
active: !col.active,
[col.accessor]: !col.active,
})
} else {
acc.newColumnState.push(col)
acc.toggledColumns.push({
accessor: col.accessor,
active: col.active,
[col.accessor]: col.active,
})
}
@@ -182,14 +178,8 @@ export const TableColumnsProvider: React.FC<Props> = ({
const setActiveColumns = React.useCallback(
async (activeColumnAccessors: string[]) => {
const activeColumns: Pick<Column, 'accessor' | 'active'>[] = tableColumns
.map((col) => {
return {
accessor: col.accessor,
active: activeColumnAccessors.includes(col.accessor),
}
})
.sort((first, second) => {
const activeColumns: ColumnPreference[] = formatColumnPreferences(
tableColumns.sort((first, second) => {
const indexOfFirst = activeColumnAccessors.indexOf(first.accessor)
const indexOfSecond = activeColumnAccessors.indexOf(second.accessor)
@@ -198,7 +188,8 @@ export const TableColumnsProvider: React.FC<Props> = ({
}
return indexOfFirst > indexOfSecond ? 1 : -1
})
}),
)
const { state: columnState, Table } = await getTableState({
collectionSlug,
@@ -239,7 +230,7 @@ export const TableColumnsProvider: React.FC<Props> = ({
if (collectionHasChanged || !listPreferences) {
const currentPreferences = await getPreference<{
columns: ListPreferences['columns']
columns: ColumnPreference[]
}>(preferenceKey)
prevCollection.current = defaultCollection

View File

@@ -3,9 +3,10 @@ import type {
ClientConfig,
ClientField,
CollectionConfig,
Column,
ColumnPreference,
Field,
ImportMap,
ListPreferences,
PaginatedDocs,
Payload,
SanitizedCollectionConfig,
@@ -14,9 +15,6 @@ import type {
import { getTranslation, type I18nClient } from '@payloadcms/translations'
import { fieldAffectsData, fieldIsHiddenOrDisabled, flattenTopLevelFields } from 'payload/shared'
// eslint-disable-next-line payload/no-imports-from-exports-dir
import type { Column } from '../exports/client/index.js'
import { RenderServerComponent } from '../elements/RenderServerComponent/index.js'
import { buildColumnState } from '../elements/TableColumns/buildColumnState.js'
import { buildPolymorphicColumnState } from '../elements/TableColumns/buildPolymorphicColumnState.js'
@@ -71,8 +69,8 @@ export const renderTable = ({
clientConfig?: ClientConfig
collectionConfig?: SanitizedCollectionConfig
collections?: string[]
columnPreferences: ListPreferences['columns']
columns?: ListPreferences['columns']
columnPreferences: ColumnPreference[]
columns?: ColumnPreference[]
customCellProps?: Record<string, any>
docs: PaginatedDocs['docs']
drawerSlug?: string
@@ -109,7 +107,7 @@ export const renderTable = ({
const columns = columnsFromArgs
? columnsFromArgs?.filter((column) =>
flattenTopLevelFields(fields, true)?.some(
(field) => 'name' in field && field.name === column.accessor,
(field) => 'name' in field && column[field.name],
),
)
: getInitialColumns(fields, useAsTitle, [])
@@ -130,7 +128,7 @@ export const renderTable = ({
const columns = columnsFromArgs
? columnsFromArgs?.filter((column) =>
flattenTopLevelFields(clientCollectionConfig.fields, true)?.some(
(field) => 'name' in field && field.name === column.accessor,
(field) => 'name' in field && field.name in column,
),
)
: getInitialColumns(

View File

@@ -151,6 +151,7 @@ export function DefaultListView(props: ListViewClientProps) {
])
}
}, [setStepNav, labels, drawerDepth])
return (
<Fragment>
<TableColumnsProvider

View File

@@ -170,12 +170,7 @@ describe('Text', () => {
user: client.user,
key: 'text-fields-list',
value: {
columns: [
{
accessor: 'disableListColumnText',
active: true,
},
],
columns: [{ disableListColumnText: true }],
},
})

View File

@@ -31,7 +31,7 @@
}
],
"paths": {
"@payload-config": ["./test/fields-relationship/config.ts"],
"@payload-config": ["./test/_community/config.ts"],
"@payloadcms/live-preview": ["./packages/live-preview/src"],
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
"@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],