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.
This commit is contained in:
Patrik
2025-04-10 08:54:50 -04:00
committed by GitHub
parent ae9e5e19ad
commit 18ff9cbdb1
6 changed files with 132 additions and 6 deletions

View File

@@ -96,7 +96,7 @@ export const DefaultFilter: React.FC<Props> = ({
field={internalField?.field as TextFieldClient}
onChange={onChange}
operator={operator}
value={value as string}
value={value as string | string[]}
/>
)
}

View File

@@ -4,14 +4,77 @@ import React from 'react'
import type { TextFilterProps as Props } from './types.js'
import { useTranslation } from '../../../../providers/Translation/index.js'
import { ReactSelect } from '../../../ReactSelect/index.js'
import './index.scss'
const baseClass = 'condition-value-text'
export const Text: React.FC<Props> = ({ disabled, onChange, value }) => {
export const Text: React.FC<Props> = (props) => {
const {
disabled,
field: { hasMany },
onChange,
operator,
value,
} = props
const { t } = useTranslation()
return (
const isMulti = ['in', 'not_in'].includes(operator) || hasMany
const [valueToRender, setValueToRender] = React.useState<
{ id: string; label: string; value: { value: string } }[]
>([])
const onSelect = React.useCallback(
(selectedOption) => {
let newValue
if (!selectedOption) {
newValue = []
} else if (isMulti) {
if (Array.isArray(selectedOption)) {
newValue = selectedOption.map((option) => option.value?.value || option.value)
} else {
newValue = [selectedOption.value?.value || selectedOption.value]
}
}
onChange(newValue)
},
[isMulti, onChange],
)
React.useEffect(() => {
if (Array.isArray(value)) {
setValueToRender(
value.map((val, index) => {
return {
id: `${val}${index}`, // append index to avoid duplicate keys but allow duplicate numbers
label: `${val}`,
value: {
toString: () => `${val}${index}`,
value: (val as any)?.value || val,
},
}
}),
)
} else {
setValueToRender([])
}
}, [value])
return isMulti ? (
<ReactSelect
disabled={disabled}
isClearable
isCreatable
isMulti={isMulti}
isSortable
onChange={onSelect}
options={[]}
placeholder={t('general:enterAValue')}
value={valueToRender || []}
/>
) : (
<input
className={baseClass}
disabled={disabled}

View File

@@ -5,5 +5,5 @@ import type { DefaultFilterProps } from '../types.js'
export type TextFilterProps = {
readonly field: TextFieldClient
readonly onChange: (val: string) => void
readonly value: string
readonly value: string | string[]
} & DefaultFilterProps

View File

@@ -2,7 +2,6 @@ import type { Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import { addListFilter } from 'helpers/e2e/addListFilter.js'
import { openListFilters } from 'helpers/e2e/openListFilters.js'
import path from 'path'
import { wait } from 'payload/shared'
import { fileURLToPath } from 'url'

View File

@@ -2,10 +2,12 @@ import type { Page } from '@playwright/test'
import type { GeneratedTypes } from 'helpers/sdk/types.js'
import { expect, test } from '@playwright/test'
import { addListFilter } from 'helpers/e2e/addListFilter.js'
import { openListColumns } from 'helpers/e2e/openListColumns.js'
import { upsertPreferences } from 'helpers/e2e/preferences.js'
import { toggleColumn } from 'helpers/e2e/toggleColumn.js'
import path from 'path'
import { wait } from 'payload/shared'
import { fileURLToPath } from 'url'
import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'
@@ -281,4 +283,64 @@ describe('Text', () => {
// Verify it does not become editable
await expect(field.locator('.multi-value-label__text')).not.toHaveClass(/.*--editable/)
})
test('should filter Text field hasMany: false in the collection list view - in', async () => {
await page.goto(url.list)
await expect(page.locator('table >> tbody >> tr')).toHaveCount(2)
await addListFilter({
page,
fieldLabel: 'Text',
operatorLabel: 'is in',
value: 'Another text document',
})
await wait(300)
await expect(page.locator('table >> tbody >> tr')).toHaveCount(1)
})
test('should filter Text field hasMany: false in the collection list view - is not in', async () => {
await page.goto(url.list)
await expect(page.locator('table >> tbody >> tr')).toHaveCount(2)
await addListFilter({
page,
fieldLabel: 'Text',
operatorLabel: 'is not in',
value: 'Another text document',
})
await wait(300)
await expect(page.locator('table >> tbody >> tr')).toHaveCount(1)
})
test('should filter Text field hasMany: true in the collection list view - in', async () => {
await page.goto(url.list)
await expect(page.locator('table >> tbody >> tr')).toHaveCount(2)
await addListFilter({
page,
fieldLabel: 'Has Many',
operatorLabel: 'is in',
value: 'one',
})
await wait(300)
await expect(page.locator('table >> tbody >> tr')).toHaveCount(1)
})
test('should filter Text field hasMany: true in the collection list view - is not in', async () => {
await page.goto(url.list)
await expect(page.locator('table >> tbody >> tr')).toHaveCount(2)
await addListFilter({
page,
fieldLabel: 'Has Many',
operatorLabel: 'is not in',
value: 'four',
})
await wait(300)
await expect(page.locator('table >> tbody >> tr')).toHaveCount(1)
})
})

View File

@@ -1,4 +1,4 @@
import type { RequiredDataFromCollection } from 'payload/types'
import type { RequiredDataFromCollection } from 'payload'
import type { TextField } from '../../payload-types.js'
@@ -8,8 +8,10 @@ export const textFieldsSlug = 'text-fields'
export const textDoc: RequiredDataFromCollection<TextField> = {
text: 'Seeded text document',
localizedText: 'Localized text',
hasMany: ['one', 'two'],
}
export const anotherTextDoc: RequiredDataFromCollection<TextField> = {
text: 'Another text document',
hasMany: ['three', 'four'],
}