fix(next): nested relationship filter options (#11375)
Continuation of #11008. When `filterOptions` are set on a relationship field that is _nested within another field_, those filter options are not applied to `Filter` component in the list view. This is because we were only shallowly resolving filter options on top-level fields, as opposed to recursively traversing fields to resolve them even when deeply nested.
This commit is contained in:
@@ -153,7 +153,7 @@ export const renderListView = async (
|
||||
const renderedFilters = renderFilters(collectionConfig.fields, req.payload.importMap)
|
||||
|
||||
const resolvedFilterOptions = await resolveAllFilterOptions({
|
||||
collectionConfig,
|
||||
fields: collectionConfig.fields,
|
||||
req,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import type { CollectionConfig, PayloadRequest, ResolvedFilterOptions } from 'payload'
|
||||
import type { Field, PayloadRequest, ResolvedFilterOptions } from 'payload'
|
||||
|
||||
import { resolveFilterOptions } from '@payloadcms/ui/rsc'
|
||||
import { fieldIsHiddenOrDisabled } from 'payload/shared'
|
||||
import { fieldHasSubFields, fieldIsHiddenOrDisabled } from 'payload/shared'
|
||||
|
||||
export const resolveAllFilterOptions = async ({
|
||||
collectionConfig,
|
||||
fields,
|
||||
req,
|
||||
result,
|
||||
}: {
|
||||
collectionConfig: CollectionConfig
|
||||
fields: Field[]
|
||||
req: PayloadRequest
|
||||
result?: Map<string, ResolvedFilterOptions>
|
||||
}): Promise<Map<string, ResolvedFilterOptions>> => {
|
||||
const resolvedFilterOptions = new Map<string, ResolvedFilterOptions>()
|
||||
const resolvedFilterOptions = !result ? new Map<string, ResolvedFilterOptions>() : result
|
||||
|
||||
await Promise.all(
|
||||
collectionConfig.fields.map(async (field) => {
|
||||
fields.map(async (field) => {
|
||||
if (fieldIsHiddenOrDisabled(field)) {
|
||||
return
|
||||
}
|
||||
@@ -28,8 +30,29 @@ export const resolveAllFilterOptions = async ({
|
||||
siblingData: {}, // use empty object to prevent breaking queries when accessing properties of data
|
||||
user: req.user,
|
||||
})
|
||||
|
||||
resolvedFilterOptions.set(field.name, options)
|
||||
}
|
||||
|
||||
if (fieldHasSubFields(field)) {
|
||||
await resolveAllFilterOptions({
|
||||
fields: field.fields,
|
||||
req,
|
||||
result: resolvedFilterOptions,
|
||||
})
|
||||
}
|
||||
|
||||
if (field.type === 'tabs') {
|
||||
await Promise.all(
|
||||
field.tabs.map((tab) =>
|
||||
resolveAllFilterOptions({
|
||||
fields: tab.fields,
|
||||
req,
|
||||
result: resolvedFilterOptions,
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -88,6 +88,28 @@ export const Relationship: CollectionConfig = {
|
||||
relationTo: slug,
|
||||
type: 'relationship',
|
||||
},
|
||||
{
|
||||
type: 'collapsible',
|
||||
label: 'Collapsible',
|
||||
fields: [
|
||||
{
|
||||
name: 'nestedRelationshipFilteredByField',
|
||||
filterOptions: () => {
|
||||
return {
|
||||
filter: {
|
||||
equals: 'Include me',
|
||||
},
|
||||
}
|
||||
},
|
||||
admin: {
|
||||
description:
|
||||
'This will filter the relationship options if the filter field in this document is set to "Include me"',
|
||||
},
|
||||
relationTo: slug,
|
||||
type: 'relationship',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'relationshipFilteredAsync',
|
||||
filterOptions: (args: FilterOptionsProps<FieldsRelationship>) => {
|
||||
|
||||
@@ -351,6 +351,41 @@ describe('Relationship Field', () => {
|
||||
await expect(valueOptions.locator(`text=${idToInclude}`)).toBeVisible()
|
||||
})
|
||||
|
||||
test('should apply filter options of nested fields to list view filter controls', async () => {
|
||||
const { id: idToInclude } = await payload.create({
|
||||
collection: slug,
|
||||
data: {
|
||||
filter: 'Include me',
|
||||
},
|
||||
})
|
||||
|
||||
// first ensure that filter options are applied in the edit view
|
||||
await page.goto(url.edit(idToInclude))
|
||||
const field = page.locator('#field-nestedRelationshipFilteredByField')
|
||||
await field.click({ delay: 100 })
|
||||
const options = field.locator('.rs__option')
|
||||
await expect(options).toHaveCount(1)
|
||||
await expect(options).toContainText(idToInclude)
|
||||
|
||||
// now ensure that the same filter options are applied in the list view
|
||||
await page.goto(url.list)
|
||||
|
||||
const whereBuilder = await addListFilter({
|
||||
page,
|
||||
fieldLabel: 'Collapsible > Nested Relationship Filtered By Field',
|
||||
operatorLabel: 'equals',
|
||||
skipValueInput: true,
|
||||
})
|
||||
|
||||
const valueInput = page.locator('.condition__value input')
|
||||
await valueInput.click()
|
||||
const valueOptions = whereBuilder.locator('.condition__value .rs__option')
|
||||
|
||||
await expect(valueOptions).toHaveCount(2)
|
||||
await expect(valueOptions.locator(`text=None`)).toBeVisible()
|
||||
await expect(valueOptions.locator(`text=${idToInclude}`)).toBeVisible()
|
||||
})
|
||||
|
||||
test('should allow usage of relationTo in filterOptions', async () => {
|
||||
const { id: include } = (await payload.create({
|
||||
collection: relationOneSlug,
|
||||
|
||||
@@ -177,6 +177,10 @@ export interface FieldsRelationship {
|
||||
* This will filter the relationship options if the filter field in this document is set to "Include me"
|
||||
*/
|
||||
relationshipFilteredByField?: (string | null) | FieldsRelationship;
|
||||
/**
|
||||
* This will filter the relationship options if the filter field in this document is set to "Include me"
|
||||
*/
|
||||
nestedRelationshipFilteredByField?: (string | null) | FieldsRelationship;
|
||||
relationshipFilteredAsync?: (string | null) | RelationOne;
|
||||
relationshipManyFiltered?:
|
||||
| (
|
||||
@@ -506,6 +510,7 @@ export interface FieldsRelationshipSelect<T extends boolean = true> {
|
||||
relationshipWithTitle?: T;
|
||||
relationshipFilteredByID?: T;
|
||||
relationshipFilteredByField?: T;
|
||||
nestedRelationshipFilteredByField?: T;
|
||||
relationshipFilteredAsync?: T;
|
||||
relationshipManyFiltered?: T;
|
||||
filter?: T;
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@payload-config": ["./test/admin/config.ts"],
|
||||
"@payload-config": ["./test/fields-relationship/config.ts"],
|
||||
"@payloadcms/live-preview": ["./packages/live-preview/src"],
|
||||
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
|
||||
"@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],
|
||||
|
||||
Reference in New Issue
Block a user