Skip to content

Commit ec8732c

Browse files
ruslan-ambossjacobsfletch
authored andcommitted
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>
1 parent 4c1c480 commit ec8732c

File tree

5 files changed

+57
-1
lines changed

5 files changed

+57
-1
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,11 @@ export const Condition: React.FC<Props> = (props) => {
141141
<div className={`${baseClass}__field`}>
142142
<ReactSelect
143143
disabled={disabled}
144+
filterOption={(option, inputValue) =>
145+
((option?.data?.plainTextLabel as string) || option.label)
146+
.toLowerCase()
147+
.includes(inputValue.toLowerCase())
148+
}
144149
isClearable={false}
145150
onChange={handleFieldChange}
146151
options={reducedFields.filter((field) => !field.field.admin.disableListFilter)}

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { ClientField } from 'payload'
44

55
import { getTranslation } from '@payloadcms/translations'
66
import { fieldIsHiddenOrDisabled, fieldIsID, tabHasName } from 'payload/shared'
7+
import { renderToStaticMarkup } from 'react-dom/server'
78

89
import type { ReducedField } from './types.js'
910

@@ -152,10 +153,15 @@ export const reduceFields = ({
152153
})
153154
: localizedLabel
154155

156+
// React elements in filter options are not searchable in React Select
157+
// Extract plain text to make them filterable in dropdowns
158+
const textFromLabel = extractTextFromReactNode(formattedLabel)
159+
155160
const fieldPath = pathPrefix ? createNestedClientFieldPath(pathPrefix, field) : field.name
156161

157162
const formattedField: ReducedField = {
158163
label: formattedLabel,
164+
plainTextLabel: textFromLabel,
159165
value: fieldPath,
160166
...fieldTypes[field.type],
161167
field,
@@ -168,3 +174,29 @@ export const reduceFields = ({
168174
return reduced
169175
}, [])
170176
}
177+
178+
/**
179+
* Extracts plain text content from a React node by removing HTML tags.
180+
* Used to make React elements searchable in filter dropdowns.
181+
*/
182+
const extractTextFromReactNode = (reactNode: React.ReactNode): string => {
183+
if (!reactNode) {
184+
return ''
185+
}
186+
if (typeof reactNode === 'string') {
187+
return reactNode
188+
}
189+
190+
const html = renderToStaticMarkup(reactNode)
191+
192+
// Handle different environments (server vs browser)
193+
if (typeof document !== 'undefined') {
194+
// Browser environment - use actual DOM
195+
const div = document.createElement('div')
196+
div.innerHTML = html
197+
return div.textContent || ''
198+
} else {
199+
// Server environment - use regex to strip HTML tags
200+
return html.replace(/<[^>]*>/g, '')
201+
}
202+
}

packages/ui/src/elements/WhereBuilder/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export type ReducedField = {
2323
label: string
2424
value: Operator
2525
}[]
26+
plainTextLabel?: string
2627
value: Value
2728
}
2829

test/admin/e2e/list-view/e2e.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,24 @@ describe('List View', () => {
393393
await expect(page.locator(tableRowLocator)).toHaveCount(2)
394394
})
395395

396+
test('should search for nested fields in field dropdown', async () => {
397+
await page.goto(postsUrl.list)
398+
399+
await openListFilters(page, {})
400+
401+
const whereBuilder = page.locator('.where-builder')
402+
await whereBuilder.locator('.where-builder__add-first-filter').click()
403+
const conditionField = whereBuilder.locator('.condition__field')
404+
await conditionField.click()
405+
await conditionField.locator('input.rs__input').fill('Tab 1 > Title')
406+
407+
await expect(
408+
conditionField.locator('.rs__menu-list').locator('div', {
409+
hasText: exactText('Tab 1 > Title'),
410+
}),
411+
).toBeVisible()
412+
})
413+
396414
test('should allow to filter in array field', async () => {
397415
await createArray()
398416

tsconfig.base.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
}
3232
],
3333
"paths": {
34-
"@payload-config": ["./test/_community/config.ts"],
34+
"@payload-config": ["./test/admin/config.ts"],
3535
"@payloadcms/admin-bar": ["./packages/admin-bar/src"],
3636
"@payloadcms/live-preview": ["./packages/live-preview/src"],
3737
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],

0 commit comments

Comments
 (0)