Skip to content

Commit a507fcc

Browse files
authored
fix(ui): json and richText fields expose unsupported operators in WhereBuilder (#16353)
# Overview Fixes broken operators exposed in the WhereBuilder for `json` and `richText` fields. Both field types are stored as JSONB in Postgres, so string operators like `contains`, `like`, and `not_like` throw `operator does not exist: jsonb ~~* text`. In MongoDB, `$regex` on a BSON document field silently returns 0 results. The `within`/`intersects` operators on `json` were also wrong since those are geo operators. ## Key Changes Reduced the WhereBuilder operator list for both `json` and `richText` to `[exists]` only, and added e2e tests asserting only `exists` is available in the dropdown for both field types. ## Design Decisions `exists` is the only operator that works and makes sense for these fields in the UI. Programmatic queries via the API with `equals`/`in` still work if you pass the exact JSON document, this only affects what the WhereBuilder exposes.
1 parent 60d8678 commit a507fcc

4 files changed

Lines changed: 130 additions & 74 deletions

File tree

packages/ui/src/elements/WhereBuilder/field-types.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export const fieldTypeConditions: {
108108
},
109109
json: {
110110
component: 'Text',
111-
operators: [...base, like, contains, notLike, within, intersects],
111+
operators: [exists],
112112
},
113113
number: {
114114
component: 'Number',
@@ -128,7 +128,7 @@ export const fieldTypeConditions: {
128128
},
129129
richText: {
130130
component: 'Text',
131-
operators: [...base, like, notLike, contains],
131+
operators: [exists],
132132
},
133133
select: {
134134
component: 'Select',

test/fields/collections/JSON/e2e.spec.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ import { fileURLToPath } from 'url'
88
import type { PayloadTestSDK } from '../../../__helpers/shared/sdk/index.js'
99
import type { Config } from '../../payload-types.js'
1010

11+
import { openListFilters } from '../../../__helpers/e2e/filters/openListFilters.js'
1112
import {
1213
ensureCompilationIsDone,
1314
initPageConsoleErrorCatch,
1415
saveDocAndAssert,
1516
} from '../../../__helpers/e2e/helpers.js'
1617
import { AdminUrlUtil } from '../../../__helpers/shared/adminUrlUtil.js'
17-
import { initPayloadE2ENoConfig } from '../../../__helpers/shared/initPayloadE2ENoConfig.js'
1818
import { reInitializeDB } from '../../../__helpers/shared/clearAndSeed/reInitializeDB.js'
19+
import { initPayloadE2ENoConfig } from '../../../__helpers/shared/initPayloadE2ENoConfig.js'
1920
import { RESTClient } from '../../../__helpers/shared/rest.js'
2021
import { TEST_TIMEOUT_LONG } from '../../../playwright.config.js'
2122
import { jsonFieldsSlug } from '../../slugs.js'
@@ -220,6 +221,32 @@ describe('JSON', () => {
220221
}).toPass()
221222
})
222223

224+
describe('WhereBuilder', () => {
225+
test('should only expose exists operator for json field', async () => {
226+
await page.goto(url.list)
227+
228+
await openListFilters(page, {})
229+
230+
const whereBuilder = page.locator('.where-builder')
231+
await whereBuilder.locator('.where-builder__add-first-filter').click()
232+
233+
const condition = whereBuilder.locator('.where-builder__or-filters > li').first()
234+
235+
// Select the 'json' field
236+
await condition.locator('.condition__field .rs__control').click()
237+
await page
238+
.locator('.rs__option', { hasText: /^json$/i })
239+
.first()
240+
.click()
241+
242+
// Open the operator dropdown and collect all available options
243+
await condition.locator('.condition__operator .rs__control').click()
244+
const operatorOptions = page.locator('.rs__option')
245+
await expect(operatorOptions).toHaveCount(1)
246+
await expect(operatorOptions.first()).toHaveText('exists')
247+
})
248+
})
249+
223250
describe('A11y', () => {
224251
test('Edit view should have no accessibility violations', async ({}, testInfo) => {
225252
await page.goto(url.create)

test/lexical/collections/RichText/e2e.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import path from 'path'
55
import { wait } from 'payload/shared'
66
import { fileURLToPath } from 'url'
77

8+
import { openListFilters } from '../../../__helpers/e2e/filters/openListFilters.js'
89
import {
910
ensureCompilationIsDone,
1011
initPageConsoleErrorCatch,
@@ -16,6 +17,7 @@ import { AdminUrlUtil } from '../../../__helpers/shared/adminUrlUtil.js'
1617
import { reInitializeDB } from '../../../__helpers/shared/clearAndSeed/reInitializeDB.js'
1718
import { initPayloadE2ENoConfig } from '../../../__helpers/shared/initPayloadE2ENoConfig.js'
1819
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../../../playwright.config.js'
20+
import { richTextFieldsSlug } from '../../slugs.js'
1921

2022
const filename = fileURLToPath(import.meta.url)
2123
const currentFolder = path.dirname(filename)
@@ -429,4 +431,31 @@ describe('Rich Text', () => {
429431
expect(richTextValue).toContain('Rich text')
430432
})
431433
})
434+
435+
describe('WhereBuilder', () => {
436+
test('should only expose exists operator for richText field', async () => {
437+
const url = new AdminUrlUtil(serverURL, richTextFieldsSlug)
438+
await page.goto(url.list)
439+
440+
await openListFilters(page, {})
441+
442+
const whereBuilder = page.locator('.where-builder')
443+
await whereBuilder.locator('.where-builder__add-first-filter').click()
444+
445+
const condition = whereBuilder.locator('.where-builder__or-filters > li').first()
446+
447+
// Select the 'richText' field
448+
await condition.locator('.condition__field .rs__control').click()
449+
await page
450+
.locator('.rs__option', { hasText: /^Rich Text$/ })
451+
.first()
452+
.click()
453+
454+
// Open the operator dropdown and collect all available options
455+
await condition.locator('.condition__operator .rs__control').click()
456+
const operatorOptions = page.locator('.rs__option')
457+
await expect(operatorOptions).toHaveCount(1)
458+
await expect(operatorOptions.first()).toHaveText('exists')
459+
})
460+
})
432461
})

0 commit comments

Comments
 (0)