Skip to content

Commit f5a955d

Browse files
authored
fix(ui): properly filters fields from list view columns and conditions (#10246)
Fixes #10234. Some fields, such as focal point fields for upload enabled collections, were rendering in the condition selector despite being hidden from the column selector. This was because the logic for the column selector was filtering fields without labels, but the same was not being done for the filter conditions. This, however, is not a good way to filter these fields as it requires this specific logic to be written in multiple places. Instead, they need to explicitly check for `hidden` and `disabled` in addition to `disableListFilter` and `disableListColumn`. The actual filtering logic has been improved across the two instances as well, removing multiple duplicative loops. This change has also exposed a underlying issue with the way columns were handled within the table columns provider. When row selections were enabled, the selector columns were present in column state. This caused problems when interacting with column indices, such as when reordering columns. Instead of needing to manually filter these out every time we need to work with column state, they no longer appear there in the first place. Instead, we inject the row selectors directly into the table itself, completely isolating these row selectors from the column state.
1 parent 885e966 commit f5a955d

File tree

13 files changed

+164
-86
lines changed

13 files changed

+164
-86
lines changed

packages/payload/src/uploads/getBaseFields.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ export const getBaseUploadFields = ({ collection, config }: Options): Field[] =>
170170
name,
171171
type: 'number',
172172
admin: {
173+
disableListColumn: true,
174+
disableListFilter: true,
173175
hidden: true,
174176
},
175177
}

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

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

4-
import React, { useId } from 'react'
5-
6-
import type { Column } from '../Table/index.js'
4+
import { fieldIsHiddenOrDisabled, fieldIsID } from 'payload/shared'
5+
import React, { useId, useMemo } from 'react'
76

87
import { FieldLabel } from '../../fields/FieldLabel/index.js'
98
import { PlusIcon } from '../../icons/Plus/index.js'
@@ -20,24 +19,26 @@ export type Props = {
2019
readonly collectionSlug: SanitizedCollectionConfig['slug']
2120
}
2221

23-
const filterColumnFields = (columns: Column[]): Column[] => {
24-
return columns.filter((c) => {
25-
return !c?.field?.admin?.disableListColumn
26-
})
27-
}
28-
2922
export const ColumnSelector: React.FC<Props> = ({ collectionSlug }) => {
3023
const { columns, moveColumn, toggleColumn } = useTableColumns()
3124

3225
const uuid = useId()
3326
const editDepth = useEditDepth()
3427

28+
const filteredColumns = useMemo(
29+
() =>
30+
columns.filter(
31+
(col) =>
32+
!(fieldIsHiddenOrDisabled(col.field) && !fieldIsID(col.field)) &&
33+
!col?.field?.admin?.disableListColumn,
34+
),
35+
[columns],
36+
)
37+
3538
if (!columns) {
3639
return null
3740
}
3841

39-
const filteredColumns = filterColumnFields(columns)
40-
4142
return (
4243
<DraggableSortable
4344
className={baseClass}
@@ -50,21 +51,8 @@ export const ColumnSelector: React.FC<Props> = ({ collectionSlug }) => {
5051
}}
5152
>
5253
{filteredColumns.map((col, i) => {
53-
if (!col) {
54-
return null
55-
}
56-
5754
const { accessor, active, field } = col
5855

59-
if (
60-
col.accessor === '_select' ||
61-
!field ||
62-
col.CustomLabel === null ||
63-
(col.CustomLabel === undefined && !('label' in field))
64-
) {
65-
return null
66-
}
67-
6856
return (
6957
<Pill
7058
alignIcon="left"
@@ -80,7 +68,9 @@ export const ColumnSelector: React.FC<Props> = ({ collectionSlug }) => {
8068
void toggleColumn(accessor)
8169
}}
8270
>
83-
{col.CustomLabel ?? <FieldLabel label={'label' in field && field.label} unstyled />}
71+
{col.CustomLabel ?? (
72+
<FieldLabel label={field && 'label' in field && field.label} unstyled />
73+
)}
8474
</Pill>
8575
)
8676
})}

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

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -279,16 +279,6 @@ export const buildColumnState = (args: Args): Column[] => {
279279
return acc
280280
}, [])
281281

282-
if (enableRowSelections) {
283-
sorted?.unshift({
284-
accessor: '_select',
285-
active: true,
286-
field: null,
287-
Heading: <SelectAll />,
288-
renderedCells: docs.map((_, i) => <SelectRow key={i} rowData={docs[i]} />),
289-
})
290-
}
291-
292282
if (beforeRows) {
293283
sorted.unshift(...beforeRows)
294284
}

packages/ui/src/elements/WhereBuilder/Condition/index.tsx

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use client'
2-
import React, { useEffect, useMemo, useState } from 'react'
2+
import React, { useEffect, useState } from 'react'
33

44
import type { FieldCondition } from '../types.js'
55

@@ -17,9 +17,9 @@ export type Props = {
1717
}) => void
1818
readonly andIndex: number
1919
readonly fieldName: string
20-
readonly fields: FieldCondition[]
2120
readonly initialValue: string
2221
readonly operator: Operator
22+
readonly options: FieldCondition[]
2323
readonly orIndex: number
2424
readonly removeCondition: ({ andIndex, orIndex }: { andIndex: number; orIndex: number }) => void
2525
readonly RenderedFilter: React.ReactNode
@@ -56,21 +56,17 @@ export const Condition: React.FC<Props> = (props) => {
5656
addCondition,
5757
andIndex,
5858
fieldName,
59-
fields,
6059
initialValue,
6160
operator,
61+
options,
6262
orIndex,
6363
removeCondition,
6464
RenderedFilter,
6565
updateCondition,
6666
} = props
6767

68-
const options = useMemo(() => {
69-
return fields.filter(({ field }) => !field.admin.disableListFilter)
70-
}, [fields])
71-
72-
const [internalField, setInternalField] = useState<FieldCondition>(() =>
73-
fields.find((field) => fieldName === field.value),
68+
const [fieldOption, setFieldOption] = useState<FieldCondition>(() =>
69+
options.find((field) => fieldName === field.value),
7470
)
7571

7672
const { t } = useTranslation()
@@ -82,13 +78,13 @@ export const Condition: React.FC<Props> = (props) => {
8278
useEffect(() => {
8379
// This is to trigger changes when the debounced value changes
8480
if (
85-
(internalField?.value || typeof internalField?.value === 'number') &&
81+
(fieldOption?.value || typeof fieldOption?.value === 'number') &&
8682
internalOperatorOption &&
8783
![null, undefined].includes(debouncedValue)
8884
) {
8985
updateCondition({
9086
andIndex,
91-
fieldName: internalField.value,
87+
fieldName: fieldOption.value,
9288
operator: internalOperatorOption,
9389
orIndex,
9490
value: debouncedValue,
@@ -97,15 +93,15 @@ export const Condition: React.FC<Props> = (props) => {
9793
}, [
9894
debouncedValue,
9995
andIndex,
100-
internalField?.value,
96+
fieldOption?.value,
10197
internalOperatorOption,
10298
orIndex,
10399
updateCondition,
104100
operator,
105101
])
106102

107103
const booleanSelect =
108-
['exists'].includes(internalOperatorOption) || internalField?.field?.type === 'checkbox'
104+
['exists'].includes(internalOperatorOption) || fieldOption?.field?.type === 'checkbox'
109105

110106
let valueOptions
111107

@@ -114,13 +110,13 @@ export const Condition: React.FC<Props> = (props) => {
114110
{ label: t('general:true'), value: 'true' },
115111
{ label: t('general:false'), value: 'false' },
116112
]
117-
} else if (internalField?.field && 'options' in internalField.field) {
118-
valueOptions = internalField.field.options
113+
} else if (fieldOption?.field && 'options' in fieldOption.field) {
114+
valueOptions = fieldOption.field.options
119115
}
120116

121117
const disabled =
122-
(!internalField?.value && typeof internalField?.value !== 'number') ||
123-
internalField?.field?.admin?.disableListFilter
118+
(!fieldOption?.value && typeof fieldOption?.value !== 'number') ||
119+
fieldOption?.field?.admin?.disableListFilter
124120

125121
return (
126122
<div className={baseClass}>
@@ -131,14 +127,14 @@ export const Condition: React.FC<Props> = (props) => {
131127
disabled={disabled}
132128
isClearable={false}
133129
onChange={(field: Option) => {
134-
setInternalField(fields.find((f) => f.value === field.value))
130+
setFieldOption(options.find((f) => f.value === field.value))
135131
setInternalOperatorOption(undefined)
136132
setInternalQueryValue(undefined)
137133
}}
138-
options={options}
134+
options={options.filter((field) => !field.field.admin.disableListFilter)}
139135
value={
140-
fields.find((field) => internalField?.value === field.value) || {
141-
value: internalField?.value,
136+
options.find((field) => fieldOption?.value === field.value) || {
137+
value: fieldOption?.value,
142138
}
143139
}
144140
/>
@@ -150,9 +146,9 @@ export const Condition: React.FC<Props> = (props) => {
150146
onChange={(operator: Option<Operator>) => {
151147
setInternalOperatorOption(operator.value)
152148
}}
153-
options={internalField?.operators}
149+
options={fieldOption?.operators}
154150
value={
155-
internalField?.operators.find(
151+
fieldOption?.operators.find(
156152
(operator) => internalOperatorOption === operator.value,
157153
) || null
158154
}
@@ -162,8 +158,12 @@ export const Condition: React.FC<Props> = (props) => {
162158
{RenderedFilter || (
163159
<DefaultFilter
164160
booleanSelect={booleanSelect}
165-
disabled={!internalOperatorOption || internalField?.field?.admin?.disableListFilter}
166-
internalField={internalField}
161+
disabled={
162+
!internalOperatorOption ||
163+
!fieldOption ||
164+
fieldOption?.field?.admin?.disableListFilter
165+
}
166+
internalField={fieldOption}
167167
onChange={setInternalQueryValue}
168168
operator={internalOperatorOption}
169169
options={valueOptions}
@@ -194,7 +194,7 @@ export const Condition: React.FC<Props> = (props) => {
194194
onClick={() =>
195195
addCondition({
196196
andIndex: andIndex + 1,
197-
fieldName: fields.find((field) => !field.field.admin?.disableListFilter).value,
197+
fieldName: options.find((field) => !field.field.admin?.disableListFilter).value,
198198
orIndex,
199199
relation: 'and',
200200
})

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

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ import { useListQuery } from '../../providers/ListQuery/index.js'
1010
import { useTranslation } from '../../providers/Translation/index.js'
1111
import { Button } from '../Button/index.js'
1212
import { Condition } from './Condition/index.js'
13-
import './index.scss'
1413
import { reduceClientFields } from './reduceClientFields.js'
1514
import { transformWhereQuery } from './transformWhereQuery.js'
1615
import validateWhereQuery from './validateWhereQuery.js'
16+
import './index.scss'
1717

1818
const baseClass = 'where-builder'
1919

@@ -27,10 +27,10 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
2727
const { collectionPluralLabel, fields, renderedFilters } = props
2828
const { i18n, t } = useTranslation()
2929

30-
const [reducedFields, setReducedColumns] = useState(() => reduceClientFields({ fields, i18n }))
30+
const [options, setOptions] = useState(() => reduceClientFields({ fields, i18n }))
3131

3232
useEffect(() => {
33-
setReducedColumns(reduceClientFields({ fields, i18n }))
33+
setOptions(reduceClientFields({ fields, i18n }))
3434
}, [fields, i18n])
3535

3636
const { handleWhereChange, query } = useListQuery()
@@ -185,9 +185,9 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
185185
addCondition={addCondition}
186186
andIndex={andIndex}
187187
fieldName={initialFieldName}
188-
fields={reducedFields}
189188
initialValue={initialValue}
190189
operator={initialOperator}
190+
options={options}
191191
orIndex={orIndex}
192192
removeCondition={removeCondition}
193193
RenderedFilter={renderedFilters?.get(initialFieldName)}
@@ -210,7 +210,7 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
210210
onClick={() => {
211211
addCondition({
212212
andIndex: 0,
213-
fieldName: reducedFields[0].value,
213+
fieldName: options[0].value,
214214
orIndex: conditions.length,
215215
relation: 'or',
216216
})
@@ -230,11 +230,10 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
230230
iconPosition="left"
231231
iconStyle="with-border"
232232
onClick={() => {
233-
if (reducedFields.length > 0) {
233+
if (options.length > 0) {
234234
addCondition({
235235
andIndex: 0,
236-
fieldName: reducedFields.find((field) => !field.field.admin?.disableListFilter)
237-
.value,
236+
fieldName: options.find((field) => !field.field.admin?.disableListFilter).value,
238237
orIndex: conditions.length,
239238
relation: 'or',
240239
})

packages/ui/src/elements/WhereBuilder/reduceClientFields.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const reduceClientFields = ({
2929
pathPrefix,
3030
}: ReduceClientFieldsArgs): FieldCondition[] => {
3131
return fields.reduce((reduced, field) => {
32+
// Do not filter out `field.admin.disableListFilter` fields here, as these should still render as disabled if they appear in the URL query
3233
if (fieldIsHiddenOrDisabled(field) && !fieldIsID(field)) {
3334
return reduced
3435
}

packages/ui/src/utilities/renderTable.tsx

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { buildColumnState } from '../elements/TableColumns/buildColumnState.js'
1919
import { filterFields } from '../elements/TableColumns/filterFields.js'
2020
import { getInitialColumns } from '../elements/TableColumns/getInitialColumns.js'
2121
// eslint-disable-next-line payload/no-imports-from-exports-dir
22-
import { Pill, Table } from '../exports/client/index.js'
22+
import { Pill, SelectAll, SelectRow, Table } from '../exports/client/index.js'
2323

2424
export const renderFilters = (
2525
fields: Field[],
@@ -91,19 +91,6 @@ export const renderTable = ({
9191
)
9292

9393
const columnState = buildColumnState({
94-
beforeRows: renderRowTypes
95-
? [
96-
{
97-
accessor: 'collection',
98-
active: true,
99-
field: null,
100-
Heading: i18n.t('version:type'),
101-
renderedCells: docs.map((_, i) => (
102-
<Pill key={i}>{getTranslation(clientCollectionConfig.labels.singular, i18n)}</Pill>
103-
)),
104-
},
105-
]
106-
: undefined,
10794
clientCollectionConfig,
10895
collectionConfig,
10996
columnPreferences,
@@ -117,8 +104,42 @@ export const renderTable = ({
117104
useAsTitle,
118105
})
119106

107+
const columnsToUse = [...columnState]
108+
109+
if (renderRowTypes) {
110+
columnsToUse.unshift({
111+
accessor: 'collection',
112+
active: true,
113+
field: {
114+
admin: {
115+
disabled: true,
116+
},
117+
hidden: true,
118+
},
119+
Heading: i18n.t('version:type'),
120+
renderedCells: docs.map((_, i) => (
121+
<Pill key={i}>{getTranslation(clientCollectionConfig.labels.singular, i18n)}</Pill>
122+
)),
123+
} as Column)
124+
}
125+
126+
if (enableRowSelections) {
127+
columnsToUse.unshift({
128+
accessor: '_select',
129+
active: true,
130+
field: {
131+
admin: {
132+
disabled: true,
133+
},
134+
hidden: true,
135+
},
136+
Heading: <SelectAll />,
137+
renderedCells: docs.map((_, i) => <SelectRow key={i} rowData={docs[i]} />),
138+
} as Column)
139+
}
140+
120141
return {
121142
columnState,
122-
Table: <Table appearance={tableAppearance} columns={columnState} data={docs} />,
143+
Table: <Table appearance={tableAppearance} columns={columnsToUse} data={docs} />,
123144
}
124145
}

0 commit comments

Comments
 (0)