fix(ui): unable to search for nested fields in WhereBuilder field selection (#11986)

### What?
Extract text from the React node label in WhereBuilder

### Why?
If you have a nested field in filter options, the label would show
correctly, but the search will not work

### How
By adding an `extractTextFromReactNode` function that gets text out of
React.node label

### Code setup:
```
{
      type: "collapsible",
      label: "Meta",
      fields: [
        {
          name: 'media',
          type: 'relationship',
          relationTo: 'media',
          label: 'Ferrari',
          filterOptions: () => {
            return {
              id: { in: ['67efdbc872ca925bc2868933'] },
            }
          }
        },
        {
          name: 'media2',
          type: 'relationship',
          relationTo: 'media',
          label: 'Williams',
          filterOptions: () => {
            return {
              id: { in: ['67efdbc272ca925bc286891c'] },
            }
          }
        },
      ],
    },
    
 ```
  
### Before:

https://github.com/user-attachments/assets/25d4b3a2-6ac0-476b-973e-575238e916c4

  
 ### After:

https://github.com/user-attachments/assets/92346a6c-b2d1-4e08-b1e4-9ac1484f9ef3

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
This commit is contained in:
Ruslan
2025-05-05 19:09:26 +02:00
committed by GitHub
parent a6d76d6058
commit 38186346f7
5 changed files with 57 additions and 1 deletions

View File

@@ -141,6 +141,11 @@ export const Condition: React.FC<Props> = (props) => {
<div className={`${baseClass}__field`}>
<ReactSelect
disabled={disabled}
filterOption={(option, inputValue) =>
((option?.data?.plainTextLabel as string) || option.label)
.toLowerCase()
.includes(inputValue.toLowerCase())
}
isClearable={false}
onChange={handleFieldChange}
options={reducedFields.filter((field) => !field.field.admin.disableListFilter)}

View File

@@ -4,6 +4,7 @@ import type { ClientField } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { fieldIsHiddenOrDisabled, fieldIsID, tabHasName } from 'payload/shared'
import { renderToStaticMarkup } from 'react-dom/server'
import type { ReducedField } from './types.js'
@@ -152,10 +153,15 @@ export const reduceFields = ({
})
: localizedLabel
// React elements in filter options are not searchable in React Select
// Extract plain text to make them filterable in dropdowns
const textFromLabel = extractTextFromReactNode(formattedLabel)
const fieldPath = pathPrefix ? createNestedClientFieldPath(pathPrefix, field) : field.name
const formattedField: ReducedField = {
label: formattedLabel,
plainTextLabel: textFromLabel,
value: fieldPath,
...fieldTypes[field.type],
field,
@@ -168,3 +174,29 @@ export const reduceFields = ({
return reduced
}, [])
}
/**
* Extracts plain text content from a React node by removing HTML tags.
* Used to make React elements searchable in filter dropdowns.
*/
const extractTextFromReactNode = (reactNode: React.ReactNode): string => {
if (!reactNode) {
return ''
}
if (typeof reactNode === 'string') {
return reactNode
}
const html = renderToStaticMarkup(reactNode)
// Handle different environments (server vs browser)
if (typeof document !== 'undefined') {
// Browser environment - use actual DOM
const div = document.createElement('div')
div.innerHTML = html
return div.textContent || ''
} else {
// Server environment - use regex to strip HTML tags
return html.replace(/<[^>]*>/g, '')
}
}

View File

@@ -23,6 +23,7 @@ export type ReducedField = {
label: string
value: Operator
}[]
plainTextLabel?: string
value: Value
}

View File

@@ -393,6 +393,24 @@ describe('List View', () => {
await expect(page.locator(tableRowLocator)).toHaveCount(2)
})
test('should search for nested fields in field dropdown', async () => {
await page.goto(postsUrl.list)
await openListFilters(page, {})
const whereBuilder = page.locator('.where-builder')
await whereBuilder.locator('.where-builder__add-first-filter').click()
const conditionField = whereBuilder.locator('.condition__field')
await conditionField.click()
await conditionField.locator('input.rs__input').fill('Tab 1 > Title')
await expect(
conditionField.locator('.rs__menu-list').locator('div', {
hasText: exactText('Tab 1 > Title'),
}),
).toBeVisible()
})
test('should allow to filter in array field', async () => {
await createArray()

View File

@@ -31,7 +31,7 @@
}
],
"paths": {
"@payload-config": ["./test/_community/config.ts"],
"@payload-config": ["./test/admin/config.ts"],
"@payloadcms/admin-bar": ["./packages/admin-bar/src"],
"@payloadcms/live-preview": ["./packages/live-preview/src"],
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],