Skip to content

Commit 6c83046

Browse files
fix(ui): where builder crashing with invalid queries (#14342)
Fixes #14278 Fixes issues with filtering on relationship fields in the list view.
1 parent 5caebd1 commit 6c83046

File tree

11 files changed

+213
-60
lines changed

11 files changed

+213
-60
lines changed
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
11
'use client'
2+
import { getTranslation } from '@payloadcms/translations'
23
import React from 'react'
34

45
import type { DateFilterProps as Props } from './types.js'
56

7+
import { useTranslation } from '../../../../providers/Translation/index.js'
68
import { DatePickerField } from '../../../DatePicker/index.js'
79

810
const baseClass = 'condition-value-date'
911

1012
export const DateFilter: React.FC<Props> = ({ disabled, field: { admin }, onChange, value }) => {
1113
const { date } = admin || {}
14+
const { i18n, t } = useTranslation()
1215

1316
return (
1417
<div className={baseClass}>
15-
<DatePickerField {...date} onChange={onChange} readOnly={disabled} value={value as Date} />
18+
<DatePickerField
19+
{...date}
20+
onChange={onChange}
21+
placeholder={getTranslation(admin.placeholder, i18n) || t('general:enterAValue')}
22+
readOnly={disabled}
23+
value={value as Date}
24+
/>
1625
</div>
1726
)
1827
}

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
'use client'
2+
import { getTranslation } from '@payloadcms/translations'
23
import React from 'react'
34

45
import type { NumberFilterProps as Props } from './types.js'
@@ -12,13 +13,13 @@ const baseClass = 'condition-value-number'
1213
export const NumberFilter: React.FC<Props> = (props) => {
1314
const {
1415
disabled,
15-
field: { hasMany },
16+
field: { admin, hasMany },
1617
onChange,
1718
operator,
1819
value,
1920
} = props
2021

21-
const { t } = useTranslation()
22+
const { i18n, t } = useTranslation()
2223

2324
const isMulti = ['in', 'not_in'].includes(operator) || hasMany
2425

@@ -63,6 +64,8 @@ export const NumberFilter: React.FC<Props> = (props) => {
6364
}
6465
}, [value])
6566

67+
const placeholder = getTranslation(admin?.placeholder, i18n) || t('general:enterAValue')
68+
6669
return isMulti ? (
6770
<ReactSelect
6871
disabled={disabled}
@@ -73,15 +76,15 @@ export const NumberFilter: React.FC<Props> = (props) => {
7376
numberOnly
7477
onChange={onSelect}
7578
options={[]}
76-
placeholder={t('general:enterAValue')}
79+
placeholder={placeholder}
7780
value={valueToRender || []}
7881
/>
7982
) : (
8083
<input
8184
className={baseClass}
8285
disabled={disabled}
8386
onChange={(e) => onChange(e.target.value)}
84-
placeholder={t('general:enterAValue')}
87+
placeholder={placeholder}
8588
type="number"
8689
value={value as number}
8790
/>

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export const Condition: React.FC<Props> = (props) => {
7777
const updateValue = useEffectEvent(async (debouncedValue) => {
7878
if (operator) {
7979
await updateCondition({
80+
type: 'value',
8081
andIndex,
8182
field: reducedField,
8283
operator,
@@ -98,6 +99,7 @@ export const Condition: React.FC<Props> = (props) => {
9899
async (field: Option<string>) => {
99100
setInternalValue(undefined)
100101
await updateCondition({
102+
type: 'field',
101103
andIndex,
102104
field: reducedFields.find((option) => option.value === field.value),
103105
operator,
@@ -124,6 +126,7 @@ export const Condition: React.FC<Props> = (props) => {
124126
}
125127

126128
await updateCondition({
129+
type: 'operator',
127130
andIndex,
128131
field: reducedField,
129132
operator: operator.value,

packages/ui/src/elements/WhereBuilder/field-types.tsx

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
'use client'
2+
3+
import type { ClientField } from 'payload'
4+
25
const equalsOperators = [
36
{
47
label: 'equals',
@@ -19,13 +22,14 @@ export const arrayOperators = [
1922
label: 'isNotIn',
2023
value: 'not_in',
2124
},
22-
{
23-
label: 'exists',
24-
value: 'exists',
25-
},
2625
]
2726

28-
const base = [...equalsOperators, ...arrayOperators]
27+
const exists = {
28+
label: 'exists',
29+
value: 'exists',
30+
}
31+
32+
const base = [...equalsOperators, ...arrayOperators, exists]
2933

3034
const numeric = [
3135
...base,
@@ -49,10 +53,6 @@ const numeric = [
4953

5054
const geo = [
5155
...equalsOperators,
52-
{
53-
label: 'exists',
54-
value: 'exists',
55-
},
5656
{
5757
label: 'near',
5858
value: 'near',
@@ -84,23 +84,23 @@ const contains = {
8484
value: 'contains',
8585
}
8686

87-
const fieldTypeConditions: {
87+
export const fieldTypeConditions: {
8888
[key: string]: {
8989
component: string
9090
operators: { label: string; value: string }[]
9191
}
9292
} = {
9393
checkbox: {
9494
component: 'Text',
95-
operators: equalsOperators,
95+
operators: [...equalsOperators, exists],
9696
},
9797
code: {
9898
component: 'Text',
9999
operators: [...base, like, notLike, contains],
100100
},
101101
date: {
102102
component: 'Date',
103-
operators: [...base, ...numeric],
103+
operators: [...numeric, exists],
104104
},
105105
email: {
106106
component: 'Text',
@@ -112,11 +112,11 @@ const fieldTypeConditions: {
112112
},
113113
number: {
114114
component: 'Number',
115-
operators: [...base, ...numeric],
115+
operators: [...numeric, exists],
116116
},
117117
point: {
118118
component: 'Point',
119-
operators: [...geo, within, intersects],
119+
operators: [...geo, exists, within, intersects],
120120
},
121121
radio: {
122122
component: 'Select',
@@ -148,4 +148,39 @@ const fieldTypeConditions: {
148148
},
149149
}
150150

151-
export default fieldTypeConditions
151+
export const getValidFieldOperators = ({
152+
field,
153+
operator,
154+
}: {
155+
field: ClientField
156+
operator?: string
157+
}): {
158+
validOperator: string
159+
validOperators: {
160+
label: string
161+
value: string
162+
}[]
163+
} => {
164+
let validOperators: {
165+
label: string
166+
value: string
167+
}[] = []
168+
169+
if (field.type === 'relationship' && Array.isArray(field.relationTo)) {
170+
if ('hasMany' in field && field.hasMany) {
171+
validOperators = [...equalsOperators, exists]
172+
} else {
173+
validOperators = [...base]
174+
}
175+
} else {
176+
validOperators = [...fieldTypeConditions[field.type].operators]
177+
}
178+
179+
return {
180+
validOperator:
181+
operator && validOperators.find(({ value }) => value === operator)
182+
? operator
183+
: validOperators[0].value,
184+
validOperators,
185+
}
186+
}

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

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { useTranslation } from '../../providers/Translation/index.js'
1313
import { reduceFieldsToOptions } from '../../utilities/reduceFieldsToOptions.js'
1414
import { Button } from '../Button/index.js'
1515
import { Condition } from './Condition/index.js'
16-
import fieldTypes from './field-types.js'
16+
import { fieldTypeConditions, getValidFieldOperators } from './field-types.js'
1717
import './index.scss'
1818

1919
const baseClass = 'where-builder'
@@ -69,7 +69,7 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
6969
async ({ andIndex, field, orIndex, relation }) => {
7070
const newConditions = [...conditions]
7171

72-
const defaultOperator = fieldTypes[field.field.type].operators[0].value
72+
const defaultOperator = fieldTypeConditions[field.field.type].operators[0].value
7373

7474
if (relation === 'and') {
7575
newConditions[orIndex].and.splice(andIndex, 0, {
@@ -95,30 +95,21 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
9595
)
9696

9797
const updateCondition: UpdateCondition = React.useCallback(
98-
async ({ andIndex, field, operator: incomingOperator, orIndex, value: valueArg }) => {
98+
async ({ andIndex, field, operator: incomingOperator, orIndex, value }) => {
9999
const existingCondition = conditions[orIndex].and[andIndex]
100100

101-
const defaults = fieldTypes[field.field.type]
102-
const operator = incomingOperator || defaults.operators[0].value
103-
104101
if (typeof existingCondition === 'object' && field.value) {
105-
const value = valueArg ?? existingCondition?.[operator]
106-
107-
const valueChanged = value !== existingCondition?.[String(field.value)]?.[String(operator)]
108-
109-
const operatorChanged =
110-
operator !== Object.keys(existingCondition?.[String(field.value)] || {})?.[0]
111-
112-
if (valueChanged || operatorChanged) {
113-
const newRowCondition = {
114-
[String(field.value)]: { [operator]: value },
115-
}
116-
117-
const newConditions = [...conditions]
118-
newConditions[orIndex].and[andIndex] = newRowCondition
119-
120-
await handleWhereChange({ or: newConditions })
102+
const { validOperator } = getValidFieldOperators({
103+
field: field.field,
104+
operator: incomingOperator,
105+
})
106+
const newRowCondition = {
107+
[String(field.value)]: { [validOperator]: value },
121108
}
109+
110+
const newConditions = [...conditions]
111+
newConditions[orIndex].and[andIndex] = newRowCondition
112+
await handleWhereChange({ or: newConditions })
122113
}
123114
},
124115
[conditions, handleWhereChange],

packages/ui/src/elements/WhereBuilder/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export type AddCondition = ({
7171
}) => Promise<void> | void
7272

7373
export type UpdateCondition = ({
74+
type,
7475
andIndex,
7576
field,
7677
operator,
@@ -81,6 +82,7 @@ export type UpdateCondition = ({
8182
field: ReducedField
8283
operator: string
8384
orIndex: number
85+
type: 'field' | 'operator' | 'value'
8486
value: Value
8587
}) => Promise<void> | void
8688

packages/ui/src/utilities/reduceFieldsToOptions.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import { fieldAffectsData, fieldIsHiddenOrDisabled, fieldIsID, tabHasName } from
77

88
import type { ReducedField } from '../elements/WhereBuilder/types.js'
99

10-
import fieldTypes, { arrayOperators } from '../elements/WhereBuilder/field-types.js'
10+
import {
11+
fieldTypeConditions,
12+
getValidFieldOperators,
13+
} from '../elements/WhereBuilder/field-types.js'
1114
import { createNestedClientFieldPath } from '../forms/Form/createNestedClientFieldPath.js'
1215
import { combineFieldLabel } from './combineFieldLabel.js'
1316

@@ -199,7 +202,7 @@ export const reduceFieldsToOptions = ({
199202
return reduced
200203
}
201204

202-
if (typeof fieldTypes[field.type] === 'object') {
205+
if (typeof fieldTypeConditions[field.type] === 'object') {
203206
if (
204207
fieldIsID(field) ||
205208
fieldPermissions === true ||
@@ -208,10 +211,11 @@ export const reduceFieldsToOptions = ({
208211
) {
209212
const operatorKeys = new Set()
210213

211-
const fieldOperators =
212-
'hasMany' in field && field.hasMany ? arrayOperators : fieldTypes[field.type].operators
214+
const { validOperators } = getValidFieldOperators({
215+
field,
216+
})
213217

214-
const operators = fieldOperators.reduce((acc, operator) => {
218+
const operators = validOperators.reduce((acc, operator) => {
215219
if (!operatorKeys.has(operator.value)) {
216220
operatorKeys.add(operator.value)
217221
const operatorKey = `operators:${operator.label}` as ClientTranslationKeys
@@ -239,7 +243,7 @@ export const reduceFieldsToOptions = ({
239243
label: formattedLabel,
240244
plainTextLabel: `${labelPrefix ? labelPrefix + ' > ' : ''}${localizedLabel}`,
241245
value: fieldPath,
242-
...fieldTypes[field.type],
246+
...fieldTypeConditions[field.type],
243247
field,
244248
operators,
245249
}

0 commit comments

Comments
 (0)