diff --git a/packages/ui/src/elements/QueryPresets/cells/WhereCell/index.tsx b/packages/ui/src/elements/QueryPresets/cells/WhereCell/index.tsx index d06f81d9c95..ce179a8e2e9 100644 --- a/packages/ui/src/elements/QueryPresets/cells/WhereCell/index.tsx +++ b/packages/ui/src/elements/QueryPresets/cells/WhereCell/index.tsx @@ -9,13 +9,13 @@ const transformWhereToNaturalLanguage = (where: Where): string => { const orQuery = where.or[0] const andQuery = orQuery?.and?.[0] - if (!andQuery) { + if (!andQuery || typeof andQuery !== 'object') { return 'No where query' } const key = Object.keys(andQuery)[0] - if (!andQuery[key]) { + if (!key || !andQuery[key] || typeof andQuery[key] !== 'object') { return 'No where query' } diff --git a/packages/ui/src/elements/QueryPresets/fields/WhereField/index.tsx b/packages/ui/src/elements/QueryPresets/fields/WhereField/index.tsx index 0c3bb2a873e..a8b1cc7cf23 100644 --- a/packages/ui/src/elements/QueryPresets/fields/WhereField/index.tsx +++ b/packages/ui/src/elements/QueryPresets/fields/WhereField/index.tsx @@ -23,9 +23,13 @@ const transformWhereToNaturalLanguage = ( } const renderCondition = (condition: any): React.ReactNode => { + if (!condition || typeof condition !== 'object') { + return 'No where query' + } + const key = Object.keys(condition)[0] - if (!condition[key]) { + if (!key || !condition[key] || typeof condition[key] !== 'object') { return 'No where query' } diff --git a/packages/ui/src/providers/ListQuery/sanitizeQuery.ts b/packages/ui/src/providers/ListQuery/sanitizeQuery.ts index 551ddf459e4..fcaa4fc1d3d 100644 --- a/packages/ui/src/providers/ListQuery/sanitizeQuery.ts +++ b/packages/ui/src/providers/ListQuery/sanitizeQuery.ts @@ -16,7 +16,12 @@ export const sanitizeQuery = (toSanitize: ListQuery): ListQuery => { delete sanitized[key] } - if (key === 'where' && typeof value === 'object' && !Object.keys(value as Where).length) { + if ( + key === 'where' && + typeof value === 'object' && + value !== null && + !Object.keys(value as Where).length + ) { delete sanitized[key] } diff --git a/test/query-presets/e2e.spec.ts b/test/query-presets/e2e.spec.ts index 082650906b0..f4f44b923bc 100644 --- a/test/query-presets/e2e.spec.ts +++ b/test/query-presets/e2e.spec.ts @@ -149,6 +149,65 @@ describe('Query Presets', () => { } }) + test('can create and view preset with no filters or columns', async () => { + await page.goto(pagesUrl.list) + + const presetTitle = 'Empty Preset' + + // Create a new preset without setting any filters or columns + await page.locator('#create-new-preset').click() + const modal = page.locator('[id^=doc-drawer_payload-query-presets_0_]') + await expect(modal).toBeVisible() + await modal.locator('input[name="title"]').fill(presetTitle) + + const currentURL = page.url() + await saveDocAndAssert(page) + await expect(modal).toBeHidden() + + await page.waitForURL(() => page.url() !== currentURL) + + await expect( + page.locator('button#select-preset', { + hasText: exactText(presetTitle), + }), + ).toBeVisible() + + // Open the edit modal to verify where/columns fields handle null values + await page.locator('#edit-preset').click() + const editModal = page.locator('[id^=doc-drawer_payload-query-presets_0_]') + await expect(editModal).toBeVisible() + + // Verify the Where field displays "No where query" instead of crashing + const whereFieldContent = editModal.locator('.query-preset-where-field .value-wrapper') + await expect(whereFieldContent).toBeVisible() + await expect(whereFieldContent).toContainText('No where query') + + // Verify the Columns field displays "No columns selected" instead of crashing + const columnsFieldContent = editModal.locator('.query-preset-columns-field .value-wrapper') + await expect(columnsFieldContent).toBeVisible() + await expect(columnsFieldContent).toContainText('No columns selected') + + await editModal.locator('button.doc-drawer__header-close').click() + await expect(editModal).toBeHidden() + + await openQueryPresetDrawer({ page }) + const drawer = page.locator('[id^=list-drawer_0_]') + await expect(drawer).toBeVisible() + + const presetRow = drawer.locator('tbody tr', { + has: page.locator(`button:has-text("${presetTitle}")`), + }) + + await expect(presetRow).toBeVisible() + + // Column order: title (0), isShared (1), access (2), where (3), columns (4) + const whereCell = presetRow.locator('td').nth(3) + await expect(whereCell).toContainText('No where query') + + const columnsCell = presetRow.locator('td').nth(4) + await expect(columnsCell).toContainText('No columns selected') + }) + test('should select preset and apply filters', async () => { await page.goto(pagesUrl.list) diff --git a/test/query-presets/int.spec.ts b/test/query-presets/int.spec.ts index c157f70f513..a62d08febbc 100644 --- a/test/query-presets/int.spec.ts +++ b/test/query-presets/int.spec.ts @@ -803,5 +803,43 @@ describe('Query Presets', () => { ], }) }) + + it('should handle empty where and columns fields', async () => { + const result = await payload.create({ + collection: queryPresetsCollectionSlug, + user: adminUser, + overrideAccess: false, + data: { + title: 'Empty Where and Columns', + // Not including where or columns at all + access: { + read: { + constraint: 'everyone', + }, + update: { + constraint: 'everyone', + }, + delete: { + constraint: 'everyone', + }, + }, + relatedCollection: 'pages', + }, + }) + + expect(result.where == null).toBe(true) + expect(result.columns == null).toBe(true) + + const fetched = await payload.findByID({ + collection: queryPresetsCollectionSlug, + depth: 0, + user: adminUser, + overrideAccess: false, + id: result.id, + }) + + expect(fetched.where == null).toBe(true) + expect(fetched.columns == null).toBe(true) + }) }) }) diff --git a/test/query-presets/payload-types.ts b/test/query-presets/payload-types.ts index 53bc703e785..bf4fc79de79 100644 --- a/test/query-presets/payload-types.ts +++ b/test/query-presets/payload-types.ts @@ -70,6 +70,7 @@ export interface Config { pages: Page; posts: Post; users: User; + 'payload-kv': PayloadKv; 'payload-locked-documents': PayloadLockedDocument; 'payload-preferences': PayloadPreference; 'payload-migrations': PayloadMigration; @@ -80,6 +81,7 @@ export interface Config { pages: PagesSelect | PagesSelect; posts: PostsSelect | PostsSelect; users: UsersSelect | UsersSelect; + 'payload-kv': PayloadKvSelect | PayloadKvSelect; 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect; 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; @@ -88,6 +90,7 @@ export interface Config { db: { defaultIDType: string; }; + fallbackLocale: null; globals: {}; globalsSelect: {}; locale: null; @@ -164,25 +167,33 @@ export interface User { | null; password?: string | null; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-kv". + */ +export interface PayloadKv { + id: string; + key: string; + data: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "payload-locked-documents". */ export interface PayloadLockedDocument { id: string; - document?: - | ({ - relationTo: 'pages'; - value: string | Page; - } | null) - | ({ - relationTo: 'posts'; - value: string | Post; - } | null) - | ({ - relationTo: 'users'; - value: string | User; - } | null); + document?: { + relationTo: 'users'; + value: string | User; + } | null; globalSlug?: string | null; user: { relationTo: 'users'; @@ -318,6 +329,14 @@ export interface UsersSelect { expiresAt?: T; }; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-kv_select". + */ +export interface PayloadKvSelect { + key?: T; + data?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "payload-locked-documents_select".