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.
This commit is contained in:
@@ -136,21 +136,19 @@ Note: If `sortOptions` is not defined, the default sorting behavior of the Relat
|
|||||||
|
|
||||||
## Filtering relationship options
|
## Filtering relationship options
|
||||||
|
|
||||||
Options can be dynamically limited by supplying a [query constraint](/docs/queries/overview), which will be used both
|
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.
|
||||||
for validating input and filtering available relationships in the UI.
|
|
||||||
|
|
||||||
The `filterOptions` property can either be a `Where` query, or a function returning `true` to not filter, `false` to
|
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:
|
||||||
prevent all, or a `Where` query. When using a function, it will be
|
|
||||||
called with an argument object with the following properties:
|
|
||||||
|
|
||||||
| Property | Description |
|
| Property | Description |
|
||||||
| ------------- | ----------------------------------------------------------------------------------------------------- |
|
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `relationTo` | The collection `slug` to filter against, limited to this field's `relationTo` property |
|
| `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. |
|
||||||
| `data` | An object containing the full collection or global document currently being edited |
|
| `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. |
|
||||||
| `siblingData` | An object containing document data that is scoped to only fields within the same parent of this field |
|
| `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. |
|
||||||
| `id` | The `id` of the current document being edited. `id` is `undefined` during the `create` operation |
|
| `relationTo` | The collection `slug` to filter against, limited to this field's `relationTo` property. |
|
||||||
| `user` | An object containing the currently authenticated user |
|
|
||||||
| `req` | The Payload Request, which contains references to `payload`, `user`, `locale`, and more. |
|
| `req` | The Payload Request, which contains references to `payload`, `user`, `locale`, and more. |
|
||||||
|
| `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. |
|
||||||
|
| `user` | An object containing the currently authenticated user. |
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { isNumber } from 'payload/shared'
|
|||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
|
|
||||||
import { renderListViewSlots } from './renderListViewSlots.js'
|
import { renderListViewSlots } from './renderListViewSlots.js'
|
||||||
|
import { resolveAllFilterOptions } from './resolveAllFilterOptions.js'
|
||||||
|
|
||||||
export { generateListMetadata } from './meta.js'
|
export { generateListMetadata } from './meta.js'
|
||||||
|
|
||||||
@@ -149,6 +150,11 @@ export const renderListView = async (
|
|||||||
|
|
||||||
const renderedFilters = renderFilters(collectionConfig.fields, req.payload.importMap)
|
const renderedFilters = renderFilters(collectionConfig.fields, req.payload.importMap)
|
||||||
|
|
||||||
|
const resolvedFilterOptions = await resolveAllFilterOptions({
|
||||||
|
collectionConfig,
|
||||||
|
req,
|
||||||
|
})
|
||||||
|
|
||||||
const staticDescription =
|
const staticDescription =
|
||||||
typeof collectionConfig.admin.description === 'function'
|
typeof collectionConfig.admin.description === 'function'
|
||||||
? collectionConfig.admin.description({ t: i18n.t })
|
? collectionConfig.admin.description({ t: i18n.t })
|
||||||
@@ -192,6 +198,7 @@ export const renderListView = async (
|
|||||||
enableRowSelections,
|
enableRowSelections,
|
||||||
listPreferences,
|
listPreferences,
|
||||||
renderedFilters,
|
renderedFilters,
|
||||||
|
resolvedFilterOptions,
|
||||||
Table,
|
Table,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
37
packages/next/src/views/List/resolveAllFilterOptions.ts
Normal file
37
packages/next/src/views/List/resolveAllFilterOptions.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type { CollectionConfig, PayloadRequest, ResolvedFilterOptions } from 'payload'
|
||||||
|
|
||||||
|
import { resolveFilterOptions } from '@payloadcms/ui/rsc'
|
||||||
|
import { fieldIsHiddenOrDisabled } from 'payload/shared'
|
||||||
|
|
||||||
|
export const resolveAllFilterOptions = async ({
|
||||||
|
collectionConfig,
|
||||||
|
req,
|
||||||
|
}: {
|
||||||
|
collectionConfig: CollectionConfig
|
||||||
|
req: PayloadRequest
|
||||||
|
}): Promise<Map<string, ResolvedFilterOptions>> => {
|
||||||
|
const resolvedFilterOptions = new Map<string, ResolvedFilterOptions>()
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
collectionConfig.fields.map(async (field) => {
|
||||||
|
if (fieldIsHiddenOrDisabled(field)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('name' in field && 'filterOptions' in field && field.filterOptions) {
|
||||||
|
const options = await resolveFilterOptions(field.filterOptions, {
|
||||||
|
id: undefined,
|
||||||
|
blockData: undefined,
|
||||||
|
data: {}, // use empty object to prevent breaking queries when accessing properties of data
|
||||||
|
relationTo: field.relationTo,
|
||||||
|
req,
|
||||||
|
siblingData: {}, // use empty object to prevent breaking queries when accessing properties of data
|
||||||
|
user: req.user,
|
||||||
|
})
|
||||||
|
resolvedFilterOptions.set(field.name, options)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return resolvedFilterOptions
|
||||||
|
}
|
||||||
@@ -269,15 +269,15 @@ export type Condition<TData extends TypeWithID = any, TSiblingData = any> = (
|
|||||||
|
|
||||||
export type FilterOptionsProps<TData = any> = {
|
export type FilterOptionsProps<TData = any> = {
|
||||||
/**
|
/**
|
||||||
* The data of the nearest parent block. If the field is not within a block, `blockData` will be equal to `undefined`.
|
* 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.
|
||||||
*/
|
*/
|
||||||
blockData: TData
|
blockData: TData
|
||||||
/**
|
/**
|
||||||
* An object containing the full collection or global document currently being edited.
|
* 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.
|
||||||
*/
|
*/
|
||||||
data: TData
|
data: TData
|
||||||
/**
|
/**
|
||||||
* The `id` of the current document being edited. `id` is undefined during the `create` operation.
|
* 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.
|
||||||
*/
|
*/
|
||||||
id: number | string
|
id: number | string
|
||||||
/**
|
/**
|
||||||
@@ -286,7 +286,7 @@ export type FilterOptionsProps<TData = any> = {
|
|||||||
relationTo: CollectionSlug
|
relationTo: CollectionSlug
|
||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
/**
|
/**
|
||||||
* An object containing document data that is scoped to only fields within the same parent of this field.
|
* 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.
|
||||||
*/
|
*/
|
||||||
siblingData: unknown
|
siblingData: unknown
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -239,3 +239,5 @@ export type TransformGlobalWithSelect<
|
|||||||
: DataFromGlobalSlug<TSlug>
|
: DataFromGlobalSlug<TSlug>
|
||||||
|
|
||||||
export type PopulateType = Partial<TypedCollectionSelect>
|
export type PopulateType = Partial<TypedCollectionSelect>
|
||||||
|
|
||||||
|
export type ResolvedFilterOptions = { [collection: string]: Where }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { ClientCollectionConfig, Where } from 'payload'
|
import type { ClientCollectionConfig, ResolvedFilterOptions, Where } from 'payload'
|
||||||
|
|
||||||
import { useWindowInfo } from '@faceless-ui/window-info'
|
import { useWindowInfo } from '@faceless-ui/window-info'
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
@@ -20,8 +20,8 @@ import { SearchFilter } from '../SearchFilter/index.js'
|
|||||||
import { UnpublishMany } from '../UnpublishMany/index.js'
|
import { UnpublishMany } from '../UnpublishMany/index.js'
|
||||||
import { WhereBuilder } from '../WhereBuilder/index.js'
|
import { WhereBuilder } from '../WhereBuilder/index.js'
|
||||||
import validateWhereQuery from '../WhereBuilder/validateWhereQuery.js'
|
import validateWhereQuery from '../WhereBuilder/validateWhereQuery.js'
|
||||||
import './index.scss'
|
|
||||||
import { getTextFieldsToBeSearched } from './getTextFieldsToBeSearched.js'
|
import { getTextFieldsToBeSearched } from './getTextFieldsToBeSearched.js'
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
const baseClass = 'list-controls'
|
const baseClass = 'list-controls'
|
||||||
|
|
||||||
@@ -37,6 +37,7 @@ export type ListControlsProps = {
|
|||||||
readonly handleSortChange?: (sort: string) => void
|
readonly handleSortChange?: (sort: string) => void
|
||||||
readonly handleWhereChange?: (where: Where) => void
|
readonly handleWhereChange?: (where: Where) => void
|
||||||
readonly renderedFilters?: Map<string, React.ReactNode>
|
readonly renderedFilters?: Map<string, React.ReactNode>
|
||||||
|
readonly resolvedFilterOptions?: Map<string, ResolvedFilterOptions>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,6 +55,7 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
|
|||||||
enableColumns = true,
|
enableColumns = true,
|
||||||
enableSort = false,
|
enableSort = false,
|
||||||
renderedFilters,
|
renderedFilters,
|
||||||
|
resolvedFilterOptions,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const { handleSearchChange, query } = useListQuery()
|
const { handleSearchChange, query } = useListQuery()
|
||||||
@@ -214,6 +216,7 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
|
|||||||
collectionSlug={collectionConfig.slug}
|
collectionSlug={collectionConfig.slug}
|
||||||
fields={collectionConfig?.fields}
|
fields={collectionConfig?.fields}
|
||||||
renderedFilters={renderedFilters}
|
renderedFilters={renderedFilters}
|
||||||
|
resolvedFilterOptions={resolvedFilterOptions}
|
||||||
/>
|
/>
|
||||||
</AnimateHeight>
|
</AnimateHeight>
|
||||||
{enableSort && (
|
{enableSort && (
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import type { Operator, Option, SelectFieldClient, TextFieldClient } from 'payload'
|
import type {
|
||||||
|
Operator,
|
||||||
|
Option,
|
||||||
|
ResolvedFilterOptions,
|
||||||
|
SelectFieldClient,
|
||||||
|
TextFieldClient,
|
||||||
|
} from 'payload'
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
@@ -13,6 +19,7 @@ import { Text } from '../Text/index.js'
|
|||||||
type Props = {
|
type Props = {
|
||||||
booleanSelect: boolean
|
booleanSelect: boolean
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
|
filterOptions: ResolvedFilterOptions
|
||||||
internalField: ReducedField
|
internalField: ReducedField
|
||||||
onChange: React.Dispatch<React.SetStateAction<string>>
|
onChange: React.Dispatch<React.SetStateAction<string>>
|
||||||
operator: Operator
|
operator: Operator
|
||||||
@@ -23,6 +30,7 @@ type Props = {
|
|||||||
export const DefaultFilter: React.FC<Props> = ({
|
export const DefaultFilter: React.FC<Props> = ({
|
||||||
booleanSelect,
|
booleanSelect,
|
||||||
disabled,
|
disabled,
|
||||||
|
filterOptions,
|
||||||
internalField,
|
internalField,
|
||||||
onChange,
|
onChange,
|
||||||
operator,
|
operator,
|
||||||
@@ -73,6 +81,7 @@ export const DefaultFilter: React.FC<Props> = ({
|
|||||||
<RelationshipFilter
|
<RelationshipFilter
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
field={internalField.field}
|
field={internalField.field}
|
||||||
|
filterOptions={filterOptions}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
operator={operator}
|
operator={operator}
|
||||||
value={value}
|
value={value}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export const RelationshipFilter: React.FC<Props> = (props) => {
|
|||||||
const {
|
const {
|
||||||
disabled,
|
disabled,
|
||||||
field: { admin: { isSortable } = {}, hasMany, relationTo },
|
field: { admin: { isSortable } = {}, hasMany, relationTo },
|
||||||
|
filterOptions,
|
||||||
onChange,
|
onChange,
|
||||||
value,
|
value,
|
||||||
} = props
|
} = props
|
||||||
@@ -104,6 +105,10 @@ export const RelationshipFilter: React.FC<Props> = (props) => {
|
|||||||
where,
|
where,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filterOptions && filterOptions?.[relationSlug]) {
|
||||||
|
query.where.and.push(filterOptions[relationSlug])
|
||||||
|
}
|
||||||
|
|
||||||
if (debouncedSearch) {
|
if (debouncedSearch) {
|
||||||
query.where.and.push({
|
query.where.and.push({
|
||||||
[fieldToSearch]: {
|
[fieldToSearch]: {
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import type { I18nClient } from '@payloadcms/translations'
|
import type { I18nClient } from '@payloadcms/translations'
|
||||||
import type { ClientCollectionConfig, PaginatedDocs, RelationshipFieldClient } from 'payload'
|
import type {
|
||||||
|
ClientCollectionConfig,
|
||||||
|
PaginatedDocs,
|
||||||
|
RelationshipFieldClient,
|
||||||
|
ResolvedFilterOptions,
|
||||||
|
} from 'payload'
|
||||||
|
|
||||||
import type { DefaultFilterProps } from '../types.js'
|
import type { DefaultFilterProps } from '../types.js'
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
readonly field: RelationshipFieldClient
|
readonly field: RelationshipFieldClient
|
||||||
|
readonly filterOptions: ResolvedFilterOptions
|
||||||
} & DefaultFilterProps
|
} & DefaultFilterProps
|
||||||
|
|
||||||
export type Option = {
|
export type Option = {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export type Props = {
|
|||||||
readonly addCondition: AddCondition
|
readonly addCondition: AddCondition
|
||||||
readonly andIndex: number
|
readonly andIndex: number
|
||||||
readonly fieldName: string
|
readonly fieldName: string
|
||||||
|
readonly filterOptions: ResolvedFilterOptions
|
||||||
readonly operator: Operator
|
readonly operator: Operator
|
||||||
readonly orIndex: number
|
readonly orIndex: number
|
||||||
readonly reducedFields: ReducedField[]
|
readonly reducedFields: ReducedField[]
|
||||||
@@ -16,7 +17,7 @@ export type Props = {
|
|||||||
readonly value: string
|
readonly value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
import type { Operator, Option as PayloadOption } from 'payload'
|
import type { Operator, Option as PayloadOption, ResolvedFilterOptions } from 'payload'
|
||||||
|
|
||||||
import type { Option } from '../../ReactSelect/index.js'
|
import type { Option } from '../../ReactSelect/index.js'
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ export const Condition: React.FC<Props> = (props) => {
|
|||||||
addCondition,
|
addCondition,
|
||||||
andIndex,
|
andIndex,
|
||||||
fieldName,
|
fieldName,
|
||||||
|
filterOptions,
|
||||||
operator,
|
operator,
|
||||||
orIndex,
|
orIndex,
|
||||||
reducedFields,
|
reducedFields,
|
||||||
@@ -145,6 +147,7 @@ export const Condition: React.FC<Props> = (props) => {
|
|||||||
disabled={
|
disabled={
|
||||||
!operator || !reducedField || reducedField?.field?.admin?.disableListFilter
|
!operator || !reducedField || reducedField?.field?.admin?.disableListFilter
|
||||||
}
|
}
|
||||||
|
filterOptions={filterOptions}
|
||||||
internalField={reducedField}
|
internalField={reducedField}
|
||||||
onChange={setInternalValue}
|
onChange={setInternalValue}
|
||||||
operator={operator}
|
operator={operator}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export { WhereBuilderProps }
|
|||||||
* It is part of the {@link ListControls} component which is used to render the controls (search, filter, where).
|
* It is part of the {@link ListControls} component which is used to render the controls (search, filter, where).
|
||||||
*/
|
*/
|
||||||
export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
|
export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
|
||||||
const { collectionPluralLabel, fields, renderedFilters } = props
|
const { collectionPluralLabel, fields, renderedFilters, resolvedFilterOptions } = props
|
||||||
const { i18n, t } = useTranslation()
|
const { i18n, t } = useTranslation()
|
||||||
|
|
||||||
const reducedFields = useMemo(() => reduceFields({ fields, i18n }), [fields, i18n])
|
const reducedFields = useMemo(() => reduceFields({ fields, i18n }), [fields, i18n])
|
||||||
@@ -166,6 +166,7 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
|
|||||||
addCondition={addCondition}
|
addCondition={addCondition}
|
||||||
andIndex={andIndex}
|
andIndex={andIndex}
|
||||||
fieldName={fieldName}
|
fieldName={fieldName}
|
||||||
|
filterOptions={resolvedFilterOptions?.get(fieldName)}
|
||||||
operator={operator}
|
operator={operator}
|
||||||
orIndex={orIndex}
|
orIndex={orIndex}
|
||||||
reducedFields={reducedFields}
|
reducedFields={reducedFields}
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import type { ClientField, Operator, SanitizedCollectionConfig, Where } from 'payload'
|
import type {
|
||||||
|
ClientField,
|
||||||
|
Operator,
|
||||||
|
ResolvedFilterOptions,
|
||||||
|
SanitizedCollectionConfig,
|
||||||
|
Where,
|
||||||
|
} from 'payload'
|
||||||
|
|
||||||
export type WhereBuilderProps = {
|
export type WhereBuilderProps = {
|
||||||
readonly collectionPluralLabel: SanitizedCollectionConfig['labels']['plural']
|
readonly collectionPluralLabel: SanitizedCollectionConfig['labels']['plural']
|
||||||
readonly collectionSlug: SanitizedCollectionConfig['slug']
|
readonly collectionSlug: SanitizedCollectionConfig['slug']
|
||||||
readonly fields?: ClientField[]
|
readonly fields?: ClientField[]
|
||||||
readonly renderedFilters?: Map<string, React.ReactNode>
|
readonly renderedFilters?: Map<string, React.ReactNode>
|
||||||
|
readonly resolvedFilterOptions?: Map<string, ResolvedFilterOptions>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ReducedField = {
|
export type ReducedField = {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export { copyDataFromLocaleHandler } from '../../utilities/copyDataFromLocale.js'
|
export { copyDataFromLocaleHandler } from '../../utilities/copyDataFromLocale.js'
|
||||||
export { renderFilters, renderTable } from '../../utilities/renderTable.js'
|
export { renderFilters, renderTable } from '../../utilities/renderTable.js'
|
||||||
|
export { resolveFilterOptions } from '../../utilities/resolveFilterOptions.js'
|
||||||
export { upsertPreferences } from '../../utilities/upsertPreferences.js'
|
export { upsertPreferences } from '../../utilities/upsertPreferences.js'
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
|
|
||||||
import type { RenderFieldMethod } from './types.js'
|
import type { RenderFieldMethod } from './types.js'
|
||||||
|
|
||||||
import { getFilterOptionsQuery } from './getFilterOptionsQuery.js'
|
import { resolveFilterOptions } from '../../utilities/resolveFilterOptions.js'
|
||||||
import { iterateFields } from './iterateFields.js'
|
import { iterateFields } from './iterateFields.js'
|
||||||
|
|
||||||
const ObjectId = (ObjectIdImport.default ||
|
const ObjectId = (ObjectIdImport.default ||
|
||||||
@@ -578,7 +578,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof field.filterOptions === 'function') {
|
if (typeof field.filterOptions === 'function') {
|
||||||
const query = await getFilterOptionsQuery(field.filterOptions, {
|
const query = await resolveFilterOptions(field.filterOptions, {
|
||||||
id,
|
id,
|
||||||
blockData,
|
blockData,
|
||||||
data: fullData,
|
data: fullData,
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ import type {
|
|||||||
ClientCollectionConfig,
|
ClientCollectionConfig,
|
||||||
CollectionConfig,
|
CollectionConfig,
|
||||||
Field,
|
Field,
|
||||||
|
FilterOptionsProps,
|
||||||
ImportMap,
|
ImportMap,
|
||||||
ListPreferences,
|
ListPreferences,
|
||||||
PaginatedDocs,
|
PaginatedDocs,
|
||||||
Payload,
|
Payload,
|
||||||
SanitizedCollectionConfig,
|
SanitizedCollectionConfig,
|
||||||
|
Where,
|
||||||
} from 'payload'
|
} from 'payload'
|
||||||
|
import type { MarkOptional } from 'ts-essentials'
|
||||||
|
|
||||||
import { getTranslation, type I18nClient } from '@payloadcms/translations'
|
import { getTranslation, type I18nClient } from '@payloadcms/translations'
|
||||||
import { fieldIsHiddenOrDisabled, flattenTopLevelFields } from 'payload/shared'
|
import { fieldIsHiddenOrDisabled, flattenTopLevelFields } from 'payload/shared'
|
||||||
@@ -19,6 +22,7 @@ import { RenderServerComponent } from '../elements/RenderServerComponent/index.j
|
|||||||
import { buildColumnState } from '../elements/TableColumns/buildColumnState.js'
|
import { buildColumnState } from '../elements/TableColumns/buildColumnState.js'
|
||||||
import { filterFields } from '../elements/TableColumns/filterFields.js'
|
import { filterFields } from '../elements/TableColumns/filterFields.js'
|
||||||
import { getInitialColumns } from '../elements/TableColumns/getInitialColumns.js'
|
import { getInitialColumns } from '../elements/TableColumns/getInitialColumns.js'
|
||||||
|
|
||||||
// eslint-disable-next-line payload/no-imports-from-exports-dir
|
// eslint-disable-next-line payload/no-imports-from-exports-dir
|
||||||
import { Pill, SelectAll, SelectRow, Table } from '../exports/client/index.js'
|
import { Pill, SelectAll, SelectRow, Table } from '../exports/client/index.js'
|
||||||
|
|
||||||
@@ -47,6 +51,50 @@ export const renderFilters = (
|
|||||||
new Map() as Map<string, React.ReactNode>,
|
new Map() as Map<string, React.ReactNode>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// export const resolveFilterOptions = async ({
|
||||||
|
// fields,
|
||||||
|
// relationTo,
|
||||||
|
// req,
|
||||||
|
// user,
|
||||||
|
// }: { fields: Field[] } & MarkOptional<
|
||||||
|
// FilterOptionsProps,
|
||||||
|
// 'blockData' | 'data' | 'id' | 'siblingData'
|
||||||
|
// >): Promise<Map<string, Where>> => {
|
||||||
|
// const acc = new Map<string, Where>()
|
||||||
|
|
||||||
|
// for (const field of fields) {
|
||||||
|
// if (fieldIsHiddenOrDisabled(field)) {
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if ('name' in field && 'filterOptions' in field && field.filterOptions) {
|
||||||
|
// let resolvedFilterOption = {} as Where
|
||||||
|
|
||||||
|
// if (typeof field.filterOptions === 'function') {
|
||||||
|
// const result = await field.filterOptions({
|
||||||
|
// id: undefined,
|
||||||
|
// blockData: undefined,
|
||||||
|
// data: {}, // use empty object to prevent breaking queries when accessing properties of data
|
||||||
|
// relationTo,
|
||||||
|
// req,
|
||||||
|
// siblingData: {}, // use empty object to prevent breaking queries when accessing properties of siblingData
|
||||||
|
// user,
|
||||||
|
// })
|
||||||
|
|
||||||
|
// if (result && typeof result === 'object') {
|
||||||
|
// resolvedFilterOption = result
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// resolvedFilterOption = field.filterOptions
|
||||||
|
// }
|
||||||
|
|
||||||
|
// acc.set(field.name, resolvedFilterOption)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return acc
|
||||||
|
// }
|
||||||
|
|
||||||
export const renderTable = ({
|
export const renderTable = ({
|
||||||
clientCollectionConfig,
|
clientCollectionConfig,
|
||||||
collectionConfig,
|
collectionConfig,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { FilterOptions, FilterOptionsProps, Where } from 'payload'
|
import type { FilterOptions, FilterOptionsProps, ResolvedFilterOptions } from 'payload'
|
||||||
|
|
||||||
export const getFilterOptionsQuery = async (
|
export const resolveFilterOptions = async (
|
||||||
filterOptions: FilterOptions,
|
filterOptions: FilterOptions,
|
||||||
options: { relationTo: string | string[] } & Omit<FilterOptionsProps, 'relationTo'>,
|
options: { relationTo: string | string[] } & Omit<FilterOptionsProps, 'relationTo'>,
|
||||||
): Promise<{ [collection: string]: Where }> => {
|
): Promise<ResolvedFilterOptions> => {
|
||||||
const { relationTo } = options
|
const { relationTo } = options
|
||||||
|
|
||||||
const relations = Array.isArray(relationTo) ? relationTo : [relationTo]
|
const relations = Array.isArray(relationTo) ? relationTo : [relationTo]
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import type { ListPreferences } from 'payload'
|
import type { ListPreferences, ResolvedFilterOptions } from 'payload'
|
||||||
|
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
import LinkImport from 'next/link.js'
|
import LinkImport from 'next/link.js'
|
||||||
@@ -63,6 +63,7 @@ export type ListViewClientProps = {
|
|||||||
newDocumentURL: string
|
newDocumentURL: string
|
||||||
preferenceKey?: string
|
preferenceKey?: string
|
||||||
renderedFilters?: Map<string, React.ReactNode>
|
renderedFilters?: Map<string, React.ReactNode>
|
||||||
|
resolvedFilterOptions?: Map<string, ResolvedFilterOptions>
|
||||||
} & ListViewSlots
|
} & ListViewSlots
|
||||||
|
|
||||||
export const DefaultListView: React.FC<ListViewClientProps> = (props) => {
|
export const DefaultListView: React.FC<ListViewClientProps> = (props) => {
|
||||||
@@ -83,6 +84,7 @@ export const DefaultListView: React.FC<ListViewClientProps> = (props) => {
|
|||||||
newDocumentURL,
|
newDocumentURL,
|
||||||
preferenceKey,
|
preferenceKey,
|
||||||
renderedFilters,
|
renderedFilters,
|
||||||
|
resolvedFilterOptions,
|
||||||
Table: InitialTable,
|
Table: InitialTable,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
@@ -219,6 +221,7 @@ export const DefaultListView: React.FC<ListViewClientProps> = (props) => {
|
|||||||
disableBulkDelete={disableBulkDelete}
|
disableBulkDelete={disableBulkDelete}
|
||||||
disableBulkEdit={disableBulkEdit}
|
disableBulkEdit={disableBulkEdit}
|
||||||
renderedFilters={renderedFilters}
|
renderedFilters={renderedFilters}
|
||||||
|
resolvedFilterOptions={resolvedFilterOptions}
|
||||||
/>
|
/>
|
||||||
{BeforeListTable}
|
{BeforeListTable}
|
||||||
{docs.length > 0 && <RelationshipProvider>{Table}</RelationshipProvider>}
|
{docs.length > 0 && <RelationshipProvider>{Table}</RelationshipProvider>}
|
||||||
|
|||||||
@@ -302,8 +302,9 @@ describe('List View', () => {
|
|||||||
|
|
||||||
await page.waitForURL(new RegExp(encodedQueryString))
|
await page.waitForURL(new RegExp(encodedQueryString))
|
||||||
|
|
||||||
const whereBuilder = page.locator('.list-controls__where.rah-static.rah-static--height-auto')
|
await expect(
|
||||||
await expect(whereBuilder).toBeVisible()
|
page.locator('.list-controls__where.rah-static.rah-static--height-auto'),
|
||||||
|
).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should respect base list filters', async () => {
|
test('should respect base list filters', async () => {
|
||||||
@@ -356,30 +357,31 @@ describe('List View', () => {
|
|||||||
test('should reset filter value when a different field is selected', async () => {
|
test('should reset filter value when a different field is selected', async () => {
|
||||||
const id = (await page.locator('.cell-id').first().innerText()).replace('ID: ', '')
|
const id = (await page.locator('.cell-id').first().innerText()).replace('ID: ', '')
|
||||||
|
|
||||||
await addListFilter({
|
const whereBuilder = await addListFilter({
|
||||||
page,
|
page,
|
||||||
fieldLabel: 'ID',
|
fieldLabel: 'ID',
|
||||||
operatorLabel: 'equals',
|
operatorLabel: 'equals',
|
||||||
value: id,
|
value: id,
|
||||||
})
|
})
|
||||||
|
|
||||||
const filterField = page.locator('.condition__field')
|
const filterField = whereBuilder.locator('.condition__field')
|
||||||
await filterField.click()
|
await filterField.click()
|
||||||
|
|
||||||
// select new filter field of Number
|
// select new filter field of Number
|
||||||
const dropdownFieldOption = filterField.locator('.rs__option', {
|
const dropdownFieldOption = filterField.locator('.rs__option', {
|
||||||
hasText: exactText('Status'),
|
hasText: exactText('Status'),
|
||||||
})
|
})
|
||||||
|
|
||||||
await dropdownFieldOption.click()
|
await dropdownFieldOption.click()
|
||||||
await expect(filterField).toContainText('Status')
|
await expect(filterField).toContainText('Status')
|
||||||
|
|
||||||
await expect(page.locator('.condition__value input')).toHaveValue('')
|
await expect(whereBuilder.locator('.condition__value input')).toHaveValue('')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should remove condition from URL when value is cleared', async () => {
|
test('should remove condition from URL when value is cleared', async () => {
|
||||||
await page.goto(postsUrl.list)
|
await page.goto(postsUrl.list)
|
||||||
|
|
||||||
await addListFilter({
|
const whereBuilder = await addListFilter({
|
||||||
page,
|
page,
|
||||||
fieldLabel: 'Relationship',
|
fieldLabel: 'Relationship',
|
||||||
operatorLabel: 'equals',
|
operatorLabel: 'equals',
|
||||||
@@ -391,7 +393,7 @@ describe('List View', () => {
|
|||||||
|
|
||||||
await page.waitForURL(new RegExp(encodedQueryString + '[^&]*'))
|
await page.waitForURL(new RegExp(encodedQueryString + '[^&]*'))
|
||||||
|
|
||||||
await page.locator('.condition__value .clear-indicator').click()
|
await whereBuilder.locator('.condition__value .clear-indicator').click()
|
||||||
|
|
||||||
await page.waitForURL(new RegExp(encodedQueryString))
|
await page.waitForURL(new RegExp(encodedQueryString))
|
||||||
})
|
})
|
||||||
@@ -403,15 +405,13 @@ describe('List View', () => {
|
|||||||
test('should refresh relationship values when a different field is selected', async () => {
|
test('should refresh relationship values when a different field is selected', async () => {
|
||||||
await page.goto(postsUrl.list)
|
await page.goto(postsUrl.list)
|
||||||
|
|
||||||
await addListFilter({
|
const whereBuilder = await addListFilter({
|
||||||
page,
|
page,
|
||||||
fieldLabel: 'Relationship',
|
fieldLabel: 'Relationship',
|
||||||
operatorLabel: 'equals',
|
operatorLabel: 'equals',
|
||||||
value: 'post1',
|
value: 'post1',
|
||||||
})
|
})
|
||||||
|
|
||||||
const whereBuilder = page.locator('.where-builder')
|
|
||||||
|
|
||||||
const conditionField = whereBuilder.locator('.condition__field')
|
const conditionField = whereBuilder.locator('.condition__field')
|
||||||
await conditionField.click()
|
await conditionField.click()
|
||||||
|
|
||||||
@@ -565,15 +565,15 @@ describe('List View', () => {
|
|||||||
test('should reset filter values for every additional filter', async () => {
|
test('should reset filter values for every additional filter', async () => {
|
||||||
await page.goto(postsUrl.list)
|
await page.goto(postsUrl.list)
|
||||||
|
|
||||||
await addListFilter({
|
const whereBuilder = await addListFilter({
|
||||||
page,
|
page,
|
||||||
fieldLabel: 'Tab 1 > Title',
|
fieldLabel: 'Tab 1 > Title',
|
||||||
operatorLabel: 'equals',
|
operatorLabel: 'equals',
|
||||||
value: 'Test',
|
value: 'Test',
|
||||||
})
|
})
|
||||||
|
|
||||||
await page.locator('.condition__actions-add').click()
|
await whereBuilder.locator('.condition__actions-add').click()
|
||||||
const secondLi = page.locator('.where-builder__and-filters li:nth-child(2)')
|
const secondLi = whereBuilder.locator('.where-builder__and-filters li:nth-child(2)')
|
||||||
await expect(secondLi).toBeVisible()
|
await expect(secondLi).toBeVisible()
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -587,14 +587,13 @@ describe('List View', () => {
|
|||||||
test('should not re-render page upon typing in a value in the filter value field', async () => {
|
test('should not re-render page upon typing in a value in the filter value field', async () => {
|
||||||
await page.goto(postsUrl.list)
|
await page.goto(postsUrl.list)
|
||||||
|
|
||||||
await addListFilter({
|
const whereBuilder = await addListFilter({
|
||||||
page,
|
page,
|
||||||
fieldLabel: 'Tab 1 > Title',
|
fieldLabel: 'Tab 1 > Title',
|
||||||
operatorLabel: 'equals',
|
operatorLabel: 'equals',
|
||||||
skipValueInput: true,
|
skipValueInput: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const whereBuilder = page.locator('.where-builder')
|
|
||||||
const valueInput = whereBuilder.locator('.condition__value >> input')
|
const valueInput = whereBuilder.locator('.condition__value >> input')
|
||||||
|
|
||||||
// Type into the input field instead of filling it
|
// Type into the input field instead of filling it
|
||||||
@@ -611,7 +610,7 @@ describe('List View', () => {
|
|||||||
test('should still show second filter if two filters exist and first filter is removed', async () => {
|
test('should still show second filter if two filters exist and first filter is removed', async () => {
|
||||||
await page.goto(postsUrl.list)
|
await page.goto(postsUrl.list)
|
||||||
|
|
||||||
await addListFilter({
|
const whereBuilder = await addListFilter({
|
||||||
page,
|
page,
|
||||||
fieldLabel: 'Tab 1 > Title',
|
fieldLabel: 'Tab 1 > Title',
|
||||||
operatorLabel: 'equals',
|
operatorLabel: 'equals',
|
||||||
@@ -620,9 +619,9 @@ describe('List View', () => {
|
|||||||
|
|
||||||
await wait(500)
|
await wait(500)
|
||||||
|
|
||||||
await page.locator('.condition__actions-add').click()
|
await whereBuilder.locator('.condition__actions-add').click()
|
||||||
|
|
||||||
const secondLi = page.locator('.where-builder__and-filters li:nth-child(2)')
|
const secondLi = whereBuilder.locator('.where-builder__and-filters li:nth-child(2)')
|
||||||
await expect(secondLi).toBeVisible()
|
await expect(secondLi).toBeVisible()
|
||||||
const secondConditionField = secondLi.locator('.condition__field')
|
const secondConditionField = secondLi.locator('.condition__field')
|
||||||
const secondOperatorField = secondLi.locator('.condition__operator')
|
const secondOperatorField = secondLi.locator('.condition__operator')
|
||||||
@@ -705,15 +704,13 @@ describe('List View', () => {
|
|||||||
test('should properly paginate many documents', async () => {
|
test('should properly paginate many documents', async () => {
|
||||||
await page.goto(with300DocumentsUrl.list)
|
await page.goto(with300DocumentsUrl.list)
|
||||||
|
|
||||||
await addListFilter({
|
const whereBuilder = await addListFilter({
|
||||||
page,
|
page,
|
||||||
fieldLabel: 'Self Relation',
|
fieldLabel: 'Self Relation',
|
||||||
operatorLabel: 'equals',
|
operatorLabel: 'equals',
|
||||||
skipValueInput: true,
|
skipValueInput: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const whereBuilder = page.locator('.where-builder')
|
|
||||||
|
|
||||||
const valueField = whereBuilder.locator('.condition__value')
|
const valueField = whereBuilder.locator('.condition__value')
|
||||||
await valueField.click()
|
await valueField.click()
|
||||||
await page.keyboard.type('4')
|
await page.keyboard.type('4')
|
||||||
|
|||||||
@@ -72,6 +72,22 @@ export const Relationship: CollectionConfig = {
|
|||||||
'This will filter the relationship options based on id, which is the same as the relationship field in this document',
|
'This will filter the relationship options based on id, which is the same as the relationship field in this document',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'relationshipFilteredByField',
|
||||||
|
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',
|
name: 'relationshipFilteredAsync',
|
||||||
filterOptions: (args: FilterOptionsProps<FieldsRelationship>) => {
|
filterOptions: (args: FilterOptionsProps<FieldsRelationship>) => {
|
||||||
|
|||||||
@@ -316,6 +316,41 @@ describe('Relationship Field', () => {
|
|||||||
await runFilterOptionsTest('relationshipFilteredAsync', 'Relationship Filtered Async')
|
await runFilterOptionsTest('relationshipFilteredAsync', 'Relationship Filtered Async')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('should apply filter options within 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-relationshipFilteredByField')
|
||||||
|
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: '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 () => {
|
test('should allow usage of relationTo in filterOptions', async () => {
|
||||||
const { id: include } = (await payload.create({
|
const { id: include } = (await payload.create({
|
||||||
collection: relationOneSlug,
|
collection: relationOneSlug,
|
||||||
|
|||||||
@@ -6,6 +6,60 @@
|
|||||||
* and re-run `payload generate:types` to regenerate this file.
|
* and re-run `payload generate:types` to regenerate this file.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supported timezones in IANA format.
|
||||||
|
*
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "supportedTimezones".
|
||||||
|
*/
|
||||||
|
export type SupportedTimezones =
|
||||||
|
| 'Pacific/Midway'
|
||||||
|
| 'Pacific/Niue'
|
||||||
|
| 'Pacific/Honolulu'
|
||||||
|
| 'Pacific/Rarotonga'
|
||||||
|
| 'America/Anchorage'
|
||||||
|
| 'Pacific/Gambier'
|
||||||
|
| 'America/Los_Angeles'
|
||||||
|
| 'America/Tijuana'
|
||||||
|
| 'America/Denver'
|
||||||
|
| 'America/Phoenix'
|
||||||
|
| 'America/Chicago'
|
||||||
|
| 'America/Guatemala'
|
||||||
|
| 'America/New_York'
|
||||||
|
| 'America/Bogota'
|
||||||
|
| 'America/Caracas'
|
||||||
|
| 'America/Santiago'
|
||||||
|
| 'America/Buenos_Aires'
|
||||||
|
| 'America/Sao_Paulo'
|
||||||
|
| 'Atlantic/South_Georgia'
|
||||||
|
| 'Atlantic/Azores'
|
||||||
|
| 'Atlantic/Cape_Verde'
|
||||||
|
| 'Europe/London'
|
||||||
|
| 'Europe/Berlin'
|
||||||
|
| 'Africa/Lagos'
|
||||||
|
| 'Europe/Athens'
|
||||||
|
| 'Africa/Cairo'
|
||||||
|
| 'Europe/Moscow'
|
||||||
|
| 'Asia/Riyadh'
|
||||||
|
| 'Asia/Dubai'
|
||||||
|
| 'Asia/Baku'
|
||||||
|
| 'Asia/Karachi'
|
||||||
|
| 'Asia/Tashkent'
|
||||||
|
| 'Asia/Calcutta'
|
||||||
|
| 'Asia/Dhaka'
|
||||||
|
| 'Asia/Almaty'
|
||||||
|
| 'Asia/Jakarta'
|
||||||
|
| 'Asia/Bangkok'
|
||||||
|
| 'Asia/Shanghai'
|
||||||
|
| 'Asia/Singapore'
|
||||||
|
| 'Asia/Tokyo'
|
||||||
|
| 'Asia/Seoul'
|
||||||
|
| 'Australia/Sydney'
|
||||||
|
| 'Pacific/Guam'
|
||||||
|
| 'Pacific/Noumea'
|
||||||
|
| 'Pacific/Auckland'
|
||||||
|
| 'Pacific/Fiji';
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
auth: {
|
auth: {
|
||||||
users: UserAuthOperations;
|
users: UserAuthOperations;
|
||||||
@@ -118,6 +172,10 @@ export interface FieldsRelationship {
|
|||||||
* This will filter the relationship options based on id, which is the same as the relationship field in this document
|
* This will filter the relationship options based on id, which is the same as the relationship field in this document
|
||||||
*/
|
*/
|
||||||
relationshipFilteredByID?: (string | null) | RelationOne;
|
relationshipFilteredByID?: (string | null) | RelationOne;
|
||||||
|
/**
|
||||||
|
* This will filter the relationship options if the filter field in this document is set to "Include me"
|
||||||
|
*/
|
||||||
|
relationshipFilteredByField?: (string | null) | FieldsRelationship;
|
||||||
relationshipFilteredAsync?: (string | null) | RelationOne;
|
relationshipFilteredAsync?: (string | null) | RelationOne;
|
||||||
relationshipManyFiltered?:
|
relationshipManyFiltered?:
|
||||||
| (
|
| (
|
||||||
@@ -446,6 +504,7 @@ export interface FieldsRelationshipSelect<T extends boolean = true> {
|
|||||||
relationshipRestricted?: T;
|
relationshipRestricted?: T;
|
||||||
relationshipWithTitle?: T;
|
relationshipWithTitle?: T;
|
||||||
relationshipFilteredByID?: T;
|
relationshipFilteredByID?: T;
|
||||||
|
relationshipFilteredByField?: T;
|
||||||
relationshipFilteredAsync?: T;
|
relationshipFilteredAsync?: T;
|
||||||
relationshipManyFiltered?: T;
|
relationshipManyFiltered?: T;
|
||||||
filter?: T;
|
filter?: T;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Page } from '@playwright/test'
|
import type { Locator, Page } from '@playwright/test'
|
||||||
|
|
||||||
import { expect } from '@playwright/test'
|
import { expect } from '@playwright/test'
|
||||||
import { exactText } from 'helpers.js'
|
import { exactText } from 'helpers.js'
|
||||||
@@ -18,7 +18,7 @@ export const addListFilter = async ({
|
|||||||
replaceExisting?: boolean
|
replaceExisting?: boolean
|
||||||
skipValueInput?: boolean
|
skipValueInput?: boolean
|
||||||
value?: string
|
value?: string
|
||||||
}) => {
|
}): Promise<Locator> => {
|
||||||
await openListFilters(page, {})
|
await openListFilters(page, {})
|
||||||
|
|
||||||
const whereBuilder = page.locator('.where-builder')
|
const whereBuilder = page.locator('.where-builder')
|
||||||
@@ -52,4 +52,6 @@ export const addListFilter = async ({
|
|||||||
await valueOptions.locator(`text=${value}`).click()
|
await valueOptions.locator(`text=${value}`).click()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return whereBuilder
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user