Skip to content

Commit 42222cd

Browse files
fix(ui): where builder issues (#6478)
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
1 parent e3222f2 commit 42222cd

File tree

5 files changed

+208
-43
lines changed

5 files changed

+208
-43
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from 'react'
2+
3+
export type RenderCustomComponentProps = {
4+
CustomComponent?: React.ComponentType<any>
5+
DefaultComponent: React.ComponentType<any>
6+
componentProps?: Record<string, any>
7+
}
8+
9+
export const RenderCustomClientComponent: React.FC<RenderCustomComponentProps> = (props) => {
10+
const { CustomComponent, DefaultComponent, componentProps = {} } = props
11+
12+
if (CustomComponent) {
13+
return <CustomComponent {...componentProps} />
14+
}
15+
16+
if (DefaultComponent) {
17+
return <DefaultComponent {...componentProps} />
18+
}
19+
20+
return null
21+
}

packages/ui/src/elements/RenderCustomComponent/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ export type RenderCustomComponentProps = {
1414
serverOnlyProps?: ServerProps
1515
}
1616

17+
/**
18+
* If you are passing dynamic props or function props to this component,
19+
* you should instead use the <RenderCustomClientComponent/>
20+
*/
21+
1722
export const RenderCustomComponent: React.FC<RenderCustomComponentProps> = (props) => {
1823
const { CustomComponent, DefaultComponent, componentProps, serverOnlyProps } = props
1924

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export type Props = {
3636
}) => void
3737
}
3838

39-
import { RenderCustomComponent } from '../../../elements/RenderCustomComponent/index.js'
39+
import { RenderCustomClientComponent } from '../../../elements/RenderCustomClientComponent/index.js'
4040
import { useDebounce } from '../../../hooks/useDebounce.js'
4141
import { Button } from '../../Button/index.js'
4242
import { ReactSelect } from '../../ReactSelect/index.js'
@@ -148,7 +148,7 @@ export const Condition: React.FC<Props> = (props) => {
148148
/>
149149
</div>
150150
<div className={`${baseClass}__value`}>
151-
<RenderCustomComponent
151+
<RenderCustomClientComponent
152152
CustomComponent={internalField?.props?.admin?.components?.Filter}
153153
DefaultComponent={ValueComponent}
154154
componentProps={{
@@ -190,8 +190,8 @@ export const Condition: React.FC<Props> = (props) => {
190190
iconStyle="with-border"
191191
onClick={() =>
192192
addCondition({
193-
andIndex,
194-
fieldName,
193+
andIndex: andIndex + 1,
194+
fieldName: fields[0].value,
195195
orIndex,
196196
relation: 'and',
197197
})

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

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,6 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
131131
setConditions((prevConditions) => {
132132
const newConditions = [...prevConditions]
133133
newConditions[orIndex].and.splice(andIndex, 1)
134-
135134
if (newConditions[orIndex].and.length === 0) {
136135
newConditions.splice(orIndex, 1)
137136
}
@@ -156,44 +155,48 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
156155
{t('general:filterWhere', { label: getTranslation(collectionPluralLabel, i18n) })}
157156
</div>
158157
<ul className={`${baseClass}__or-filters`}>
159-
{conditions.map((or, orIndex) => (
160-
<li key={orIndex}>
161-
{orIndex !== 0 && <div className={`${baseClass}__label`}>{t('general:or')}</div>}
162-
<ul className={`${baseClass}__and-filters`}>
163-
{Array.isArray(or?.and) &&
164-
or.and.map((_, andIndex) => {
165-
const initialFieldName = Object.keys(conditions[orIndex].and[andIndex])[0]
166-
const initialOperator =
167-
Object.keys(
168-
conditions[orIndex].and[andIndex]?.[initialFieldName] || {},
169-
)?.[0] || undefined
170-
const initialValue =
171-
conditions[orIndex].and[andIndex]?.[initialFieldName]?.[initialOperator] ||
172-
''
173-
174-
return (
175-
<li key={andIndex}>
176-
{andIndex !== 0 && (
177-
<div className={`${baseClass}__label`}>{t('general:and')}</div>
178-
)}
179-
<Condition
180-
addCondition={addCondition}
181-
andIndex={andIndex}
182-
fieldName={initialFieldName}
183-
fields={reducedFields}
184-
initialValue={initialValue}
185-
key={andIndex}
186-
operator={initialOperator}
187-
orIndex={orIndex}
188-
removeCondition={removeCondition}
189-
updateCondition={updateCondition}
190-
/>
191-
</li>
192-
)
193-
})}
194-
</ul>
195-
</li>
196-
))}
158+
{conditions.map((or, orIndex) => {
159+
const compoundOrKey = `${orIndex}_${Array.isArray(or?.and) ? or.and.length : ''}`
160+
161+
return (
162+
<li key={compoundOrKey}>
163+
{orIndex !== 0 && <div className={`${baseClass}__label`}>{t('general:or')}</div>}
164+
<ul className={`${baseClass}__and-filters`}>
165+
{Array.isArray(or?.and) &&
166+
or.and.map((_, andIndex) => {
167+
const initialFieldName = Object.keys(conditions[orIndex].and[andIndex])[0]
168+
const initialOperator =
169+
Object.keys(
170+
conditions[orIndex].and[andIndex]?.[initialFieldName] || {},
171+
)?.[0] || undefined
172+
const initialValue =
173+
conditions[orIndex].and[andIndex]?.[initialFieldName]?.[
174+
initialOperator
175+
] || ''
176+
177+
return (
178+
<li key={andIndex}>
179+
{andIndex !== 0 && (
180+
<div className={`${baseClass}__label`}>{t('general:and')}</div>
181+
)}
182+
<Condition
183+
addCondition={addCondition}
184+
andIndex={andIndex}
185+
fieldName={initialFieldName}
186+
fields={reducedFields}
187+
initialValue={initialValue}
188+
operator={initialOperator}
189+
orIndex={orIndex}
190+
removeCondition={removeCondition}
191+
updateCondition={updateCondition}
192+
/>
193+
</li>
194+
)
195+
})}
196+
</ul>
197+
</li>
198+
)
199+
})}
197200
</ul>
198201
<Button
199202
buttonStyle="icon-label"

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

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

33
import { expect, test } from '@playwright/test'
44
import path from 'path'
5+
import { wait } from 'payload/utilities'
56
import { fileURLToPath } from 'url'
67

78
import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'
@@ -205,4 +206,139 @@ describe('Text', () => {
205206
await expect(field.locator('.rs__value-container')).toContainText(input)
206207
await expect(field.locator('.rs__value-container')).toContainText(furtherInput)
207208
})
209+
210+
test('should reset filter conditions when adding additional filters', async () => {
211+
await page.goto(url.list)
212+
213+
// open the first filter options
214+
await page.locator('.list-controls__toggle-where').click()
215+
await expect(page.locator('.list-controls__where.rah-static--height-auto')).toBeVisible()
216+
await page.locator('.where-builder__add-first-filter').click()
217+
218+
const firstInitialField = page.locator('.condition__field')
219+
const firstOperatorField = page.locator('.condition__operator')
220+
const firstValueField = page.locator('.condition__value >> input')
221+
222+
await firstInitialField.click()
223+
const firstInitialFieldOptions = firstInitialField.locator('.rs__option')
224+
await firstInitialFieldOptions.locator('text=text').first().click()
225+
await expect(firstInitialField.locator('.rs__single-value')).toContainText('Text')
226+
227+
await firstOperatorField.click()
228+
await firstOperatorField.locator('.rs__option').locator('text=equals').click()
229+
230+
await firstValueField.fill('hello')
231+
232+
await wait(500)
233+
234+
await expect(firstValueField).toHaveValue('hello')
235+
236+
// open the second filter options
237+
await page.locator('.condition__actions-add').click()
238+
239+
const secondLi = page.locator('.where-builder__and-filters li:nth-child(2)')
240+
241+
await expect(secondLi).toBeVisible()
242+
243+
const secondInitialField = secondLi.locator('.condition__field')
244+
const secondOperatorField = secondLi.locator('.condition__operator >> input')
245+
const secondValueField = secondLi.locator('.condition__value >> input')
246+
247+
await expect(secondInitialField.locator('.rs__single-value')).toContainText('Text')
248+
await expect(secondOperatorField).toHaveValue('')
249+
await expect(secondValueField).toHaveValue('')
250+
})
251+
252+
test('should not re-render page upon typing in a value in the filter value field', async () => {
253+
await page.goto(url.list)
254+
255+
// open the first filter options
256+
await page.locator('.list-controls__toggle-where').click()
257+
await expect(page.locator('.list-controls__where.rah-static--height-auto')).toBeVisible()
258+
await page.locator('.where-builder__add-first-filter').click()
259+
260+
const firstInitialField = page.locator('.condition__field')
261+
const firstOperatorField = page.locator('.condition__operator')
262+
const firstValueField = page.locator('.condition__value >> input')
263+
264+
await firstInitialField.click()
265+
const firstInitialFieldOptions = firstInitialField.locator('.rs__option')
266+
await firstInitialFieldOptions.locator('text=text').first().click()
267+
await expect(firstInitialField.locator('.rs__single-value')).toContainText('Text')
268+
269+
await firstOperatorField.click()
270+
await firstOperatorField.locator('.rs__option').locator('text=equals').click()
271+
272+
// Type into the input field instead of filling it
273+
await firstValueField.click()
274+
await firstValueField.type('hello', { delay: 100 }) // Add a delay to simulate typing speed
275+
276+
// Wait for a short period to see if the input loses focus
277+
await page.waitForTimeout(500)
278+
279+
// Check if the input still has the correct value
280+
await expect(firstValueField).toHaveValue('hello')
281+
})
282+
283+
test('should still show second filter if two filters exist and first filter is removed', async () => {
284+
await page.goto(url.list)
285+
286+
// open the first filter options
287+
await page.locator('.list-controls__toggle-where').click()
288+
await expect(page.locator('.list-controls__where.rah-static--height-auto')).toBeVisible()
289+
await page.locator('.where-builder__add-first-filter').click()
290+
291+
const firstInitialField = page.locator('.condition__field')
292+
const firstOperatorField = page.locator('.condition__operator')
293+
const firstValueField = page.locator('.condition__value >> input')
294+
295+
await firstInitialField.click()
296+
const firstInitialFieldOptions = firstInitialField.locator('.rs__option')
297+
await firstInitialFieldOptions.locator('text=text').first().click()
298+
await expect(firstInitialField.locator('.rs__single-value')).toContainText('Text')
299+
300+
await firstOperatorField.click()
301+
await firstOperatorField.locator('.rs__option').locator('text=equals').click()
302+
303+
await firstValueField.fill('hello')
304+
305+
await wait(500)
306+
307+
await expect(firstValueField).toHaveValue('hello')
308+
309+
// open the second filter options
310+
await page.locator('.condition__actions-add').click()
311+
312+
const secondLi = page.locator('.where-builder__and-filters li:nth-child(2)')
313+
314+
await expect(secondLi).toBeVisible()
315+
316+
const secondInitialField = secondLi.locator('.condition__field')
317+
const secondOperatorField = secondLi.locator('.condition__operator')
318+
const secondValueField = secondLi.locator('.condition__value >> input')
319+
320+
await secondInitialField.click()
321+
const secondInitialFieldOptions = secondInitialField.locator('.rs__option')
322+
await secondInitialFieldOptions.locator('text=text').first().click()
323+
await expect(secondInitialField.locator('.rs__single-value')).toContainText('Text')
324+
325+
await secondOperatorField.click()
326+
await secondOperatorField.locator('.rs__option').locator('text=equals').click()
327+
328+
await secondValueField.fill('world')
329+
await expect(secondValueField).toHaveValue('world')
330+
331+
await wait(500)
332+
333+
const firstLi = page.locator('.where-builder__and-filters li:nth-child(1)')
334+
const removeButton = firstLi.locator('.condition__actions-remove')
335+
336+
// remove first filter
337+
await removeButton.click()
338+
339+
const filterListItems = page.locator('.where-builder__and-filters li')
340+
await expect(filterListItems).toHaveCount(1)
341+
342+
await expect(firstValueField).toHaveValue('world')
343+
})
208344
})

0 commit comments

Comments
 (0)