Skip to content

Commit 2a0094d

Browse files
authored
fix(ui): relationship filterOptions not applied within the list view (#11008)
Fixes #10440. When `filterOptions` are set on a relationship field, those same filters are not applied to the `Filter` component within the list view. This is because `filterOptions` is not being thread into the `RelationshipFilter` component responsible for populating the available options. To do this, we first need to be resolve the filter options on the server as they accept functions. Once resolved, they can be prop-drilled into the proper component and appended onto the client-side "where" query. Reliant on #11080.
1 parent 48471b7 commit 2a0094d

File tree

22 files changed

+294
-55
lines changed

22 files changed

+294
-55
lines changed

docs/fields/relationship.mdx

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -136,21 +136,19 @@ Note: If `sortOptions` is not defined, the default sorting behavior of the Relat
136136

137137
## Filtering relationship options
138138

139-
Options can be dynamically limited by supplying a [query constraint](/docs/queries/overview), which will be used both
140-
for validating input and filtering available relationships in the UI.
141-
142-
The `filterOptions` property can either be a `Where` query, or a function returning `true` to not filter, `false` to
143-
prevent all, or a `Where` query. When using a function, it will be
144-
called with an argument object with the following properties:
145-
146-
| Property | Description |
147-
| ------------- | ----------------------------------------------------------------------------------------------------- |
148-
| `relationTo` | The collection `slug` to filter against, limited to this field's `relationTo` property |
149-
| `data` | An object containing the full collection or global document currently being edited |
150-
| `siblingData` | An object containing document data that is scoped to only fields within the same parent of this field |
151-
| `id` | The `id` of the current document being edited. `id` is `undefined` during the `create` operation |
152-
| `user` | An object containing the currently authenticated user |
153-
| `req` | The Payload Request, which contains references to `payload`, `user`, `locale`, and more. |
139+
Options can be dynamically limited by supplying a [query constraint](/docs/queries/overview), which will be used both for validating input and filtering available relationships in the UI.
140+
141+
The `filterOptions` property can either be a `Where` query, or a function returning `true` to not filter, `false` to prevent all, or a `Where` query. When using a function, it will be called with an argument object with the following properties:
142+
143+
| Property | Description |
144+
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
145+
| `blockData` | The data of the nearest parent block. Will be `undefined` if the field is not within a block or when called on a `Filter` component within the list view. |
146+
| `data` | An object containing the full collection or global document currently being edited. Will be an empty object when called on a `Filter` component within the list view. |
147+
| `id` | The `id` of the current document being edited. Will be `undefined` during the `create` operation or when called on a `Filter` component within the list view. |
148+
| `relationTo` | The collection `slug` to filter against, limited to this field's `relationTo` property. |
149+
| `req` | The Payload Request, which contains references to `payload`, `user`, `locale`, and more. |
150+
| `siblingData` | An object containing document data that is scoped to only fields within the same parent of this field. Will be an emprt object when called on a `Filter` component within the list view. |
151+
| `user` | An object containing the currently authenticated user. |
154152

155153
## Example
156154

packages/next/src/views/List/index.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { isNumber } from 'payload/shared'
1515
import React, { Fragment } from 'react'
1616

1717
import { renderListViewSlots } from './renderListViewSlots.js'
18+
import { resolveAllFilterOptions } from './resolveAllFilterOptions.js'
1819

1920
export { generateListMetadata } from './meta.js'
2021

@@ -149,6 +150,11 @@ export const renderListView = async (
149150

150151
const renderedFilters = renderFilters(collectionConfig.fields, req.payload.importMap)
151152

153+
const resolvedFilterOptions = await resolveAllFilterOptions({
154+
collectionConfig,
155+
req,
156+
})
157+
152158
const staticDescription =
153159
typeof collectionConfig.admin.description === 'function'
154160
? collectionConfig.admin.description({ t: i18n.t })
@@ -192,6 +198,7 @@ export const renderListView = async (
192198
enableRowSelections,
193199
listPreferences,
194200
renderedFilters,
201+
resolvedFilterOptions,
195202
Table,
196203
}
197204

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { CollectionConfig, PayloadRequest, ResolvedFilterOptions } from 'payload'
2+
3+
import { resolveFilterOptions } from '@payloadcms/ui/rsc'
4+
import { fieldIsHiddenOrDisabled } from 'payload/shared'
5+
6+
export const resolveAllFilterOptions = async ({
7+
collectionConfig,
8+
req,
9+
}: {
10+
collectionConfig: CollectionConfig
11+
req: PayloadRequest
12+
}): Promise<Map<string, ResolvedFilterOptions>> => {
13+
const resolvedFilterOptions = new Map<string, ResolvedFilterOptions>()
14+
15+
await Promise.all(
16+
collectionConfig.fields.map(async (field) => {
17+
if (fieldIsHiddenOrDisabled(field)) {
18+
return
19+
}
20+
21+
if ('name' in field && 'filterOptions' in field && field.filterOptions) {
22+
const options = await resolveFilterOptions(field.filterOptions, {
23+
id: undefined,
24+
blockData: undefined,
25+
data: {}, // use empty object to prevent breaking queries when accessing properties of data
26+
relationTo: field.relationTo,
27+
req,
28+
siblingData: {}, // use empty object to prevent breaking queries when accessing properties of data
29+
user: req.user,
30+
})
31+
resolvedFilterOptions.set(field.name, options)
32+
}
33+
}),
34+
)
35+
36+
return resolvedFilterOptions
37+
}

packages/payload/src/fields/config/types.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -269,15 +269,15 @@ export type Condition<TData extends TypeWithID = any, TSiblingData = any> = (
269269

270270
export type FilterOptionsProps<TData = any> = {
271271
/**
272-
* The data of the nearest parent block. If the field is not within a block, `blockData` will be equal to `undefined`.
272+
* The data of the nearest parent block. Will be `undefined` if the field is not within a block or when called on a `Filter` component within the list view.
273273
*/
274274
blockData: TData
275275
/**
276-
* An object containing the full collection or global document currently being edited.
276+
* An object containing the full collection or global document currently being edited. Will be an empty object when called on a `Filter` component within the list view.
277277
*/
278278
data: TData
279279
/**
280-
* The `id` of the current document being edited. `id` is undefined during the `create` operation.
280+
* The `id` of the current document being edited. Will be undefined during the `create` operation or when called on a `Filter` component within the list view.
281281
*/
282282
id: number | string
283283
/**
@@ -286,7 +286,7 @@ export type FilterOptionsProps<TData = any> = {
286286
relationTo: CollectionSlug
287287
req: PayloadRequest
288288
/**
289-
* An object containing document data that is scoped to only fields within the same parent of this field.
289+
* An object containing document data that is scoped to only fields within the same parent of this field. Will be an empty object when called on a `Filter` component within the list view.
290290
*/
291291
siblingData: unknown
292292
/**

packages/payload/src/types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,5 @@ export type TransformGlobalWithSelect<
239239
: DataFromGlobalSlug<TSlug>
240240

241241
export type PopulateType = Partial<TypedCollectionSelect>
242+
243+
export type ResolvedFilterOptions = { [collection: string]: Where }

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use client'
2-
import type { ClientCollectionConfig, Where } from 'payload'
2+
import type { ClientCollectionConfig, ResolvedFilterOptions, Where } from 'payload'
33

44
import { useWindowInfo } from '@faceless-ui/window-info'
55
import { getTranslation } from '@payloadcms/translations'
@@ -20,8 +20,8 @@ import { SearchFilter } from '../SearchFilter/index.js'
2020
import { UnpublishMany } from '../UnpublishMany/index.js'
2121
import { WhereBuilder } from '../WhereBuilder/index.js'
2222
import validateWhereQuery from '../WhereBuilder/validateWhereQuery.js'
23-
import './index.scss'
2423
import { getTextFieldsToBeSearched } from './getTextFieldsToBeSearched.js'
24+
import './index.scss'
2525

2626
const baseClass = 'list-controls'
2727

@@ -37,6 +37,7 @@ export type ListControlsProps = {
3737
readonly handleSortChange?: (sort: string) => void
3838
readonly handleWhereChange?: (where: Where) => void
3939
readonly renderedFilters?: Map<string, React.ReactNode>
40+
readonly resolvedFilterOptions?: Map<string, ResolvedFilterOptions>
4041
}
4142

4243
/**
@@ -54,6 +55,7 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
5455
enableColumns = true,
5556
enableSort = false,
5657
renderedFilters,
58+
resolvedFilterOptions,
5759
} = props
5860

5961
const { handleSearchChange, query } = useListQuery()
@@ -214,6 +216,7 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
214216
collectionSlug={collectionConfig.slug}
215217
fields={collectionConfig?.fields}
216218
renderedFilters={renderedFilters}
219+
resolvedFilterOptions={resolvedFilterOptions}
217220
/>
218221
</AnimateHeight>
219222
{enableSort && (

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import type { Operator, Option, SelectFieldClient, TextFieldClient } from 'payload'
1+
import type {
2+
Operator,
3+
Option,
4+
ResolvedFilterOptions,
5+
SelectFieldClient,
6+
TextFieldClient,
7+
} from 'payload'
28

39
import React from 'react'
410

@@ -13,6 +19,7 @@ import { Text } from '../Text/index.js'
1319
type Props = {
1420
booleanSelect: boolean
1521
disabled: boolean
22+
filterOptions: ResolvedFilterOptions
1623
internalField: ReducedField
1724
onChange: React.Dispatch<React.SetStateAction<string>>
1825
operator: Operator
@@ -23,6 +30,7 @@ type Props = {
2330
export const DefaultFilter: React.FC<Props> = ({
2431
booleanSelect,
2532
disabled,
33+
filterOptions,
2634
internalField,
2735
onChange,
2836
operator,
@@ -73,6 +81,7 @@ export const DefaultFilter: React.FC<Props> = ({
7381
<RelationshipFilter
7482
disabled={disabled}
7583
field={internalField.field}
84+
filterOptions={filterOptions}
7685
onChange={onChange}
7786
operator={operator}
7887
value={value}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const RelationshipFilter: React.FC<Props> = (props) => {
2323
const {
2424
disabled,
2525
field: { admin: { isSortable } = {}, hasMany, relationTo },
26+
filterOptions,
2627
onChange,
2728
value,
2829
} = props
@@ -104,6 +105,10 @@ export const RelationshipFilter: React.FC<Props> = (props) => {
104105
where,
105106
}
106107

108+
if (filterOptions && filterOptions?.[relationSlug]) {
109+
query.where.and.push(filterOptions[relationSlug])
110+
}
111+
107112
if (debouncedSearch) {
108113
query.where.and.push({
109114
[fieldToSearch]: {

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import type { I18nClient } from '@payloadcms/translations'
2-
import type { ClientCollectionConfig, PaginatedDocs, RelationshipFieldClient } from 'payload'
2+
import type {
3+
ClientCollectionConfig,
4+
PaginatedDocs,
5+
RelationshipFieldClient,
6+
ResolvedFilterOptions,
7+
} from 'payload'
38

49
import type { DefaultFilterProps } from '../types.js'
510

611
export type Props = {
712
readonly field: RelationshipFieldClient
13+
readonly filterOptions: ResolvedFilterOptions
814
} & DefaultFilterProps
915

1016
export type Option = {

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type Props = {
77
readonly addCondition: AddCondition
88
readonly andIndex: number
99
readonly fieldName: string
10+
readonly filterOptions: ResolvedFilterOptions
1011
readonly operator: Operator
1112
readonly orIndex: number
1213
readonly reducedFields: ReducedField[]
@@ -16,7 +17,7 @@ export type Props = {
1617
readonly value: string
1718
}
1819

19-
import type { Operator, Option as PayloadOption } from 'payload'
20+
import type { Operator, Option as PayloadOption, ResolvedFilterOptions } from 'payload'
2021

2122
import type { Option } from '../../ReactSelect/index.js'
2223

@@ -35,6 +36,7 @@ export const Condition: React.FC<Props> = (props) => {
3536
addCondition,
3637
andIndex,
3738
fieldName,
39+
filterOptions,
3840
operator,
3941
orIndex,
4042
reducedFields,
@@ -145,6 +147,7 @@ export const Condition: React.FC<Props> = (props) => {
145147
disabled={
146148
!operator || !reducedField || reducedField?.field?.admin?.disableListFilter
147149
}
150+
filterOptions={filterOptions}
148151
internalField={reducedField}
149152
onChange={setInternalValue}
150153
operator={operator}

0 commit comments

Comments
 (0)