fix(tests): number field e2e (#5452)
This commit is contained in:
@@ -18,6 +18,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__field {
|
||||
.field-label {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
|
||||
@@ -1,15 +1,39 @@
|
||||
import type { Where } from 'payload/types'
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import type { Action, FieldCondition } from '../types.js'
|
||||
import type { FieldCondition } from '../types.js'
|
||||
|
||||
export type Props = {
|
||||
addCondition: ({
|
||||
andIndex,
|
||||
fieldName,
|
||||
orIndex,
|
||||
relation,
|
||||
}: {
|
||||
andIndex: number
|
||||
fieldName: string
|
||||
orIndex: number
|
||||
relation: 'and' | 'or'
|
||||
}) => void
|
||||
andIndex: number
|
||||
dispatch: (action: Action) => void
|
||||
fieldName: string
|
||||
fields: FieldCondition[]
|
||||
initialValue: string
|
||||
operator: string
|
||||
orIndex: number
|
||||
value: Where
|
||||
removeCondition: ({ andIndex, orIndex }: { andIndex: number; orIndex: number }) => void
|
||||
updateCondition: ({
|
||||
andIndex,
|
||||
fieldName,
|
||||
operator,
|
||||
orIndex,
|
||||
value,
|
||||
}: {
|
||||
andIndex: number
|
||||
fieldName: string
|
||||
operator: string
|
||||
orIndex: number
|
||||
value: string
|
||||
}) => void
|
||||
}
|
||||
|
||||
import { RenderCustomComponent } from '../../../elements/RenderCustomComponent/index.js'
|
||||
@@ -35,42 +59,46 @@ const valueFields: Record<ComponentType, React.FC> = {
|
||||
const baseClass = 'condition'
|
||||
|
||||
export const Condition: React.FC<Props> = (props) => {
|
||||
const { andIndex, dispatch, fields, orIndex, value } = props
|
||||
const fieldName = Object.keys(value)[0]
|
||||
const {
|
||||
addCondition,
|
||||
andIndex,
|
||||
fieldName,
|
||||
fields,
|
||||
initialValue,
|
||||
operator,
|
||||
orIndex,
|
||||
removeCondition,
|
||||
updateCondition,
|
||||
} = props
|
||||
const [activeField, setActiveField] = useState<FieldCondition>(() =>
|
||||
fields.find((field) => fieldName === field.value),
|
||||
)
|
||||
|
||||
const operatorAndValue = value?.[fieldName] ? Object.entries(value[fieldName])[0] : undefined
|
||||
const queryValue = operatorAndValue?.[1]
|
||||
const operatorValue = operatorAndValue?.[0]
|
||||
const [internalQueryValue, setInternalQueryValue] = useState<string>(initialValue)
|
||||
const [internalOperatorOption, setInternalOperatorOption] = useState(operator)
|
||||
|
||||
const [internalValue, setInternalValue] = useState(queryValue)
|
||||
const [internalOperatorField, setInternalOperatorField] = useState(operatorValue)
|
||||
|
||||
const debouncedValue = useDebounce(internalValue, 300)
|
||||
const debouncedValue = useDebounce(internalQueryValue, 300)
|
||||
|
||||
useEffect(() => {
|
||||
const newActiveField = fields.find(({ value: name }) => name === fieldName)
|
||||
|
||||
if (newActiveField && newActiveField !== activeField) {
|
||||
setActiveField(newActiveField)
|
||||
setInternalOperatorField(null)
|
||||
setInternalValue('')
|
||||
}
|
||||
}, [fieldName, fields, activeField])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({
|
||||
type: 'update',
|
||||
updateCondition({
|
||||
andIndex,
|
||||
fieldName,
|
||||
operator: internalOperatorOption,
|
||||
orIndex,
|
||||
value: debouncedValue || '',
|
||||
value: debouncedValue,
|
||||
})
|
||||
}, [debouncedValue, dispatch, orIndex, andIndex])
|
||||
}, [
|
||||
debouncedValue,
|
||||
andIndex,
|
||||
fieldName,
|
||||
internalOperatorOption,
|
||||
orIndex,
|
||||
updateCondition,
|
||||
operator,
|
||||
])
|
||||
|
||||
const booleanSelect =
|
||||
['exists'].includes(operatorValue) || activeField?.props?.type === 'checkbox'
|
||||
['exists'].includes(internalOperatorOption) || activeField?.props?.type === 'checkbox'
|
||||
const ValueComponent = booleanSelect
|
||||
? Select
|
||||
: valueFields[activeField?.component] || valueFields.Text
|
||||
@@ -90,15 +118,17 @@ export const Condition: React.FC<Props> = (props) => {
|
||||
<ReactSelect
|
||||
isClearable={false}
|
||||
onChange={(field) => {
|
||||
dispatch({
|
||||
type: 'update',
|
||||
setActiveField(fields.find((f) => f.value === field.value))
|
||||
updateCondition({
|
||||
andIndex,
|
||||
field: field?.value,
|
||||
fieldName: field.value,
|
||||
operator,
|
||||
orIndex,
|
||||
value: internalQueryValue,
|
||||
})
|
||||
}}
|
||||
options={fields}
|
||||
value={fields.find((field) => fieldName === field.value)}
|
||||
value={fields.find((field) => fieldName === field.value) || fields[0]}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${baseClass}__operator`}>
|
||||
@@ -106,18 +136,19 @@ export const Condition: React.FC<Props> = (props) => {
|
||||
disabled={!fieldName}
|
||||
isClearable={false}
|
||||
onChange={(operator) => {
|
||||
dispatch({
|
||||
type: 'update',
|
||||
setInternalOperatorOption(operator.value)
|
||||
updateCondition({
|
||||
andIndex,
|
||||
fieldName,
|
||||
operator: operator.value,
|
||||
orIndex,
|
||||
value: internalQueryValue,
|
||||
})
|
||||
setInternalOperatorField(operator.value)
|
||||
}}
|
||||
options={activeField?.operators}
|
||||
value={
|
||||
activeField?.operators.find(
|
||||
(operator) => internalOperatorField === operator.value,
|
||||
(operator) => internalOperatorOption === operator.value,
|
||||
) || null
|
||||
}
|
||||
/>
|
||||
@@ -128,11 +159,11 @@ export const Condition: React.FC<Props> = (props) => {
|
||||
DefaultComponent={ValueComponent}
|
||||
componentProps={{
|
||||
...activeField?.props,
|
||||
disabled: !operatorValue,
|
||||
onChange: setInternalValue,
|
||||
operator: operatorValue,
|
||||
disabled: !internalOperatorOption,
|
||||
onChange: setInternalQueryValue,
|
||||
operator: internalOperatorOption,
|
||||
options: valueOptions,
|
||||
value: internalValue,
|
||||
value: internalQueryValue ?? '',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -144,8 +175,7 @@ export const Condition: React.FC<Props> = (props) => {
|
||||
icon="x"
|
||||
iconStyle="with-border"
|
||||
onClick={() =>
|
||||
dispatch({
|
||||
type: 'remove',
|
||||
removeCondition({
|
||||
andIndex,
|
||||
orIndex,
|
||||
})
|
||||
@@ -158,10 +188,9 @@ export const Condition: React.FC<Props> = (props) => {
|
||||
icon="plus"
|
||||
iconStyle="with-border"
|
||||
onClick={() =>
|
||||
dispatch({
|
||||
type: 'add',
|
||||
andIndex: andIndex + 1,
|
||||
field: fields[0].value,
|
||||
addCondition({
|
||||
andIndex,
|
||||
fieldName,
|
||||
orIndex,
|
||||
relation: 'and',
|
||||
})
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { Where } from 'payload/types'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { flattenTopLevelFields } from 'payload/utilities'
|
||||
import React, { useReducer, useState } from 'react'
|
||||
import { FieldLabel } from '@payloadcms/ui/forms/FieldLabel'
|
||||
import { useComponentMap } from '@payloadcms/ui/providers/ComponentMap'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import type {
|
||||
CollectionComponentMap,
|
||||
FieldMap,
|
||||
} from '../../providers/ComponentMap/buildComponentMap/types.js'
|
||||
import type { WhereBuilderProps } from './types.js'
|
||||
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
import { useListQuery } from '../../providers/ListQuery/index.js'
|
||||
import { useSearchParams } from '../../providers/SearchParams/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
@@ -14,14 +16,13 @@ import { Button } from '../Button/index.js'
|
||||
import { Condition } from './Condition/index.js'
|
||||
import fieldTypes from './field-types.js'
|
||||
import './index.scss'
|
||||
import reducer from './reducer.js'
|
||||
import { transformWhereQuery } from './transformWhereQuery.js'
|
||||
import validateWhereQuery from './validateWhereQuery.js'
|
||||
|
||||
const baseClass = 'where-builder'
|
||||
|
||||
const reduceFields = (fields, i18n) =>
|
||||
flattenTopLevelFields(fields).reduce((reduced, field) => {
|
||||
const reduceFields = (fieldMap: FieldMap, i18n) =>
|
||||
fieldMap.reduce((reduced, field) => {
|
||||
if (typeof fieldTypes[field.type] === 'object') {
|
||||
const operatorKeys = new Set()
|
||||
const operators = fieldTypes[field.type].operators.reduce((acc, operator) => {
|
||||
@@ -39,7 +40,12 @@ const reduceFields = (fields, i18n) =>
|
||||
}, [])
|
||||
|
||||
const formattedField = {
|
||||
label: getTranslation(field.label || field.name, i18n),
|
||||
label: (
|
||||
<FieldLabel
|
||||
CustomLabel={field.fieldComponentProps.CustomLabel}
|
||||
{...field.fieldComponentProps.labelProps}
|
||||
/>
|
||||
),
|
||||
value: field.name,
|
||||
...fieldTypes[field.type],
|
||||
operators,
|
||||
@@ -63,39 +69,131 @@ export { WhereBuilderProps }
|
||||
export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
|
||||
const { collectionPluralLabel, collectionSlug } = props
|
||||
const { i18n, t } = useTranslation()
|
||||
const { getComponentMap } = useComponentMap()
|
||||
|
||||
const config = useConfig()
|
||||
const collection = config.collections.find((c) => c.slug === collectionSlug)
|
||||
const [reducedFields] = useState(() => reduceFields(collection.fields, i18n))
|
||||
const { fieldMap } = getComponentMap({ collectionSlug }) as CollectionComponentMap
|
||||
const [reducedFields] = useState(() => reduceFields(fieldMap, i18n))
|
||||
|
||||
const { searchParams } = useSearchParams()
|
||||
const { handleWhereChange } = useListQuery()
|
||||
const [shouldUpdateQuery, setShouldUpdateQuery] = React.useState(false)
|
||||
|
||||
// This handles initializing the where conditions from the search query (URL). That way, if you pass in
|
||||
// query params to the URL, the where conditions will be initialized from those and displayed in the UI.
|
||||
// Example: /admin/collections/posts?where[or][0][and][0][text][equals]=example%20post
|
||||
const [conditions, dispatchConditions] = useReducer(
|
||||
reducer((state) => handleWhereChange({ or: state })),
|
||||
searchParams.where as Where,
|
||||
(whereFromSearch: Where) => {
|
||||
if (whereFromSearch) {
|
||||
if (validateWhereQuery(whereFromSearch)) {
|
||||
return whereFromSearch.or
|
||||
}
|
||||
/*
|
||||
stored conditions look like this:
|
||||
[
|
||||
_or_ & _and_ queries have the same shape:
|
||||
{
|
||||
and: [{
|
||||
category: {
|
||||
equals: 'category-a'
|
||||
}
|
||||
}]
|
||||
},
|
||||
|
||||
// Transform the where query to be in the right format. This will transform something simple like [text][equals]=example%20post to the right format
|
||||
const transformedWhere = transformWhereQuery(whereFromSearch)
|
||||
|
||||
if (validateWhereQuery(transformedWhere)) {
|
||||
return transformedWhere.or
|
||||
}
|
||||
|
||||
console.warn(`Invalid where query in URL: ${JSON.stringify(whereFromSearch)}`)
|
||||
{
|
||||
and:[{
|
||||
category: {
|
||||
equals: 'category-b'
|
||||
},
|
||||
text: {
|
||||
not_equals: 'oranges'
|
||||
},
|
||||
}]
|
||||
}
|
||||
return []
|
||||
]
|
||||
*/
|
||||
const [conditions, setConditions] = React.useState(() => {
|
||||
const whereFromSearch = searchParams.where
|
||||
if (whereFromSearch) {
|
||||
if (validateWhereQuery(whereFromSearch)) {
|
||||
return whereFromSearch.or
|
||||
}
|
||||
|
||||
// Transform the where query to be in the right format. This will transform something simple like [text][equals]=example%20post to the right format
|
||||
const transformedWhere = transformWhereQuery(whereFromSearch)
|
||||
|
||||
if (validateWhereQuery(transformedWhere)) {
|
||||
return transformedWhere.or
|
||||
}
|
||||
|
||||
console.warn(`Invalid where query in URL: ${JSON.stringify(whereFromSearch)}`)
|
||||
}
|
||||
|
||||
return []
|
||||
})
|
||||
|
||||
const addCondition = React.useCallback(({ andIndex, fieldName, orIndex, relation }) => {
|
||||
setConditions((prevConditions) => {
|
||||
const newConditions = [...prevConditions]
|
||||
if (relation === 'and') {
|
||||
newConditions[orIndex].and.splice(andIndex, 0, { [fieldName]: {} })
|
||||
} else {
|
||||
newConditions.push({
|
||||
and: [
|
||||
{
|
||||
[fieldName]: {},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
return newConditions
|
||||
})
|
||||
}, [])
|
||||
|
||||
const updateCondition = React.useCallback(
|
||||
({ andIndex, fieldName: fieldNameArg, operator: operatorArg, orIndex, value: valueArg }) => {
|
||||
setConditions((prevConditions) => {
|
||||
const newConditions = [...prevConditions]
|
||||
if (typeof newConditions[orIndex].and[andIndex] === 'object') {
|
||||
const [existingFieldName, existingCondition] = Object.entries(
|
||||
newConditions[orIndex].and[andIndex],
|
||||
)?.[0] || [fieldNameArg, operatorArg]
|
||||
const fieldName = existingFieldName || fieldNameArg
|
||||
const operator = operatorArg || Object.keys(existingCondition)?.[0] || undefined
|
||||
const value = valueArg ?? (operator ? newConditions[orIndex].and[andIndex][operator] : '')
|
||||
|
||||
if (fieldName) {
|
||||
newConditions[orIndex].and[andIndex] = {
|
||||
[fieldName]: operator ? { [operator]: value } : {},
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldName && operator && ![null, undefined].includes(value)) {
|
||||
setShouldUpdateQuery(true)
|
||||
}
|
||||
}
|
||||
|
||||
return newConditions
|
||||
})
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const removeCondition = React.useCallback(({ andIndex, orIndex }) => {
|
||||
setConditions((prevConditions) => {
|
||||
const newConditions = [...prevConditions]
|
||||
newConditions[orIndex].and.splice(andIndex, 1)
|
||||
|
||||
if (newConditions[orIndex].and.length === 0) {
|
||||
newConditions.splice(orIndex, 1)
|
||||
}
|
||||
|
||||
return newConditions
|
||||
})
|
||||
setShouldUpdateQuery(true)
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (shouldUpdateQuery) {
|
||||
handleWhereChange({ or: conditions })
|
||||
setShouldUpdateQuery(false)
|
||||
}
|
||||
}, [conditions, handleWhereChange, shouldUpdateQuery])
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
{conditions.length > 0 && (
|
||||
@@ -109,21 +207,34 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
|
||||
{orIndex !== 0 && <div className={`${baseClass}__label`}>{t('general:or')}</div>}
|
||||
<ul className={`${baseClass}__and-filters`}>
|
||||
{Array.isArray(or?.and) &&
|
||||
or.and.map((_, andIndex) => (
|
||||
<li key={andIndex}>
|
||||
{andIndex !== 0 && (
|
||||
<div className={`${baseClass}__label`}>{t('general:and')}</div>
|
||||
)}
|
||||
<Condition
|
||||
andIndex={andIndex}
|
||||
dispatch={dispatchConditions}
|
||||
fields={reducedFields}
|
||||
key={andIndex}
|
||||
orIndex={orIndex}
|
||||
value={conditions[orIndex].and[andIndex]}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
or.and.map((_, andIndex) => {
|
||||
const fieldName = Object.keys(conditions[orIndex].and[andIndex])[0]
|
||||
const operator =
|
||||
Object.keys(conditions[orIndex].and[andIndex]?.[fieldName] || {})?.[0] ||
|
||||
undefined
|
||||
const initialValue =
|
||||
conditions[orIndex].and[andIndex]?.[fieldName]?.[operator] || ''
|
||||
|
||||
return (
|
||||
<li key={andIndex}>
|
||||
{andIndex !== 0 && (
|
||||
<div className={`${baseClass}__label`}>{t('general:and')}</div>
|
||||
)}
|
||||
<Condition
|
||||
addCondition={addCondition}
|
||||
andIndex={andIndex}
|
||||
fieldName={fieldName}
|
||||
fields={reducedFields}
|
||||
initialValue={initialValue}
|
||||
key={andIndex}
|
||||
operator={operator}
|
||||
orIndex={orIndex}
|
||||
removeCondition={removeCondition}
|
||||
updateCondition={updateCondition}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
@@ -135,8 +246,12 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
|
||||
iconPosition="left"
|
||||
iconStyle="with-border"
|
||||
onClick={() => {
|
||||
if (reducedFields.length > 0)
|
||||
dispatchConditions({ type: 'add', field: reducedFields[0].value })
|
||||
addCondition({
|
||||
andIndex: 0,
|
||||
fieldName: reducedFields[0].value,
|
||||
orIndex: conditions.length,
|
||||
relation: 'or',
|
||||
})
|
||||
}}
|
||||
>
|
||||
{t('general:or')}
|
||||
@@ -153,8 +268,14 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
|
||||
iconPosition="left"
|
||||
iconStyle="with-border"
|
||||
onClick={() => {
|
||||
if (reducedFields.length > 0)
|
||||
dispatchConditions({ type: 'add', field: reducedFields[0].value })
|
||||
if (reducedFields.length > 0) {
|
||||
addCondition({
|
||||
andIndex: 0,
|
||||
fieldName: reducedFields[0].value,
|
||||
orIndex: conditions.length,
|
||||
relation: 'or',
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('general:addFilter')}
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
import type { Where } from 'payload/types'
|
||||
|
||||
import type { Action } from './types.js'
|
||||
|
||||
const reducer =
|
||||
(callback?: (state: Where[]) => void) =>
|
||||
(state: Where[], action: Action): Where[] => {
|
||||
const newState = [...state]
|
||||
|
||||
const { andIndex, orIndex } = action
|
||||
|
||||
switch (action.type) {
|
||||
case 'add': {
|
||||
const { field, relation } = action
|
||||
|
||||
if (relation === 'and') {
|
||||
newState[orIndex].and.splice(andIndex, 0, { [field]: {} })
|
||||
} else {
|
||||
newState.push({
|
||||
and: [
|
||||
{
|
||||
[field]: {},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'remove': {
|
||||
newState[orIndex].and.splice(andIndex, 1)
|
||||
|
||||
if (newState[orIndex].and.length === 0) {
|
||||
newState.splice(orIndex, 1)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'update': {
|
||||
const { field, operator, value } = action
|
||||
|
||||
if (typeof newState[orIndex].and[andIndex] === 'object') {
|
||||
newState[orIndex].and[andIndex] = {
|
||||
...newState[orIndex].and[andIndex],
|
||||
}
|
||||
|
||||
const [existingFieldName, existingCondition] = Object.entries(
|
||||
newState[orIndex].and[andIndex],
|
||||
)[0] || [undefined, undefined]
|
||||
|
||||
if (operator) {
|
||||
newState[orIndex].and[andIndex] = {
|
||||
[existingFieldName]: {
|
||||
[operator]: Object.values(existingCondition)[0],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (field) {
|
||||
newState[orIndex].and[andIndex] = {
|
||||
[field]: operator ? { [operator]: value } : {},
|
||||
}
|
||||
}
|
||||
|
||||
if (value !== undefined) {
|
||||
newState[orIndex].and[andIndex] = {
|
||||
[existingFieldName]: Object.keys(existingCondition)[0]
|
||||
? {
|
||||
[Object.keys(existingCondition)[0]]: value,
|
||||
}
|
||||
: {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
return newState
|
||||
}
|
||||
}
|
||||
|
||||
if (callback) callback(newState)
|
||||
return newState
|
||||
}
|
||||
|
||||
export default reducer
|
||||
Reference in New Issue
Block a user