diff --git a/packages/ui/src/elements/WhereBuilder/Condition/Date/index.tsx b/packages/ui/src/elements/WhereBuilder/Condition/Date/index.tsx index 53796c549..bd9a09fe8 100644 --- a/packages/ui/src/elements/WhereBuilder/Condition/Date/index.tsx +++ b/packages/ui/src/elements/WhereBuilder/Condition/Date/index.tsx @@ -1,7 +1,7 @@ 'use client' import React from 'react' -import type { Props } from './types.js' +import type { DateFilterProps as Props } from './types.js' import { DatePickerField } from '../../../DatePicker/index.js' diff --git a/packages/ui/src/elements/WhereBuilder/Condition/Date/types.ts b/packages/ui/src/elements/WhereBuilder/Condition/Date/types.ts index 84406b706..8175e9534 100644 --- a/packages/ui/src/elements/WhereBuilder/Condition/Date/types.ts +++ b/packages/ui/src/elements/WhereBuilder/Condition/Date/types.ts @@ -2,7 +2,7 @@ import type { DateFieldClient } from 'payload' import type { DefaultFilterProps } from '../types.js' -export type Props = { +export type DateFilterProps = { readonly field: DateFieldClient readonly value: Date | string } & DefaultFilterProps diff --git a/packages/ui/src/elements/WhereBuilder/Condition/DefaultFilter/index.tsx b/packages/ui/src/elements/WhereBuilder/Condition/DefaultFilter/index.tsx index bd2b3f216..e939e475a 100644 --- a/packages/ui/src/elements/WhereBuilder/Condition/DefaultFilter/index.tsx +++ b/packages/ui/src/elements/WhereBuilder/Condition/DefaultFilter/index.tsx @@ -8,7 +8,7 @@ import type { import React from 'react' -import type { ReducedField } from '../../types.js' +import type { ReducedField, Value } from '../../types.js' import { DateFilter } from '../Date/index.js' import { NumberFilter } from '../Number/index.js' @@ -24,7 +24,7 @@ type Props = { onChange: React.Dispatch> operator: Operator options: Option[] - value: string + value: Value } export const DefaultFilter: React.FC = ({ @@ -46,7 +46,7 @@ export const DefaultFilter: React.FC = ({ onChange={onChange} operator={operator} options={options} - value={value} + value={value as string} /> ) } @@ -59,7 +59,7 @@ export const DefaultFilter: React.FC = ({ field={internalField.field} onChange={onChange} operator={operator} - value={value} + value={value as Date | string} /> ) } @@ -71,7 +71,7 @@ export const DefaultFilter: React.FC = ({ field={internalField.field} onChange={onChange} operator={operator} - value={value} + value={value as number | number[]} /> ) } @@ -96,7 +96,7 @@ export const DefaultFilter: React.FC = ({ field={internalField?.field as TextFieldClient} onChange={onChange} operator={operator} - value={value} + value={value as string} /> ) } diff --git a/packages/ui/src/elements/WhereBuilder/Condition/Number/index.tsx b/packages/ui/src/elements/WhereBuilder/Condition/Number/index.tsx index 6dba4f3bc..a6ba00f4a 100644 --- a/packages/ui/src/elements/WhereBuilder/Condition/Number/index.tsx +++ b/packages/ui/src/elements/WhereBuilder/Condition/Number/index.tsx @@ -1,24 +1,89 @@ 'use client' import React from 'react' -import type { Props } from './types.js' +import type { NumberFilterProps 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-number' -export const NumberFilter: React.FC = ({ disabled, onChange, value }) => { +export const NumberFilter: React.FC = (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: number } }[] + >([]) + + const onSelect = React.useCallback( + (selectedOption) => { + let newValue + if (!selectedOption) { + newValue = [] + } else if (isMulti) { + if (Array.isArray(selectedOption)) { + newValue = selectedOption.map((option) => Number(option.value?.value || option.value)) + } else { + newValue = [Number(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 ? ( + + ) : ( onChange(e.target.value)} placeholder={t('general:enterAValue')} type="number" - value={value} + value={value as number} /> ) } diff --git a/packages/ui/src/elements/WhereBuilder/Condition/Number/types.ts b/packages/ui/src/elements/WhereBuilder/Condition/Number/types.ts index 96f9053bc..9c57459f1 100644 --- a/packages/ui/src/elements/WhereBuilder/Condition/Number/types.ts +++ b/packages/ui/src/elements/WhereBuilder/Condition/Number/types.ts @@ -2,8 +2,8 @@ import type { NumberFieldClient } from 'payload' import type { DefaultFilterProps } from '../types.js' -export type Props = { +export type NumberFilterProps = { readonly field: NumberFieldClient readonly onChange: (e: string) => void - readonly value: string + readonly value: number | number[] } & DefaultFilterProps diff --git a/packages/ui/src/elements/WhereBuilder/Condition/Relationship/index.tsx b/packages/ui/src/elements/WhereBuilder/Condition/Relationship/index.tsx index 2cca0b06e..cd5e1ea22 100644 --- a/packages/ui/src/elements/WhereBuilder/Condition/Relationship/index.tsx +++ b/packages/ui/src/elements/WhereBuilder/Condition/Relationship/index.tsx @@ -5,7 +5,7 @@ import * as qs from 'qs-esm' import React, { useCallback, useEffect, useReducer, useState } from 'react' import type { Option } from '../../../ReactSelect/types.js' -import type { Props, ValueWithRelation } from './types.js' +import type { RelationshipFilterProps as Props, ValueWithRelation } from './types.js' import { useDebounce } from '../../../../hooks/useDebounce.js' import { useEffectEvent } from '../../../../hooks/useEffectEvent.js' diff --git a/packages/ui/src/elements/WhereBuilder/Condition/Relationship/types.ts b/packages/ui/src/elements/WhereBuilder/Condition/Relationship/types.ts index ac164577b..a49a115a4 100644 --- a/packages/ui/src/elements/WhereBuilder/Condition/Relationship/types.ts +++ b/packages/ui/src/elements/WhereBuilder/Condition/Relationship/types.ts @@ -8,7 +8,7 @@ import type { import type { DefaultFilterProps } from '../types.js' -export type Props = { +export type RelationshipFilterProps = { readonly field: RelationshipFieldClient readonly filterOptions: ResolvedFilterOptions } & DefaultFilterProps diff --git a/packages/ui/src/elements/WhereBuilder/Condition/Select/index.tsx b/packages/ui/src/elements/WhereBuilder/Condition/Select/index.tsx index 3757714e9..c9b1ebf0b 100644 --- a/packages/ui/src/elements/WhereBuilder/Condition/Select/index.tsx +++ b/packages/ui/src/elements/WhereBuilder/Condition/Select/index.tsx @@ -3,7 +3,7 @@ import { getTranslation } from '@payloadcms/translations' import React from 'react' -import type { Props } from './types.js' +import type { SelectFilterProps as Props } from './types.js' import { useTranslation } from '../../../../providers/Translation/index.js' import { ReactSelect } from '../../../ReactSelect/index.js' diff --git a/packages/ui/src/elements/WhereBuilder/Condition/Select/types.ts b/packages/ui/src/elements/WhereBuilder/Condition/Select/types.ts index cf82c02f1..27c34b3f5 100644 --- a/packages/ui/src/elements/WhereBuilder/Condition/Select/types.ts +++ b/packages/ui/src/elements/WhereBuilder/Condition/Select/types.ts @@ -2,7 +2,7 @@ import type { Option, SelectFieldClient } from 'payload' import type { DefaultFilterProps } from '../types.js' -export type Props = { +export type SelectFilterProps = { readonly field: SelectFieldClient readonly isClearable?: boolean readonly onChange: (val: string) => void diff --git a/packages/ui/src/elements/WhereBuilder/Condition/Text/index.tsx b/packages/ui/src/elements/WhereBuilder/Condition/Text/index.tsx index 95d958e7f..f4cd8b217 100644 --- a/packages/ui/src/elements/WhereBuilder/Condition/Text/index.tsx +++ b/packages/ui/src/elements/WhereBuilder/Condition/Text/index.tsx @@ -1,7 +1,7 @@ 'use client' import React from 'react' -import type { Props } from './types.js' +import type { TextFilterProps as Props } from './types.js' import { useTranslation } from '../../../../providers/Translation/index.js' import './index.scss' diff --git a/packages/ui/src/elements/WhereBuilder/Condition/Text/types.ts b/packages/ui/src/elements/WhereBuilder/Condition/Text/types.ts index b6d39cc36..9e25e2e98 100644 --- a/packages/ui/src/elements/WhereBuilder/Condition/Text/types.ts +++ b/packages/ui/src/elements/WhereBuilder/Condition/Text/types.ts @@ -2,7 +2,7 @@ import type { TextFieldClient } from 'payload' import type { DefaultFilterProps } from '../types.js' -export type Props = { +export type TextFilterProps = { readonly field: TextFieldClient readonly onChange: (val: string) => void readonly value: string diff --git a/packages/ui/src/elements/WhereBuilder/Condition/index.tsx b/packages/ui/src/elements/WhereBuilder/Condition/index.tsx index 7cec20c6d..c57eab59f 100644 --- a/packages/ui/src/elements/WhereBuilder/Condition/index.tsx +++ b/packages/ui/src/elements/WhereBuilder/Condition/index.tsx @@ -1,7 +1,13 @@ 'use client' import React, { useCallback, useEffect, useState } from 'react' -import type { AddCondition, ReducedField, RemoveCondition, UpdateCondition } from '../types.js' +import type { + AddCondition, + ReducedField, + RemoveCondition, + UpdateCondition, + Value, +} from '../types.js' export type Props = { readonly addCondition: AddCondition @@ -14,7 +20,7 @@ export type Props = { readonly removeCondition: RemoveCondition readonly RenderedFilter: React.ReactNode readonly updateCondition: UpdateCondition - readonly value: string + readonly value: Value } import type { Operator, Option as PayloadOption, ResolvedFilterOptions } from 'payload' @@ -51,7 +57,7 @@ export const Condition: React.FC = (props) => { const reducedField = reducedFields.find((field) => field.value === fieldName) - const [internalValue, setInternalValue] = useState(value) + const [internalValue, setInternalValue] = useState(value) const debouncedValue = useDebounce(internalValue, 300) diff --git a/packages/ui/src/elements/WhereBuilder/index.tsx b/packages/ui/src/elements/WhereBuilder/index.tsx index 7bf6ed356..41ace0ace 100644 --- a/packages/ui/src/elements/WhereBuilder/index.tsx +++ b/packages/ui/src/elements/WhereBuilder/index.tsx @@ -60,7 +60,7 @@ export const WhereBuilder: React.FC = (props) => { if (relation === 'and') { newConditions[orIndex].and.splice(andIndex, 0, { - [field.value]: { + [String(field.value)]: { [defaultOperator]: undefined, }, }) @@ -68,7 +68,7 @@ export const WhereBuilder: React.FC = (props) => { newConditions.push({ and: [ { - [field.value]: { + [String(field.value)]: { [defaultOperator]: undefined, }, }, @@ -91,14 +91,14 @@ export const WhereBuilder: React.FC = (props) => { if (typeof existingCondition === 'object' && field.value) { const value = valueArg ?? existingCondition?.[operator] - const valueChanged = value !== existingCondition?.[field.value]?.[operator] + const valueChanged = value !== existingCondition?.[String(field.value)]?.[String(operator)] const operatorChanged = - operator !== Object.keys(existingCondition?.[field.value] || {})?.[0] + operator !== Object.keys(existingCondition?.[String(field.value)] || {})?.[0] if (valueChanged || operatorChanged) { const newRowCondition = { - [field.value]: { [operator]: value }, + [String(field.value)]: { [operator]: value }, } const newConditions = [...conditions] diff --git a/packages/ui/src/elements/WhereBuilder/types.ts b/packages/ui/src/elements/WhereBuilder/types.ts index e7bd61fb6..0914c5ace 100644 --- a/packages/ui/src/elements/WhereBuilder/types.ts +++ b/packages/ui/src/elements/WhereBuilder/types.ts @@ -14,6 +14,8 @@ export type WhereBuilderProps = { readonly resolvedFilterOptions?: Map } +export type Value = Date | number | number[] | string | string[] + export type ReducedField = { field: ClientField label: React.ReactNode @@ -21,7 +23,7 @@ export type ReducedField = { label: string value: Operator }[] - value: string + value: Value } export type Relation = 'and' | 'or' @@ -78,7 +80,7 @@ export type UpdateCondition = ({ field: ReducedField operator: string orIndex: number - value: string + value: Value }) => Promise | void export type RemoveCondition = ({ diff --git a/test/fields/collections/Number/e2e.spec.ts b/test/fields/collections/Number/e2e.spec.ts index 0ff48f475..7044a61c2 100644 --- a/test/fields/collections/Number/e2e.spec.ts +++ b/test/fields/collections/Number/e2e.spec.ts @@ -88,6 +88,66 @@ describe('Number', () => { await expect(page.locator('table >> tbody >> tr')).toHaveCount(2) }) + test('should filter Number field hasMany: false in the collection view - in', async () => { + await page.goto(url.list) + await expect(page.locator('table >> tbody >> tr')).toHaveCount(3) + + await addListFilter({ + page, + fieldLabel: 'Number', + operatorLabel: 'is in', + value: '2', + }) + + await wait(300) + await expect(page.locator('table >> tbody >> tr')).toHaveCount(1) + }) + + test('should filter Number field hasMany: false in the collection view - is not in', async () => { + await page.goto(url.list) + await expect(page.locator('table >> tbody >> tr')).toHaveCount(3) + + await addListFilter({ + page, + fieldLabel: 'Number', + operatorLabel: 'is not in', + value: '2', + }) + + await wait(300) + await expect(page.locator('table >> tbody >> tr')).toHaveCount(2) + }) + + test('should filter Number field hasMany: true in the collection view - in', async () => { + await page.goto(url.list) + await expect(page.locator('table >> tbody >> tr')).toHaveCount(3) + + await addListFilter({ + page, + fieldLabel: 'Has Many', + operatorLabel: 'is in', + value: '5', + }) + + await wait(300) + await expect(page.locator('table >> tbody >> tr')).toHaveCount(1) + }) + + test('should filter Number field hasMany: true in the collection view - is not in', async () => { + await page.goto(url.list) + await expect(page.locator('table >> tbody >> tr')).toHaveCount(3) + + await addListFilter({ + page, + fieldLabel: 'Has Many', + operatorLabel: 'is not in', + value: '6', + }) + + await wait(300) + await expect(page.locator('table >> tbody >> tr')).toHaveCount(3) + }) + test('should create', async () => { const input = 5 await page.goto(url.create)