fix(next): resolve filterOptions by path (#13779)
Follow up to #11375. When setting `filterOptions` on relationship or upload fields _that are nested within a named field_, those options won't be applied to the `Filter` component in the list view. This is because of how we key the results when resolving `filterOptions` on the server. Instead of using the field path as expected, we were using the field name, causing a failed lookup on the front-end. This also solves an issue where two fields with the same name would override each other's `filterOptions`, since field names alone are not unique. Unrelated: this PR also does some general housekeeping to e2e test helpers. --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1211332845301583
This commit is contained in:
@@ -1,14 +1,21 @@
|
||||
import type { Field, PayloadRequest, ResolvedFilterOptions } from 'payload'
|
||||
|
||||
import { resolveFilterOptions } from '@payloadcms/ui/rsc'
|
||||
import { fieldHasSubFields, fieldIsHiddenOrDisabled } from 'payload/shared'
|
||||
import {
|
||||
fieldAffectsData,
|
||||
fieldHasSubFields,
|
||||
fieldIsHiddenOrDisabled,
|
||||
tabHasName,
|
||||
} from 'payload/shared'
|
||||
|
||||
export const resolveAllFilterOptions = async ({
|
||||
fields,
|
||||
pathPrefix,
|
||||
req,
|
||||
result,
|
||||
}: {
|
||||
fields: Field[]
|
||||
pathPrefix?: string
|
||||
req: PayloadRequest
|
||||
result?: Map<string, ResolvedFilterOptions>
|
||||
}): Promise<Map<string, ResolvedFilterOptions>> => {
|
||||
@@ -20,6 +27,12 @@ export const resolveAllFilterOptions = async ({
|
||||
return
|
||||
}
|
||||
|
||||
const fieldPath = fieldAffectsData(field)
|
||||
? pathPrefix
|
||||
? `${pathPrefix}.${field.name}`
|
||||
: field.name
|
||||
: pathPrefix
|
||||
|
||||
if (
|
||||
(field.type === 'relationship' || field.type === 'upload') &&
|
||||
'filterOptions' in field &&
|
||||
@@ -28,19 +41,20 @@ export const resolveAllFilterOptions = async ({
|
||||
const options = await resolveFilterOptions(field.filterOptions, {
|
||||
id: undefined,
|
||||
blockData: undefined,
|
||||
data: {}, // use empty object to prevent breaking queries when accessing properties of data
|
||||
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
|
||||
siblingData: {}, // use empty object to prevent breaking queries when accessing properties of `siblingData`
|
||||
user: req.user,
|
||||
})
|
||||
|
||||
resolvedFilterOptions.set(field.name, options)
|
||||
resolvedFilterOptions.set(fieldPath, options)
|
||||
}
|
||||
|
||||
if (fieldHasSubFields(field)) {
|
||||
await resolveAllFilterOptions({
|
||||
fields: field.fields,
|
||||
pathPrefix: fieldPath,
|
||||
req,
|
||||
result: resolvedFilterOptions,
|
||||
})
|
||||
@@ -48,13 +62,20 @@ export const resolveAllFilterOptions = async ({
|
||||
|
||||
if (field.type === 'tabs') {
|
||||
await Promise.all(
|
||||
field.tabs.map((tab) =>
|
||||
resolveAllFilterOptions({
|
||||
field.tabs.map(async (tab) => {
|
||||
const tabPath = tabHasName(tab)
|
||||
? fieldPath
|
||||
? `${fieldPath}.${tab.name}`
|
||||
: tab.name
|
||||
: fieldPath
|
||||
|
||||
await resolveAllFilterOptions({
|
||||
fields: tab.fields,
|
||||
pathPrefix: tabPath,
|
||||
req,
|
||||
result: resolvedFilterOptions,
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
export type Props = {
|
||||
readonly addCondition: AddCondition
|
||||
readonly andIndex: number
|
||||
readonly fieldName: string
|
||||
readonly fieldPath: string
|
||||
readonly filterOptions: ResolvedFilterOptions
|
||||
readonly operator: Operator
|
||||
readonly orIndex: number
|
||||
@@ -42,7 +42,7 @@ export const Condition: React.FC<Props> = (props) => {
|
||||
const {
|
||||
addCondition,
|
||||
andIndex,
|
||||
fieldName,
|
||||
fieldPath,
|
||||
filterOptions,
|
||||
operator,
|
||||
orIndex,
|
||||
@@ -55,7 +55,7 @@ export const Condition: React.FC<Props> = (props) => {
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const reducedField = reducedFields.find((field) => field.value === fieldName)
|
||||
const reducedField = reducedFields.find((field) => field.value === fieldPath)
|
||||
|
||||
const [internalValue, setInternalValue] = useState<Value>(value)
|
||||
|
||||
|
||||
@@ -143,12 +143,12 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
|
||||
{Array.isArray(or?.and) &&
|
||||
or.and.map((_, andIndex) => {
|
||||
const condition = conditions[orIndex].and[andIndex]
|
||||
const fieldName = Object.keys(condition)[0]
|
||||
const fieldPath = Object.keys(condition)[0]
|
||||
|
||||
const operator =
|
||||
(Object.keys(condition?.[fieldName] || {})?.[0] as Operator) || undefined
|
||||
(Object.keys(condition?.[fieldPath] || {})?.[0] as Operator) || undefined
|
||||
|
||||
const value = condition?.[fieldName]?.[operator] || undefined
|
||||
const value = condition?.[fieldPath]?.[operator] || undefined
|
||||
|
||||
return (
|
||||
<li key={andIndex}>
|
||||
@@ -158,13 +158,13 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
|
||||
<Condition
|
||||
addCondition={addCondition}
|
||||
andIndex={andIndex}
|
||||
fieldName={fieldName}
|
||||
filterOptions={resolvedFilterOptions?.get(fieldName)}
|
||||
fieldPath={fieldPath}
|
||||
filterOptions={resolvedFilterOptions?.get(fieldPath)}
|
||||
operator={operator}
|
||||
orIndex={orIndex}
|
||||
reducedFields={reducedFields}
|
||||
removeCondition={removeCondition}
|
||||
RenderedFilter={renderedFilters?.get(fieldName)}
|
||||
RenderedFilter={renderedFilters?.get(fieldPath)}
|
||||
updateCondition={updateCondition}
|
||||
value={value}
|
||||
/>
|
||||
|
||||
@@ -19,8 +19,8 @@ type ReduceFieldOptionsArgs = {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces a field map to a flat array of fields with labels and values.
|
||||
* Used in the WhereBuilder component to render the fields in the dropdown.
|
||||
* Transforms a fields schema into a flattened array of fields with labels and values.
|
||||
* Used in the `WhereBuilder` component to render the fields in the dropdown.
|
||||
*/
|
||||
export const reduceFieldsToOptions = ({
|
||||
fields,
|
||||
|
||||
@@ -36,15 +36,17 @@ let payload: PayloadTestSDK<Config>
|
||||
|
||||
import { listViewSelectAPISlug } from 'admin/collections/ListViewSelectAPI/index.js'
|
||||
import { devUser } from 'credentials.js'
|
||||
import { addListFilter } from 'helpers/e2e/addListFilter.js'
|
||||
import { assertNetworkRequests } from 'helpers/e2e/assertNetworkRequests.js'
|
||||
import {
|
||||
openListColumns,
|
||||
reorderColumns,
|
||||
sortColumn,
|
||||
toggleColumn,
|
||||
waitForColumnInURL,
|
||||
} from 'helpers/e2e/columns/index.js'
|
||||
import { addListFilter, openListFilters } from 'helpers/e2e/filters/index.js'
|
||||
import { goToNextPage, goToPreviousPage } from 'helpers/e2e/goToNextPage.js'
|
||||
import { goToFirstCell } from 'helpers/e2e/navigateToDoc.js'
|
||||
import { openListColumns } from 'helpers/e2e/openListColumns.js'
|
||||
import { openListFilters } from 'helpers/e2e/openListFilters.js'
|
||||
import { deletePreferences } from 'helpers/e2e/preferences.js'
|
||||
import { sortColumn } from 'helpers/e2e/sortColumn.js'
|
||||
import { toggleColumn, waitForColumnInURL } from 'helpers/e2e/toggleColumn.js'
|
||||
import { openDocDrawer } from 'helpers/e2e/toggleDocDrawer.js'
|
||||
import { closeListDrawer } from 'helpers/e2e/toggleListDrawer.js'
|
||||
import path from 'path'
|
||||
@@ -53,9 +55,8 @@ import { fileURLToPath } from 'url'
|
||||
|
||||
import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'
|
||||
|
||||
import { reorderColumns } from '../../../helpers/e2e/reorderColumns.js'
|
||||
import { reInitializeDB } from '../../../helpers/reInitializeDB.js'
|
||||
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../../../playwright.config.js'
|
||||
import { TEST_TIMEOUT_LONG } from '../../../playwright.config.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const currentFolder = path.dirname(filename)
|
||||
@@ -719,7 +720,6 @@ describe('List View', () => {
|
||||
page,
|
||||
fieldLabel: 'Tab 1 > Title',
|
||||
operatorLabel: 'equals',
|
||||
skipValueInput: true,
|
||||
})
|
||||
|
||||
const valueInput = whereBuilder.locator('.condition__value >> input')
|
||||
@@ -857,7 +857,6 @@ describe('List View', () => {
|
||||
page,
|
||||
fieldLabel: 'Self Relation',
|
||||
operatorLabel: 'equals',
|
||||
skipValueInput: true,
|
||||
})
|
||||
|
||||
const valueField = whereBuilder.locator('.condition__value')
|
||||
|
||||
@@ -2,8 +2,8 @@ import type { BrowserContext, Locator, Page } from '@playwright/test'
|
||||
import type { PayloadTestSDK } from 'helpers/sdk/index.js'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { addListFilter } from 'helpers/e2e/addListFilter.js'
|
||||
import { addArrayRow } from 'helpers/e2e/fields/array/index.js'
|
||||
import { addListFilter } from 'helpers/e2e/filters/index.js'
|
||||
import { selectInput } from 'helpers/e2e/selectInput.js'
|
||||
import { toggleBlockOrArrayRow } from 'helpers/e2e/toggleCollapsible.js'
|
||||
import * as path from 'path'
|
||||
@@ -642,7 +642,6 @@ test.describe('Bulk Edit', () => {
|
||||
fieldLabel: 'ID',
|
||||
operatorLabel: 'equals',
|
||||
value: originalDoc.id,
|
||||
skipValueInput: false,
|
||||
})
|
||||
|
||||
// select first item
|
||||
|
||||
@@ -93,7 +93,30 @@ export const Relationship: CollectionConfig = {
|
||||
label: 'Collapsible',
|
||||
fields: [
|
||||
{
|
||||
name: 'nestedRelationshipFilteredByField',
|
||||
name: 'filteredByFieldInCollapsible',
|
||||
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: 'array',
|
||||
type: 'array',
|
||||
label: 'Array',
|
||||
fields: [
|
||||
{
|
||||
name: 'filteredByFieldInArray',
|
||||
filterOptions: () => {
|
||||
return {
|
||||
filter: {
|
||||
|
||||
@@ -3,8 +3,9 @@ import type { CollectionSlug } from 'payload'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { assertToastErrors } from 'helpers/assertToastErrors.js'
|
||||
import { addListFilter } from 'helpers/e2e/addListFilter.js'
|
||||
import { addArrayRow } from 'helpers/e2e/fields/array/addArrayRow.js'
|
||||
import { openCreateDocDrawer } from 'helpers/e2e/fields/relationship/openCreateDocDrawer.js'
|
||||
import { addListFilter } from 'helpers/e2e/filters/index.js'
|
||||
import { goToNextPage } from 'helpers/e2e/goToNextPage.js'
|
||||
import { openDocControls } from 'helpers/e2e/openDocControls.js'
|
||||
import { openDocDrawer } from 'helpers/e2e/toggleDocDrawer.js'
|
||||
@@ -28,12 +29,12 @@ import {
|
||||
ensureCompilationIsDone,
|
||||
initPageConsoleErrorCatch,
|
||||
saveDocAndAssert,
|
||||
throttleTest,
|
||||
// throttleTest,
|
||||
} from '../helpers.js'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||
import { assertNetworkRequests } from '../helpers/e2e/assertNetworkRequests.js'
|
||||
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
||||
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
||||
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
||||
import {
|
||||
collection1Slug,
|
||||
mixedMediaCollectionSlug,
|
||||
@@ -382,7 +383,6 @@ describe('Relationship Field', () => {
|
||||
page,
|
||||
fieldLabel: 'Relationship Filtered By Field',
|
||||
operatorLabel: 'equals',
|
||||
skipValueInput: true,
|
||||
})
|
||||
|
||||
const valueInput = page.locator('.condition__value input')
|
||||
@@ -394,7 +394,7 @@ describe('Relationship Field', () => {
|
||||
await expect(valueOptions.locator(`text=${idToInclude}`)).toBeVisible()
|
||||
})
|
||||
|
||||
test('should apply filter options of nested fields to list view filter controls', async () => {
|
||||
test('should apply `filterOptions` of nested fields to list view filter controls', async () => {
|
||||
const { id: idToInclude } = await payload.create({
|
||||
collection: slug,
|
||||
data: {
|
||||
@@ -402,42 +402,65 @@ describe('Relationship Field', () => {
|
||||
},
|
||||
})
|
||||
|
||||
// first ensure that filter options are applied in the edit view
|
||||
// First ensure that filter options are applied to the Edit View
|
||||
await page.goto(url.edit(idToInclude))
|
||||
await wait(300)
|
||||
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
|
||||
const fieldInCollapsible = page.locator('#field-filteredByFieldInCollapsible')
|
||||
await fieldInCollapsible.click({ delay: 100 })
|
||||
const optionsInCollapsible = fieldInCollapsible.locator('.rs__option')
|
||||
await expect(optionsInCollapsible).toHaveCount(1)
|
||||
await expect(optionsInCollapsible).toContainText(idToInclude)
|
||||
|
||||
await addArrayRow(page, { fieldName: 'array' })
|
||||
|
||||
const fieldInArray = page.locator('#field-array__0__filteredByFieldInArray')
|
||||
await fieldInArray.click({ delay: 100 })
|
||||
const optionsInArray = fieldInArray.locator('.rs__option')
|
||||
await expect(optionsInArray).toHaveCount(1)
|
||||
await expect(optionsInArray).toContainText(idToInclude)
|
||||
|
||||
// Now ensure that the same filter options are applied in the list view
|
||||
await page.goto(url.list)
|
||||
await wait(300)
|
||||
|
||||
const { whereBuilder } = await addListFilter({
|
||||
const { condition: condition1 } = await addListFilter({
|
||||
page,
|
||||
fieldLabel: 'Collapsible > Nested Relationship Filtered By Field',
|
||||
fieldLabel: 'Collapsible > Filtered By Field In Collapsible',
|
||||
operatorLabel: 'equals',
|
||||
skipValueInput: true,
|
||||
})
|
||||
|
||||
const valueInput = page.locator('.condition__value input')
|
||||
const valueInput = condition1.locator('.condition__value input')
|
||||
await valueInput.click()
|
||||
const valueOptions = whereBuilder.locator('.condition__value .rs__option')
|
||||
const valueOptions = condition1.locator('.condition__value .rs__option')
|
||||
|
||||
await expect(valueOptions).toHaveCount(2)
|
||||
await expect(valueOptions.locator(`text=None`)).toBeVisible()
|
||||
await expect(valueOptions.locator(`text=${idToInclude}`)).toBeVisible()
|
||||
|
||||
const { condition: condition2 } = await addListFilter({
|
||||
page,
|
||||
fieldLabel: 'Array > Filtered By Field In Array',
|
||||
operatorLabel: 'equals',
|
||||
})
|
||||
|
||||
test('should allow usage of relationTo in filterOptions', async () => {
|
||||
const valueInput2 = condition2.locator('.condition__value input')
|
||||
await valueInput2.click()
|
||||
const valueOptions2 = condition2.locator('.condition__value .rs__option')
|
||||
|
||||
await expect(valueOptions2).toHaveCount(2)
|
||||
await expect(valueOptions2.locator(`text=None`)).toBeVisible()
|
||||
await expect(valueOptions2.locator(`text=${idToInclude}`)).toBeVisible()
|
||||
})
|
||||
|
||||
test('should allow usage of relationTo in `filterOptions`', async () => {
|
||||
const { id: include } = (await payload.create({
|
||||
collection: relationOneSlug,
|
||||
data: {
|
||||
name: 'include',
|
||||
},
|
||||
})) as any
|
||||
|
||||
const { id: exclude } = (await payload.create({
|
||||
collection: relationOneSlug,
|
||||
data: {
|
||||
@@ -455,7 +478,7 @@ describe('Relationship Field', () => {
|
||||
await expect(options).not.toContainText(exclude)
|
||||
})
|
||||
|
||||
test('should allow usage of siblingData in filterOptions', async () => {
|
||||
test('should allow usage of siblingData in `filterOptions`', async () => {
|
||||
await payload.create({
|
||||
collection: relationWithTitleSlug,
|
||||
data: {
|
||||
@@ -476,7 +499,7 @@ describe('Relationship Field', () => {
|
||||
})
|
||||
|
||||
// TODO: Flaky test in CI - fix. https://github.com/payloadcms/payload/actions/runs/8559547748/job/23456806365
|
||||
test.skip('should not query for a relationship when filterOptions returns false', async () => {
|
||||
test.skip('should not query for a relationship when `filterOptions` returns false', async () => {
|
||||
await payload.create({
|
||||
collection: relationFalseFilterOptionSlug,
|
||||
data: {
|
||||
@@ -495,7 +518,7 @@ describe('Relationship Field', () => {
|
||||
})
|
||||
|
||||
// TODO: Flaky test in CI - fix.
|
||||
test('should show a relationship when filterOptions returns true', async () => {
|
||||
test('should show a relationship when `filterOptions` returns true', async () => {
|
||||
await payload.create({
|
||||
collection: relationTrueFilterOptionSlug,
|
||||
data: {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { addListFilter } from 'helpers/e2e/addListFilter.js'
|
||||
import { addListFilter } from 'helpers/e2e/filters/index.js'
|
||||
import path from 'path'
|
||||
import { wait } from 'payload/shared'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { ensureCompilationIsDone, initPageConsoleErrorCatch } from '../../../helpers.js'
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { addListFilter } from 'helpers/e2e/addListFilter.js'
|
||||
import { openListFilters } from 'helpers/e2e/openListFilters.js'
|
||||
import path from 'path'
|
||||
import { wait } from 'payload/shared'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { addListFilter } from 'helpers/e2e/addListFilter.js'
|
||||
import path from 'path'
|
||||
import { wait } from 'payload/shared'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'
|
||||
import type { Config } from '../../payload-types.js'
|
||||
|
||||
import {
|
||||
ensureCompilationIsDone,
|
||||
initPageConsoleErrorCatch,
|
||||
saveDocAndAssert,
|
||||
} from '../../../helpers.js'
|
||||
import { ensureCompilationIsDone, initPageConsoleErrorCatch } from '../../../helpers.js'
|
||||
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
|
||||
import { assertToastErrors } from '../../../helpers/assertToastErrors.js'
|
||||
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
|
||||
import { reInitializeDB } from '../../../helpers/reInitializeDB.js'
|
||||
import { RESTClient } from '../../../helpers/rest.js'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { addListFilter } from 'helpers/e2e/addListFilter.js'
|
||||
import { addListFilter } from 'helpers/e2e/filters/index.js'
|
||||
import path from 'path'
|
||||
import { wait } from 'payload/shared'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { addListFilter } from 'helpers/e2e/addListFilter.js'
|
||||
import { openCreateDocDrawer } from 'helpers/e2e/fields/relationship/openCreateDocDrawer.js'
|
||||
import { addListFilter } from 'helpers/e2e/filters/index.js'
|
||||
import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
|
||||
import { openDocControls } from 'helpers/e2e/openDocControls.js'
|
||||
import { openDocDrawer } from 'helpers/e2e/toggleDocDrawer.js'
|
||||
|
||||
@@ -2,10 +2,9 @@ import type { Page } from '@playwright/test'
|
||||
import type { GeneratedTypes } from 'helpers/sdk/types.js'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { addListFilter } from 'helpers/e2e/addListFilter.js'
|
||||
import { openListColumns } from 'helpers/e2e/openListColumns.js'
|
||||
import { openListColumns, toggleColumn } from 'helpers/e2e/columns/index.js'
|
||||
import { addListFilter } from 'helpers/e2e/filters/index.js'
|
||||
import { upsertPreferences } from 'helpers/e2e/preferences.js'
|
||||
import { toggleColumn } from 'helpers/e2e/toggleColumn.js'
|
||||
import path from 'path'
|
||||
import { wait } from 'payload/shared'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
@@ -3,12 +3,11 @@ import type { PayloadTestSDK } from 'helpers/sdk/index.js'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { devUser } from 'credentials.js'
|
||||
import { addListFilter } from 'helpers/e2e/addListFilter.js'
|
||||
import { sortColumn, toggleColumn } from 'helpers/e2e/columns/index.js'
|
||||
import { addListFilter } from 'helpers/e2e/filters/index.js'
|
||||
import { goToNextPage } from 'helpers/e2e/goToNextPage.js'
|
||||
import { addGroupBy, clearGroupBy, closeGroupBy, openGroupBy } from 'helpers/e2e/groupBy.js'
|
||||
import { addGroupBy, clearGroupBy, closeGroupBy, openGroupBy } from 'helpers/e2e/groupBy/index.js'
|
||||
import { deletePreferences } from 'helpers/e2e/preferences.js'
|
||||
import { sortColumn } from 'helpers/e2e/sortColumn.js'
|
||||
import { toggleColumn } from 'helpers/e2e/toggleColumn.js'
|
||||
import { openNav } from 'helpers/e2e/toggleNav.js'
|
||||
import { reInitializeDB } from 'helpers/reInitializeDB.js'
|
||||
import * as path from 'path'
|
||||
|
||||
5
test/helpers/e2e/columns/index.ts
Normal file
5
test/helpers/e2e/columns/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { openListColumns } from './openListColumns.js'
|
||||
export { reorderColumns } from './reorderColumns.js'
|
||||
export { sortColumn } from './sortColumn.js'
|
||||
export { toggleColumn } from './toggleColumn.js'
|
||||
export { waitForColumnInURL } from './waitForColumnsInURL.js'
|
||||
@@ -3,7 +3,7 @@ import type { Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import { wait } from 'payload/shared'
|
||||
|
||||
import { exactText } from '../../helpers.js'
|
||||
import { exactText } from '../../../helpers.js'
|
||||
|
||||
export const reorderColumns = async (
|
||||
page: Page,
|
||||
@@ -2,8 +2,9 @@ import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { exactText } from '../../helpers.js'
|
||||
import { exactText } from '../../../helpers.js'
|
||||
import { openListColumns } from './openListColumns.js'
|
||||
import { waitForColumnInURL } from './waitForColumnsInURL.js'
|
||||
|
||||
export const toggleColumn = async (
|
||||
page: Page,
|
||||
@@ -64,24 +65,3 @@ export const toggleColumn = async (
|
||||
|
||||
return { columnContainer }
|
||||
}
|
||||
|
||||
export const waitForColumnInURL = async ({
|
||||
page,
|
||||
columnName,
|
||||
state,
|
||||
}: {
|
||||
columnName: string
|
||||
page: Page
|
||||
state: 'off' | 'on'
|
||||
}): Promise<void> => {
|
||||
await page.waitForURL(/.*\?.*/)
|
||||
|
||||
const identifier = `${state === 'off' ? '-' : ''}${columnName}`
|
||||
|
||||
// Test that the identifier is in the URL
|
||||
// It must appear in the `columns` query parameter, i.e. after `columns=...` and before the next `&`
|
||||
// It must also appear in it entirety to prevent partially matching other values, i.e. between quotation marks
|
||||
const regex = new RegExp(`columns=([^&]*${encodeURIComponent(`"${identifier}"`)}[^&]*)`)
|
||||
|
||||
await page.waitForURL(regex)
|
||||
}
|
||||
22
test/helpers/e2e/columns/waitForColumnsInURL.ts
Normal file
22
test/helpers/e2e/columns/waitForColumnsInURL.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Page } from 'playwright'
|
||||
|
||||
export const waitForColumnInURL = async ({
|
||||
page,
|
||||
columnName,
|
||||
state,
|
||||
}: {
|
||||
columnName: string
|
||||
page: Page
|
||||
state: 'off' | 'on'
|
||||
}): Promise<void> => {
|
||||
await page.waitForURL(/.*\?.*/)
|
||||
|
||||
const identifier = `${state === 'off' ? '-' : ''}${columnName}`
|
||||
|
||||
// Test that the identifier is in the URL
|
||||
// It must appear in the `columns` query parameter, i.e. after `columns=...` and before the next `&`
|
||||
// It must also appear in it entirety to prevent partially matching other values, i.e. between quotation marks
|
||||
const regex = new RegExp(`columns=([^&]*${encodeURIComponent(`"${identifier}"`)}[^&]*)`)
|
||||
|
||||
await page.waitForURL(regex)
|
||||
}
|
||||
@@ -2,55 +2,77 @@ import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { selectInput } from '../selectInput.js'
|
||||
import { openListFilters } from './openListFilters.js'
|
||||
import { selectInput } from './selectInput.js'
|
||||
|
||||
export const addListFilter = async ({
|
||||
page,
|
||||
fieldLabel = 'ID',
|
||||
operatorLabel = 'equals',
|
||||
value = '',
|
||||
skipValueInput,
|
||||
value,
|
||||
}: {
|
||||
fieldLabel: string
|
||||
operatorLabel: string
|
||||
page: Page
|
||||
replaceExisting?: boolean
|
||||
skipValueInput?: boolean
|
||||
value?: string
|
||||
}): Promise<{
|
||||
/**
|
||||
* A Locator pointing to the condition that was just added.
|
||||
*/
|
||||
condition: Locator
|
||||
/**
|
||||
* A Locator pointing to the WhereBuilder node.
|
||||
*/
|
||||
whereBuilder: Locator
|
||||
}> => {
|
||||
await openListFilters(page, {})
|
||||
|
||||
const whereBuilder = page.locator('.where-builder')
|
||||
|
||||
await whereBuilder.locator('.where-builder__add-first-filter').click()
|
||||
const addFirst = whereBuilder.locator('.where-builder__add-first-filter')
|
||||
const initializedEmpty = await addFirst.isVisible()
|
||||
|
||||
if (initializedEmpty) {
|
||||
await addFirst.click()
|
||||
}
|
||||
|
||||
const filters = whereBuilder.locator('.where-builder__or-filters > li')
|
||||
expect(await filters.count()).toBeGreaterThan(0)
|
||||
|
||||
// If there were already filter(s), need to add another and manipulate _that_ instead of the existing one
|
||||
if (!initializedEmpty) {
|
||||
const addFilterButtons = whereBuilder.locator('.where-builder__add-or')
|
||||
await addFilterButtons.last().click()
|
||||
await expect(filters).toHaveCount(2)
|
||||
}
|
||||
|
||||
const condition = filters.last()
|
||||
|
||||
await selectInput({
|
||||
selectLocator: whereBuilder.locator('.condition__field'),
|
||||
selectLocator: condition.locator('.condition__field'),
|
||||
multiSelect: false,
|
||||
option: fieldLabel,
|
||||
})
|
||||
|
||||
await selectInput({
|
||||
selectLocator: whereBuilder.locator('.condition__operator'),
|
||||
selectLocator: condition.locator('.condition__operator'),
|
||||
multiSelect: false,
|
||||
option: operatorLabel,
|
||||
})
|
||||
|
||||
if (!skipValueInput) {
|
||||
if (value !== undefined) {
|
||||
const networkPromise = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(encodeURIComponent('where[or')) && response.status() === 200,
|
||||
)
|
||||
const valueLocator = whereBuilder.locator('.condition__value')
|
||||
const valueLocator = condition.locator('.condition__value')
|
||||
const valueInput = valueLocator.locator('input')
|
||||
await valueInput.fill(value)
|
||||
await expect(valueInput).toHaveValue(value)
|
||||
|
||||
if ((await valueLocator.locator('input.rs__input').count()) > 0) {
|
||||
const valueOptions = whereBuilder.locator('.condition__value .rs__option')
|
||||
const valueOptions = condition.locator('.condition__value .rs__option')
|
||||
const createValue = valueOptions.locator(`text=Create "${value}"`)
|
||||
if ((await createValue.count()) > 0) {
|
||||
await createValue.click()
|
||||
@@ -65,5 +87,5 @@ export const addListFilter = async ({
|
||||
await networkPromise
|
||||
}
|
||||
|
||||
return { whereBuilder }
|
||||
return { whereBuilder, condition }
|
||||
}
|
||||
2
test/helpers/e2e/filters/index.ts
Normal file
2
test/helpers/e2e/filters/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { addListFilter } from './addListFilter.js'
|
||||
export { openListFilters } from './openListFilters.js'
|
||||
@@ -1,90 +0,0 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
import { exactText } from 'helpers.js'
|
||||
|
||||
type ToggleOptions = {
|
||||
groupByContainerSelector: string
|
||||
targetState: 'closed' | 'open'
|
||||
togglerSelector: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the group-by drawer in the list view based on the targetState option.
|
||||
*/
|
||||
export const toggleGroupBy = async (
|
||||
page: Page,
|
||||
{
|
||||
targetState = 'open',
|
||||
togglerSelector = '#toggle-group-by',
|
||||
groupByContainerSelector = '#list-controls-group-by',
|
||||
}: ToggleOptions,
|
||||
) => {
|
||||
const groupByContainer = page.locator(groupByContainerSelector).first()
|
||||
|
||||
const isAlreadyOpen = await groupByContainer.isVisible()
|
||||
|
||||
if (!isAlreadyOpen && targetState === 'open') {
|
||||
await page.locator(togglerSelector).first().click()
|
||||
await expect(page.locator(`${groupByContainerSelector}.rah-static--height-auto`)).toBeVisible()
|
||||
}
|
||||
|
||||
if (isAlreadyOpen && targetState === 'closed') {
|
||||
await page.locator(togglerSelector).first().click()
|
||||
await expect(page.locator(`${groupByContainerSelector}.rah-static--height-auto`)).toBeHidden()
|
||||
}
|
||||
|
||||
return { groupByContainer }
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the group-by drawer in the list view. If it's already closed, does nothing.
|
||||
*/
|
||||
export const closeGroupBy = async (
|
||||
page: Page,
|
||||
options?: Omit<ToggleOptions, 'targetState'>,
|
||||
): Promise<{
|
||||
groupByContainer: Locator
|
||||
}> => toggleGroupBy(page, { ...(options || ({} as ToggleOptions)), targetState: 'closed' })
|
||||
|
||||
/**
|
||||
* Opens the group-by drawer in the list view. If it's already open, does nothing.
|
||||
*/
|
||||
export const openGroupBy = async (
|
||||
page: Page,
|
||||
options?: Omit<ToggleOptions, 'targetState'>,
|
||||
): Promise<{
|
||||
groupByContainer: Locator
|
||||
}> => toggleGroupBy(page, { ...(options || ({} as ToggleOptions)), targetState: 'open' })
|
||||
|
||||
export const addGroupBy = async (
|
||||
page: Page,
|
||||
{ fieldLabel, fieldPath }: { fieldLabel: string; fieldPath: string },
|
||||
): Promise<{ field: Locator; groupByContainer: Locator }> => {
|
||||
const { groupByContainer } = await openGroupBy(page)
|
||||
const field = groupByContainer.locator('#group-by--field-select')
|
||||
|
||||
await field.click()
|
||||
await field.locator('.rs__option', { hasText: exactText(fieldLabel) })?.click()
|
||||
await expect(field.locator('.react-select--single-value')).toHaveText(fieldLabel)
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`&groupBy=${fieldPath}`))
|
||||
|
||||
return { groupByContainer, field }
|
||||
}
|
||||
|
||||
export const clearGroupBy = async (page: Page): Promise<{ groupByContainer: Locator }> => {
|
||||
const { groupByContainer } = await openGroupBy(page)
|
||||
|
||||
await groupByContainer.locator('#group-by--reset').click()
|
||||
const field = groupByContainer.locator('#group-by--field-select')
|
||||
|
||||
await expect(field.locator('.react-select--single-value')).toHaveText('Select a value')
|
||||
await expect(groupByContainer.locator('#group-by--reset')).toBeHidden()
|
||||
await expect(page).not.toHaveURL(/&groupBy=/)
|
||||
await expect(groupByContainer.locator('#field-direction input')).toBeDisabled()
|
||||
await expect(page.locator('.table-wrap')).toHaveCount(1)
|
||||
await expect(page.locator('.group-by-header')).toHaveCount(0)
|
||||
|
||||
return { groupByContainer }
|
||||
}
|
||||
22
test/helpers/e2e/groupBy/addGroupBy.ts
Normal file
22
test/helpers/e2e/groupBy/addGroupBy.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
import { exactText } from 'helpers.js'
|
||||
|
||||
import { openGroupBy } from './openGroupBy.js'
|
||||
|
||||
export const addGroupBy = async (
|
||||
page: Page,
|
||||
{ fieldLabel, fieldPath }: { fieldLabel: string; fieldPath: string },
|
||||
): Promise<{ field: Locator; groupByContainer: Locator }> => {
|
||||
const { groupByContainer } = await openGroupBy(page)
|
||||
const field = groupByContainer.locator('#group-by--field-select')
|
||||
|
||||
await field.click()
|
||||
await field.locator('.rs__option', { hasText: exactText(fieldLabel) })?.click()
|
||||
await expect(field.locator('.react-select--single-value')).toHaveText(fieldLabel)
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`&groupBy=${fieldPath}`))
|
||||
|
||||
return { groupByContainer, field }
|
||||
}
|
||||
21
test/helpers/e2e/groupBy/clearGroupBy.ts
Normal file
21
test/helpers/e2e/groupBy/clearGroupBy.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { openGroupBy } from './openGroupBy.js'
|
||||
|
||||
export const clearGroupBy = async (page: Page): Promise<{ groupByContainer: Locator }> => {
|
||||
const { groupByContainer } = await openGroupBy(page)
|
||||
|
||||
await groupByContainer.locator('#group-by--reset').click()
|
||||
const field = groupByContainer.locator('#group-by--field-select')
|
||||
|
||||
await expect(field.locator('.react-select--single-value')).toHaveText('Select a value')
|
||||
await expect(groupByContainer.locator('#group-by--reset')).toBeHidden()
|
||||
await expect(page).not.toHaveURL(/&groupBy=/)
|
||||
await expect(groupByContainer.locator('#field-direction input')).toBeDisabled()
|
||||
await expect(page.locator('.table-wrap')).toHaveCount(1)
|
||||
await expect(page.locator('.group-by-header')).toHaveCount(0)
|
||||
|
||||
return { groupByContainer }
|
||||
}
|
||||
15
test/helpers/e2e/groupBy/closeGroupBy.ts
Normal file
15
test/helpers/e2e/groupBy/closeGroupBy.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import type { ToggleOptions } from './toggleGroupBy.js';
|
||||
|
||||
import { toggleGroupBy } from './toggleGroupBy.js'
|
||||
|
||||
/**
|
||||
* Closes the group-by drawer in the list view. If it's already closed, does nothing.
|
||||
*/
|
||||
export const closeGroupBy = async (
|
||||
page: Page,
|
||||
options?: Omit<ToggleOptions, 'targetState'>,
|
||||
): Promise<{
|
||||
groupByContainer: Locator
|
||||
}> => toggleGroupBy(page, { ...(options || ({} as ToggleOptions)), targetState: 'closed' })
|
||||
5
test/helpers/e2e/groupBy/index.js
Normal file
5
test/helpers/e2e/groupBy/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export { addGroupBy } from './addGroupBy.js'
|
||||
export { clearGroupBy } from './clearGroupBy.js'
|
||||
export { closeGroupBy } from './closeGroupBy.js'
|
||||
export { openGroupBy } from './openGroupBy.js'
|
||||
export { toggleGroupBy } from './toggleGroupBy.js'
|
||||
15
test/helpers/e2e/groupBy/openGroupBy.ts
Normal file
15
test/helpers/e2e/groupBy/openGroupBy.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import type { ToggleOptions } from './toggleGroupBy.js'
|
||||
|
||||
import { toggleGroupBy } from './toggleGroupBy.js'
|
||||
|
||||
/**
|
||||
* Opens the group-by drawer in the list view. If it's already open, does nothing.
|
||||
*/
|
||||
export const openGroupBy = async (
|
||||
page: Page,
|
||||
options?: Omit<ToggleOptions, 'targetState'>,
|
||||
): Promise<{
|
||||
groupByContainer: Locator
|
||||
}> => toggleGroupBy(page, { ...(options || ({} as ToggleOptions)), targetState: 'open' })
|
||||
37
test/helpers/e2e/groupBy/toggleGroupBy.ts
Normal file
37
test/helpers/e2e/groupBy/toggleGroupBy.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
export type ToggleOptions = {
|
||||
groupByContainerSelector: string
|
||||
targetState: 'closed' | 'open'
|
||||
togglerSelector: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the group-by drawer in the list view based on the targetState option.
|
||||
*/
|
||||
export const toggleGroupBy = async (
|
||||
page: Page,
|
||||
{
|
||||
targetState = 'open',
|
||||
togglerSelector = '#toggle-group-by',
|
||||
groupByContainerSelector = '#list-controls-group-by',
|
||||
}: ToggleOptions,
|
||||
) => {
|
||||
const groupByContainer = page.locator(groupByContainerSelector).first()
|
||||
|
||||
const isAlreadyOpen = await groupByContainer.isVisible()
|
||||
|
||||
if (!isAlreadyOpen && targetState === 'open') {
|
||||
await page.locator(togglerSelector).first().click()
|
||||
await expect(page.locator(`${groupByContainerSelector}.rah-static--height-auto`)).toBeVisible()
|
||||
}
|
||||
|
||||
if (isAlreadyOpen && targetState === 'closed') {
|
||||
await page.locator(togglerSelector).first().click()
|
||||
await expect(page.locator(`${groupByContainerSelector}.rah-static--height-auto`)).toBeHidden()
|
||||
}
|
||||
|
||||
return { groupByContainer }
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import type { PayloadTestSDK } from '../helpers/sdk/index.js'
|
||||
|
||||
import { ensureCompilationIsDone, initPageConsoleErrorCatch } from '../helpers.js'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||
import { openListFilters } from '../helpers/e2e/openListFilters.js'
|
||||
import { openListFilters } from '../helpers/e2e/filters/index.js'
|
||||
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
||||
import { reInitializeDB } from '../helpers/reInitializeDB.js'
|
||||
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
||||
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
// throttleTest,
|
||||
} from '../helpers.js'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||
import { reorderColumns } from '../helpers/e2e/columns/index.js'
|
||||
import { navigateToDoc } from '../helpers/e2e/navigateToDoc.js'
|
||||
import { reorderColumns } from '../helpers/e2e/reorderColumns.js'
|
||||
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
||||
import { reInitializeDB } from '../helpers/reInitializeDB.js'
|
||||
import { RESTClient } from '../helpers/rest.js'
|
||||
|
||||
@@ -2,8 +2,7 @@ import type { BrowserContext, Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { devUser } from 'credentials.js'
|
||||
import { openListColumns } from 'helpers/e2e/openListColumns.js'
|
||||
import { toggleColumn } from 'helpers/e2e/toggleColumn.js'
|
||||
import { openListColumns, toggleColumn } from 'helpers/e2e/columns/index.js'
|
||||
import { openNav } from 'helpers/e2e/toggleNav.js'
|
||||
import * as path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { addListFilter } from 'helpers/e2e/addListFilter.js'
|
||||
import { addListFilter } from 'helpers/e2e/filters/index.js'
|
||||
import * as path from 'path'
|
||||
import { mapAsync } from 'payload'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
@@ -2,9 +2,8 @@ import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { statSync } from 'fs'
|
||||
import { openListColumns } from 'helpers/e2e/openListColumns.js'
|
||||
import { openListFilters } from 'helpers/e2e/openListFilters.js'
|
||||
import { toggleColumn } from 'helpers/e2e/toggleColumn.js'
|
||||
import { openListColumns, toggleColumn } from 'helpers/e2e/columns/index.js'
|
||||
import { openListFilters } from 'helpers/e2e/filters/index.js'
|
||||
import { openDocDrawer } from 'helpers/e2e/toggleDocDrawer.js'
|
||||
import path from 'path'
|
||||
import { wait } from 'payload/shared'
|
||||
|
||||
Reference in New Issue
Block a user