Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ui): where builder issues #6478

Merged
merged 2 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/ui/src/elements/RenderCustomClientComponent/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react'

export type RenderCustomComponentProps = {
CustomComponent?: React.ComponentType<any>
DefaultComponent: React.ComponentType<any>
componentProps?: Record<string, any>
}

export const RenderCustomClientComponent: React.FC<RenderCustomComponentProps> = (props) => {
const { CustomComponent, DefaultComponent, componentProps = {} } = props

if (CustomComponent) {
return <CustomComponent {...componentProps} />
}

if (DefaultComponent) {
return <DefaultComponent {...componentProps} />
}

return null
}
5 changes: 5 additions & 0 deletions packages/ui/src/elements/RenderCustomComponent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ export type RenderCustomComponentProps = {
serverOnlyProps?: ServerProps
}

/**
* If you are passing dynamic props or function props to this component,
* you should instead use the <RenderCustomClientComponent/>
*/

export const RenderCustomComponent: React.FC<RenderCustomComponentProps> = (props) => {
const { CustomComponent, DefaultComponent, componentProps, serverOnlyProps } = props

Expand Down
8 changes: 4 additions & 4 deletions packages/ui/src/elements/WhereBuilder/Condition/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export type Props = {
}) => void
}

import { RenderCustomComponent } from '../../../elements/RenderCustomComponent/index.js'
import { RenderCustomClientComponent } from '../../../elements/RenderCustomClientComponent/index.js'
import { useDebounce } from '../../../hooks/useDebounce.js'
import { Button } from '../../Button/index.js'
import { ReactSelect } from '../../ReactSelect/index.js'
Expand Down Expand Up @@ -148,7 +148,7 @@ export const Condition: React.FC<Props> = (props) => {
/>
</div>
<div className={`${baseClass}__value`}>
<RenderCustomComponent
<RenderCustomClientComponent
CustomComponent={internalField?.props?.admin?.components?.Filter}
DefaultComponent={ValueComponent}
componentProps={{
Expand Down Expand Up @@ -190,8 +190,8 @@ export const Condition: React.FC<Props> = (props) => {
iconStyle="with-border"
onClick={() =>
addCondition({
andIndex,
fieldName,
andIndex: andIndex + 1,
fieldName: fields[0].value,
orIndex,
relation: 'and',
})
Expand Down
81 changes: 42 additions & 39 deletions packages/ui/src/elements/WhereBuilder/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
setConditions((prevConditions) => {
const newConditions = [...prevConditions]
newConditions[orIndex].and.splice(andIndex, 1)

if (newConditions[orIndex].and.length === 0) {
newConditions.splice(orIndex, 1)
}
Expand All @@ -156,44 +155,48 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
{t('general:filterWhere', { label: getTranslation(collectionPluralLabel, i18n) })}
</div>
<ul className={`${baseClass}__or-filters`}>
{conditions.map((or, orIndex) => (
<li key={orIndex}>
{orIndex !== 0 && <div className={`${baseClass}__label`}>{t('general:or')}</div>}
<ul className={`${baseClass}__and-filters`}>
{Array.isArray(or?.and) &&
or.and.map((_, andIndex) => {
const initialFieldName = Object.keys(conditions[orIndex].and[andIndex])[0]
const initialOperator =
Object.keys(
conditions[orIndex].and[andIndex]?.[initialFieldName] || {},
)?.[0] || undefined
const initialValue =
conditions[orIndex].and[andIndex]?.[initialFieldName]?.[initialOperator] ||
''

return (
<li key={andIndex}>
{andIndex !== 0 && (
<div className={`${baseClass}__label`}>{t('general:and')}</div>
)}
<Condition
addCondition={addCondition}
andIndex={andIndex}
fieldName={initialFieldName}
fields={reducedFields}
initialValue={initialValue}
key={andIndex}
operator={initialOperator}
orIndex={orIndex}
removeCondition={removeCondition}
updateCondition={updateCondition}
/>
</li>
)
})}
</ul>
</li>
))}
{conditions.map((or, orIndex) => {
const compoundOrKey = `${orIndex}_${Array.isArray(or?.and) ? or.and.length : ''}`

return (
<li key={compoundOrKey}>
{orIndex !== 0 && <div className={`${baseClass}__label`}>{t('general:or')}</div>}
<ul className={`${baseClass}__and-filters`}>
{Array.isArray(or?.and) &&
or.and.map((_, andIndex) => {
const initialFieldName = Object.keys(conditions[orIndex].and[andIndex])[0]
const initialOperator =
Object.keys(
conditions[orIndex].and[andIndex]?.[initialFieldName] || {},
)?.[0] || undefined
const initialValue =
conditions[orIndex].and[andIndex]?.[initialFieldName]?.[
initialOperator
] || ''

return (
<li key={andIndex}>
{andIndex !== 0 && (
<div className={`${baseClass}__label`}>{t('general:and')}</div>
)}
<Condition
addCondition={addCondition}
andIndex={andIndex}
fieldName={initialFieldName}
fields={reducedFields}
initialValue={initialValue}
operator={initialOperator}
orIndex={orIndex}
removeCondition={removeCondition}
updateCondition={updateCondition}
/>
</li>
)
})}
</ul>
</li>
)
})}
</ul>
<Button
buttonStyle="icon-label"
Expand Down
136 changes: 136 additions & 0 deletions test/fields/collections/Text/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Page } from '@playwright/test'

import { expect, test } from '@playwright/test'
import path from 'path'
import { wait } from 'payload/utilities'
import { fileURLToPath } from 'url'

import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'
Expand Down Expand Up @@ -205,4 +206,139 @@ describe('Text', () => {
await expect(field.locator('.rs__value-container')).toContainText(input)
await expect(field.locator('.rs__value-container')).toContainText(furtherInput)
})

test('should reset filter conditions when adding additional filters', async () => {
await page.goto(url.list)

// open the first filter options
await page.locator('.list-controls__toggle-where').click()
await expect(page.locator('.list-controls__where.rah-static--height-auto')).toBeVisible()
await page.locator('.where-builder__add-first-filter').click()

const firstInitialField = page.locator('.condition__field')
const firstOperatorField = page.locator('.condition__operator')
const firstValueField = page.locator('.condition__value >> input')

await firstInitialField.click()
const firstInitialFieldOptions = firstInitialField.locator('.rs__option')
await firstInitialFieldOptions.locator('text=text').first().click()
await expect(firstInitialField.locator('.rs__single-value')).toContainText('Text')

await firstOperatorField.click()
await firstOperatorField.locator('.rs__option').locator('text=equals').click()

await firstValueField.fill('hello')

await wait(500)

await expect(firstValueField).toHaveValue('hello')

// open the second filter options
await page.locator('.condition__actions-add').click()

const secondLi = page.locator('.where-builder__and-filters li:nth-child(2)')

await expect(secondLi).toBeVisible()

const secondInitialField = secondLi.locator('.condition__field')
const secondOperatorField = secondLi.locator('.condition__operator >> input')
const secondValueField = secondLi.locator('.condition__value >> input')

await expect(secondInitialField.locator('.rs__single-value')).toContainText('Text')
await expect(secondOperatorField).toHaveValue('')
await expect(secondValueField).toHaveValue('')
})

test('should not re-render page upon typing in a value in the filter value field', async () => {
await page.goto(url.list)

// open the first filter options
await page.locator('.list-controls__toggle-where').click()
await expect(page.locator('.list-controls__where.rah-static--height-auto')).toBeVisible()
await page.locator('.where-builder__add-first-filter').click()

const firstInitialField = page.locator('.condition__field')
const firstOperatorField = page.locator('.condition__operator')
const firstValueField = page.locator('.condition__value >> input')

await firstInitialField.click()
const firstInitialFieldOptions = firstInitialField.locator('.rs__option')
await firstInitialFieldOptions.locator('text=text').first().click()
await expect(firstInitialField.locator('.rs__single-value')).toContainText('Text')

await firstOperatorField.click()
await firstOperatorField.locator('.rs__option').locator('text=equals').click()

// Type into the input field instead of filling it
await firstValueField.click()
await firstValueField.type('hello', { delay: 100 }) // Add a delay to simulate typing speed

// Wait for a short period to see if the input loses focus
await page.waitForTimeout(500)

// Check if the input still has the correct value
await expect(firstValueField).toHaveValue('hello')
})

test('should still show second filter if two filters exist and first filter is removed', async () => {
await page.goto(url.list)

// open the first filter options
await page.locator('.list-controls__toggle-where').click()
await expect(page.locator('.list-controls__where.rah-static--height-auto')).toBeVisible()
await page.locator('.where-builder__add-first-filter').click()

const firstInitialField = page.locator('.condition__field')
const firstOperatorField = page.locator('.condition__operator')
const firstValueField = page.locator('.condition__value >> input')

await firstInitialField.click()
const firstInitialFieldOptions = firstInitialField.locator('.rs__option')
await firstInitialFieldOptions.locator('text=text').first().click()
await expect(firstInitialField.locator('.rs__single-value')).toContainText('Text')

await firstOperatorField.click()
await firstOperatorField.locator('.rs__option').locator('text=equals').click()

await firstValueField.fill('hello')

await wait(500)

await expect(firstValueField).toHaveValue('hello')

// open the second filter options
await page.locator('.condition__actions-add').click()

const secondLi = page.locator('.where-builder__and-filters li:nth-child(2)')

await expect(secondLi).toBeVisible()

const secondInitialField = secondLi.locator('.condition__field')
const secondOperatorField = secondLi.locator('.condition__operator')
const secondValueField = secondLi.locator('.condition__value >> input')

await secondInitialField.click()
const secondInitialFieldOptions = secondInitialField.locator('.rs__option')
await secondInitialFieldOptions.locator('text=text').first().click()
await expect(secondInitialField.locator('.rs__single-value')).toContainText('Text')

await secondOperatorField.click()
await secondOperatorField.locator('.rs__option').locator('text=equals').click()

await secondValueField.fill('world')
await expect(secondValueField).toHaveValue('world')

await wait(500)

const firstLi = page.locator('.where-builder__and-filters li:nth-child(1)')
const removeButton = firstLi.locator('.condition__actions-remove')

// remove first filter
await removeButton.click()

const filterListItems = page.locator('.where-builder__and-filters li')
await expect(filterListItems).toHaveCount(1)

await expect(firstValueField).toHaveValue('world')
})
})
Loading