fix(tests): number field e2e (#5452)

This commit is contained in:
Jarrod Flesch
2024-03-25 13:17:13 -04:00
committed by GitHub
parent 2cd8d891a1
commit 99a00a1ae2
4 changed files with 251 additions and 183 deletions

View File

@@ -18,6 +18,12 @@
}
}
&__field {
.field-label {
padding-bottom: 0;
}
}
&__actions {
flex-shrink: 0;
display: flex;

View File

@@ -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',
})

View File

@@ -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')}

View File

@@ -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