Skip to content

Commit 18ff9cb

Browse files
authored
fix(ui): adds multi select inputs for text fields in where builder (#12054)
### What? The `in` & `not_in` operators were not properly working for `text` fields as this operator requires an array of values for it's input. ### How? Conditionally renders a multi select input for `text` fields when filtering by `in` & `not_in` operators.
1 parent ae9e5e1 commit 18ff9cb

File tree

6 files changed

+132
-6
lines changed

6 files changed

+132
-6
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export const DefaultFilter: React.FC<Props> = ({
9696
field={internalField?.field as TextFieldClient}
9797
onChange={onChange}
9898
operator={operator}
99-
value={value as string}
99+
value={value as string | string[]}
100100
/>
101101
)
102102
}

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

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,77 @@ import React from 'react'
44
import type { TextFilterProps as Props } from './types.js'
55

66
import { useTranslation } from '../../../../providers/Translation/index.js'
7+
import { ReactSelect } from '../../../ReactSelect/index.js'
78
import './index.scss'
89

910
const baseClass = 'condition-value-text'
1011

11-
export const Text: React.FC<Props> = ({ disabled, onChange, value }) => {
12+
export const Text: React.FC<Props> = (props) => {
13+
const {
14+
disabled,
15+
field: { hasMany },
16+
onChange,
17+
operator,
18+
value,
19+
} = props
1220
const { t } = useTranslation()
1321

14-
return (
22+
const isMulti = ['in', 'not_in'].includes(operator) || hasMany
23+
24+
const [valueToRender, setValueToRender] = React.useState<
25+
{ id: string; label: string; value: { value: string } }[]
26+
>([])
27+
28+
const onSelect = React.useCallback(
29+
(selectedOption) => {
30+
let newValue
31+
if (!selectedOption) {
32+
newValue = []
33+
} else if (isMulti) {
34+
if (Array.isArray(selectedOption)) {
35+
newValue = selectedOption.map((option) => option.value?.value || option.value)
36+
} else {
37+
newValue = [selectedOption.value?.value || selectedOption.value]
38+
}
39+
}
40+
41+
onChange(newValue)
42+
},
43+
[isMulti, onChange],
44+
)
45+
46+
React.useEffect(() => {
47+
if (Array.isArray(value)) {
48+
setValueToRender(
49+
value.map((val, index) => {
50+
return {
51+
id: `${val}${index}`, // append index to avoid duplicate keys but allow duplicate numbers
52+
label: `${val}`,
53+
value: {
54+
toString: () => `${val}${index}`,
55+
value: (val as any)?.value || val,
56+
},
57+
}
58+
}),
59+
)
60+
} else {
61+
setValueToRender([])
62+
}
63+
}, [value])
64+
65+
return isMulti ? (
66+
<ReactSelect
67+
disabled={disabled}
68+
isClearable
69+
isCreatable
70+
isMulti={isMulti}
71+
isSortable
72+
onChange={onSelect}
73+
options={[]}
74+
placeholder={t('general:enterAValue')}
75+
value={valueToRender || []}
76+
/>
77+
) : (
1578
<input
1679
className={baseClass}
1780
disabled={disabled}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ import type { DefaultFilterProps } from '../types.js'
55
export type TextFilterProps = {
66
readonly field: TextFieldClient
77
readonly onChange: (val: string) => void
8-
readonly value: string
8+
readonly value: string | string[]
99
} & DefaultFilterProps

test/fields/collections/Number/e2e.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { Page } from '@playwright/test'
22

33
import { expect, test } from '@playwright/test'
44
import { addListFilter } from 'helpers/e2e/addListFilter.js'
5-
import { openListFilters } from 'helpers/e2e/openListFilters.js'
65
import path from 'path'
76
import { wait } from 'payload/shared'
87
import { fileURLToPath } from 'url'

test/fields/collections/Text/e2e.spec.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import type { Page } from '@playwright/test'
22
import type { GeneratedTypes } from 'helpers/sdk/types.js'
33

44
import { expect, test } from '@playwright/test'
5+
import { addListFilter } from 'helpers/e2e/addListFilter.js'
56
import { openListColumns } from 'helpers/e2e/openListColumns.js'
67
import { upsertPreferences } from 'helpers/e2e/preferences.js'
78
import { toggleColumn } from 'helpers/e2e/toggleColumn.js'
89
import path from 'path'
10+
import { wait } from 'payload/shared'
911
import { fileURLToPath } from 'url'
1012

1113
import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'
@@ -281,4 +283,64 @@ describe('Text', () => {
281283
// Verify it does not become editable
282284
await expect(field.locator('.multi-value-label__text')).not.toHaveClass(/.*--editable/)
283285
})
286+
287+
test('should filter Text field hasMany: false in the collection list view - in', async () => {
288+
await page.goto(url.list)
289+
await expect(page.locator('table >> tbody >> tr')).toHaveCount(2)
290+
291+
await addListFilter({
292+
page,
293+
fieldLabel: 'Text',
294+
operatorLabel: 'is in',
295+
value: 'Another text document',
296+
})
297+
298+
await wait(300)
299+
await expect(page.locator('table >> tbody >> tr')).toHaveCount(1)
300+
})
301+
302+
test('should filter Text field hasMany: false in the collection list view - is not in', async () => {
303+
await page.goto(url.list)
304+
await expect(page.locator('table >> tbody >> tr')).toHaveCount(2)
305+
306+
await addListFilter({
307+
page,
308+
fieldLabel: 'Text',
309+
operatorLabel: 'is not in',
310+
value: 'Another text document',
311+
})
312+
313+
await wait(300)
314+
await expect(page.locator('table >> tbody >> tr')).toHaveCount(1)
315+
})
316+
317+
test('should filter Text field hasMany: true in the collection list view - in', async () => {
318+
await page.goto(url.list)
319+
await expect(page.locator('table >> tbody >> tr')).toHaveCount(2)
320+
321+
await addListFilter({
322+
page,
323+
fieldLabel: 'Has Many',
324+
operatorLabel: 'is in',
325+
value: 'one',
326+
})
327+
328+
await wait(300)
329+
await expect(page.locator('table >> tbody >> tr')).toHaveCount(1)
330+
})
331+
332+
test('should filter Text field hasMany: true in the collection list view - is not in', async () => {
333+
await page.goto(url.list)
334+
await expect(page.locator('table >> tbody >> tr')).toHaveCount(2)
335+
336+
await addListFilter({
337+
page,
338+
fieldLabel: 'Has Many',
339+
operatorLabel: 'is not in',
340+
value: 'four',
341+
})
342+
343+
await wait(300)
344+
await expect(page.locator('table >> tbody >> tr')).toHaveCount(1)
345+
})
284346
})

test/fields/collections/Text/shared.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { RequiredDataFromCollection } from 'payload/types'
1+
import type { RequiredDataFromCollection } from 'payload'
22

33
import type { TextField } from '../../payload-types.js'
44

@@ -8,8 +8,10 @@ export const textFieldsSlug = 'text-fields'
88
export const textDoc: RequiredDataFromCollection<TextField> = {
99
text: 'Seeded text document',
1010
localizedText: 'Localized text',
11+
hasMany: ['one', 'two'],
1112
}
1213

1314
export const anotherTextDoc: RequiredDataFromCollection<TextField> = {
1415
text: 'Another text document',
16+
hasMany: ['three', 'four'],
1517
}

0 commit comments

Comments
 (0)