fix(ui): query preset where field not displaying array values (#13961)
### What?
Query preset WhereField component was not displaying array values
correctly. When using relationship fields with operators like "is in"
that accept multiple values, the array values were not being formatted
and displayed properly in the query preset modal.
### Why?
The original `renderCondition` function only handled single values and
date objects, but did not include logic for arrays. This caused
relationship fields with multiple selected values to either not display
correctly or throw errors when viewed in query preset modals.
### How?
- Added proper array detection with `Array.isArray()` in the
`renderCondition` function
- Created a reusable `formatValue` helper function that handles single
values, objects (dates), and arrays consistently
- For arrays, format each value and join with commas:
`operatorValue.map(formatValue).join(', ')`
- Enhanced `addListFilter` test helper to accept both single values
(`string`) and arrays (`string[]`) for relationship field testing
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211334352350024
This commit is contained in:
@@ -30,19 +30,24 @@ const transformWhereToNaturalLanguage = (
|
||||
}
|
||||
|
||||
const operator = Object.keys(condition[key])[0]
|
||||
let value = condition[key][operator]
|
||||
const operatorValue = condition[key][operator]
|
||||
|
||||
// TODO: this is not right, but works for now at least.
|
||||
// Ideally we look up iterate _fields_ so we know the type of the field
|
||||
// Currently, we're only iterating over the `where` field's value, so we don't know the type
|
||||
if (typeof value === 'object') {
|
||||
try {
|
||||
value = new Date(value).toLocaleDateString()
|
||||
} catch (_err) {
|
||||
value = 'Unknown error has occurred'
|
||||
// Format value - ideally would use field schema for proper typing
|
||||
const formatValue = (val: any): string => {
|
||||
if (typeof val === 'object' && val != null) {
|
||||
try {
|
||||
return new Date(val).toLocaleDateString()
|
||||
} catch {
|
||||
return 'Unknown error has occurred'
|
||||
}
|
||||
}
|
||||
return val?.toString() ?? ''
|
||||
}
|
||||
|
||||
const value = Array.isArray(operatorValue)
|
||||
? operatorValue.map(formatValue).join(', ')
|
||||
: formatValue(operatorValue)
|
||||
|
||||
return (
|
||||
<Pill pillStyle="always-white" size="small">
|
||||
<b>{toWords(key)}</b> {operator} <b>{toWords(value)}</b>
|
||||
|
||||
@@ -14,5 +14,11 @@ export const Pages: CollectionConfig = {
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'postsRelationship',
|
||||
type: 'relationship',
|
||||
relationTo: 'posts',
|
||||
hasMany: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { BrowserContext, Page } from '@playwright/test'
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { devUser } from 'credentials.js'
|
||||
import { openListColumns, toggleColumn } from 'helpers/e2e/columns/index.js'
|
||||
import { addListFilter, openListFilters } from 'helpers/e2e/filters/index.js'
|
||||
import { openNav } from 'helpers/e2e/toggleNav.js'
|
||||
import * as path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
@@ -15,7 +16,6 @@ import {
|
||||
exactText,
|
||||
initPageConsoleErrorCatch,
|
||||
saveDocAndAssert,
|
||||
// throttleTest,
|
||||
} from '../helpers.js'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
||||
@@ -408,4 +408,127 @@ describe('Query Presets', () => {
|
||||
await expect(drawer.locator('.table table > tbody > tr')).toHaveCount(3)
|
||||
await drawer.locator('.collection-list__no-results').isHidden()
|
||||
})
|
||||
|
||||
test('should display single relationship value in query preset modal', async () => {
|
||||
await page.goto(pagesUrl.list)
|
||||
|
||||
// Get a post to use for filtering
|
||||
const posts = await payload.find({
|
||||
collection: 'posts',
|
||||
limit: 1,
|
||||
})
|
||||
const testPost = posts.docs[0]
|
||||
|
||||
await addListFilter({
|
||||
page,
|
||||
fieldLabel: 'Posts Relationship',
|
||||
operatorLabel: 'is in',
|
||||
value: testPost?.text ?? '',
|
||||
})
|
||||
|
||||
// Create a new preset with this filter
|
||||
await page.locator('#create-new-preset').click()
|
||||
const modal = page.locator('[id^=doc-drawer_payload-query-presets_0_]')
|
||||
await expect(modal).toBeVisible()
|
||||
|
||||
const presetTitle = 'Single Relationship Filter Test'
|
||||
await modal.locator('input[name="title"]').fill(presetTitle)
|
||||
|
||||
await saveDocAndAssert(page)
|
||||
await expect(modal).toBeHidden()
|
||||
|
||||
// Wait for URL to update with the new preset
|
||||
await page.waitForURL((url) => url.searchParams.has('preset'))
|
||||
|
||||
// Open the edit preset modal to check the filter display
|
||||
await page.locator('#edit-preset').click()
|
||||
const editModal = page.locator('[id^=doc-drawer_payload-query-presets_0_]')
|
||||
await expect(editModal).toBeVisible()
|
||||
|
||||
// Check that the Where field properly displays the relationship filter
|
||||
const whereFieldContent = editModal.locator('.query-preset-where-field .value-wrapper')
|
||||
await expect(whereFieldContent).toBeVisible()
|
||||
|
||||
// Verify that the filter shows the relationship field, operator, and post value
|
||||
await expect(whereFieldContent).toContainText('Posts Relationship')
|
||||
await expect(whereFieldContent).toContainText('in')
|
||||
|
||||
// Check that the post ID is displayed
|
||||
await expect(whereFieldContent).toContainText(testPost?.id ?? '')
|
||||
})
|
||||
|
||||
test('should display multiple relationship values in query preset modal', async () => {
|
||||
await page.goto(pagesUrl.list)
|
||||
|
||||
// Get posts to use for filtering
|
||||
const posts = await payload.find({
|
||||
collection: 'posts',
|
||||
limit: 2,
|
||||
})
|
||||
const [testPost1, testPost2] = posts.docs
|
||||
|
||||
await openListFilters(page, {})
|
||||
|
||||
const whereBuilder = page.locator('.where-builder')
|
||||
const addFirst = whereBuilder.locator('.where-builder__add-first-filter')
|
||||
|
||||
await addFirst.click()
|
||||
|
||||
const condition = whereBuilder.locator('.where-builder__or-filters > li').first()
|
||||
|
||||
// Select field
|
||||
await condition.locator('.condition__field .rs__control').click()
|
||||
await page.locator('.rs__option:has-text("Posts Relationship")').click()
|
||||
|
||||
// Select operator
|
||||
await condition.locator('.condition__operator .rs__control').click()
|
||||
await page.locator('.rs__option:has-text("is in")').click()
|
||||
|
||||
// Select multiple values
|
||||
const valueSelect = condition.locator('.condition__value')
|
||||
|
||||
// Select first post
|
||||
await valueSelect.locator('.rs__control').click()
|
||||
await page.locator(`.rs__option:has-text("${testPost1?.text}")`).click()
|
||||
|
||||
// Reopen dropdown and select second post
|
||||
await valueSelect.locator('.rs__control').click()
|
||||
await page.locator(`.rs__option:has-text("${testPost2?.text}")`).click()
|
||||
|
||||
// Wait for network response
|
||||
await page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(encodeURIComponent('where[or')) && response.status() === 200,
|
||||
)
|
||||
|
||||
// Create a new preset with this filter
|
||||
await page.locator('#create-new-preset').click()
|
||||
const modal = page.locator('[id^=doc-drawer_payload-query-presets_0_]')
|
||||
await expect(modal).toBeVisible()
|
||||
|
||||
const presetTitle = 'Multiple Relationship Filter Test'
|
||||
await modal.locator('input[name="title"]').fill(presetTitle)
|
||||
|
||||
await saveDocAndAssert(page)
|
||||
await expect(modal).toBeHidden()
|
||||
|
||||
// Wait for URL to update with the new preset
|
||||
await page.waitForURL((url) => url.searchParams.has('preset'))
|
||||
|
||||
// Open the edit preset modal to check the filter display
|
||||
await page.locator('#edit-preset').click()
|
||||
const editModal = page.locator('[id^=doc-drawer_payload-query-presets_0_]')
|
||||
await expect(editModal).toBeVisible()
|
||||
|
||||
const whereFieldContent = editModal.locator('.query-preset-where-field .value-wrapper')
|
||||
await expect(whereFieldContent).toBeVisible()
|
||||
|
||||
await expect(whereFieldContent).toContainText('Posts Relationship')
|
||||
await expect(whereFieldContent).toContainText('in')
|
||||
|
||||
// Check that both post IDs are displayed (comma-separated)
|
||||
await expect(whereFieldContent).toContainText(testPost1?.id ?? '')
|
||||
await expect(whereFieldContent).toContainText(testPost2?.id ?? '')
|
||||
await expect(whereFieldContent).toContainText(',')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -124,6 +124,7 @@ export interface UserAuthOperations {
|
||||
export interface Page {
|
||||
id: string;
|
||||
text?: string | null;
|
||||
postsRelationship?: (string | Post)[] | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
@@ -268,7 +269,7 @@ export interface PayloadQueryPreset {
|
||||
| null;
|
||||
relatedCollection: 'pages' | 'posts';
|
||||
/**
|
||||
* This is a tempoary field used to determine if updating the preset would remove the user's access to it. When `true`, this record will be deleted after running the preset's `validate` function.
|
||||
* This is a temporary field used to determine if updating the preset would remove the user's access to it. When `true`, this record will be deleted after running the preset's `validate` function.
|
||||
*/
|
||||
isTemp?: boolean | null;
|
||||
updatedAt: string;
|
||||
@@ -280,6 +281,7 @@ export interface PayloadQueryPreset {
|
||||
*/
|
||||
export interface PagesSelect<T extends boolean = true> {
|
||||
text?: T;
|
||||
postsRelationship?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Payload, QueryPreset } from 'payload'
|
||||
|
||||
import { devUser as devCredentials, regularUser as regularCredentials } from '../credentials.js'
|
||||
import { executePromises } from '../helpers/executePromises.js'
|
||||
import { pagesSlug, usersSlug } from './slugs.js'
|
||||
import { pagesSlug, postsSlug, usersSlug } from './slugs.js'
|
||||
|
||||
type SeededQueryPreset = {
|
||||
relatedCollection: 'pages'
|
||||
@@ -136,6 +136,27 @@ export const seed = async (_payload: Payload) => {
|
||||
false,
|
||||
)
|
||||
|
||||
// Create posts first, then pages with relationships
|
||||
const [post1, post2] = await executePromises(
|
||||
[
|
||||
() =>
|
||||
_payload.create({
|
||||
collection: postsSlug,
|
||||
data: {
|
||||
text: 'Test Post 1',
|
||||
},
|
||||
}),
|
||||
() =>
|
||||
_payload.create({
|
||||
collection: postsSlug,
|
||||
data: {
|
||||
text: 'Test Post 2',
|
||||
},
|
||||
}),
|
||||
],
|
||||
false,
|
||||
)
|
||||
|
||||
await executePromises(
|
||||
[
|
||||
() =>
|
||||
@@ -143,6 +164,7 @@ export const seed = async (_payload: Payload) => {
|
||||
collection: pagesSlug,
|
||||
data: {
|
||||
text: 'example page',
|
||||
postsRelationship: [post1?.id, post2?.id],
|
||||
},
|
||||
}),
|
||||
() =>
|
||||
|
||||
Reference in New Issue
Block a user