From 4fc2eec301c10661938ba3b6c952aa62116441ec Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Tue, 25 Mar 2025 23:45:03 -0400 Subject: [PATCH] fix(ui): query presets are available for unrelated collections (#11872) When selecting query presets from the list drawer, all query presets are available for selection, even if unrelated to the underlying collection. When selecting one of these presets, the list view will crash with client-side exceptions because the columns and filters that are applied are incompatible. The fix is to the thread `filterOptions` through the query presets drawer. This will ensure that only related collections are shown. --- .../elements/ListControls/useQueryPresets.tsx | 13 +++++ test/query-presets/collections/Posts/index.ts | 21 +++++++ test/query-presets/config.ts | 3 +- test/query-presets/e2e.spec.ts | 15 +++++ .../helpers/openQueryPresetDrawer.ts | 10 +++- test/query-presets/payload-types.ts | 57 ++++++++++++++----- test/query-presets/slugs.ts | 4 +- tsconfig.base.json | 2 +- 8 files changed, 105 insertions(+), 20 deletions(-) create mode 100644 test/query-presets/collections/Posts/index.ts diff --git a/packages/ui/src/elements/ListControls/useQueryPresets.tsx b/packages/ui/src/elements/ListControls/useQueryPresets.tsx index cafc1d041d..c6ec9ca53d 100644 --- a/packages/ui/src/elements/ListControls/useQueryPresets.tsx +++ b/packages/ui/src/elements/ListControls/useQueryPresets.tsx @@ -65,9 +65,22 @@ export const useQueryPresets = ({ collectionSlug: queryPresetsSlug, }) + const filterOptions = useMemo( + () => ({ + 'payload-query-presets': { + relatedCollection: { + equals: collectionSlug, + }, + }, + }), + [collectionSlug], + ) + const [ListDrawer, , { closeDrawer: closeListDrawer, openDrawer: openListDrawer }] = useListDrawer({ collectionSlugs: [queryPresetsSlug], + filterOptions, + selectedCollection: queryPresetsSlug, }) const handlePresetChange = useCallback( diff --git a/test/query-presets/collections/Posts/index.ts b/test/query-presets/collections/Posts/index.ts new file mode 100644 index 0000000000..84aec17300 --- /dev/null +++ b/test/query-presets/collections/Posts/index.ts @@ -0,0 +1,21 @@ +import type { CollectionConfig } from 'payload' + +import { postsSlug } from '../../slugs.js' + +export const Posts: CollectionConfig = { + slug: postsSlug, + admin: { + useAsTitle: 'text', + }, + enableQueryPresets: true, + lockDocuments: false, + fields: [ + { + name: 'text', + type: 'text', + }, + ], + versions: { + drafts: true, + }, +} diff --git a/test/query-presets/config.ts b/test/query-presets/config.ts index bf0e4d4e05..2a786ef4af 100644 --- a/test/query-presets/config.ts +++ b/test/query-presets/config.ts @@ -3,6 +3,7 @@ import path from 'path' import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' import { Pages } from './collections/Pages/index.js' +import { Posts } from './collections/Posts/index.js' import { Users } from './collections/Users/index.js' import { roles } from './fields/roles.js' import { seed } from './seed.js' @@ -54,7 +55,7 @@ export default buildConfigWithDefaults({ ], }, }, - collections: [Pages, Users], + collections: [Pages, Users, Posts], onInit: async (payload) => { if (process.env.SEED_IN_CONFIG_ONINIT !== 'false') { await seed(payload) diff --git a/test/query-presets/e2e.spec.ts b/test/query-presets/e2e.spec.ts index 30c19de3d4..6f2cc43805 100644 --- a/test/query-presets/e2e.spec.ts +++ b/test/query-presets/e2e.spec.ts @@ -389,4 +389,19 @@ describe('Query Presets', () => { }), ).toBeVisible() }) + + test('only shows query presets related to the underlying collection', async () => { + // no results on `users` collection + const postsUrl = new AdminUrlUtil(serverURL, 'posts') + await page.goto(postsUrl.list) + const drawer = await openQueryPresetDrawer({ page }) + await expect(drawer.locator('.table table > tbody > tr')).toHaveCount(0) + await expect(drawer.locator('.collection-list__no-results')).toBeVisible() + + // results on `pages` collection + await page.goto(pagesUrl.list) + await openQueryPresetDrawer({ page }) + await expect(drawer.locator('.table table > tbody > tr')).toHaveCount(3) + await drawer.locator('.collection-list__no-results').isHidden() + }) }) diff --git a/test/query-presets/helpers/openQueryPresetDrawer.ts b/test/query-presets/helpers/openQueryPresetDrawer.ts index e9c5db1f1a..fa3747ff11 100644 --- a/test/query-presets/helpers/openQueryPresetDrawer.ts +++ b/test/query-presets/helpers/openQueryPresetDrawer.ts @@ -1,5 +1,11 @@ -import type { Page } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' -export async function openQueryPresetDrawer({ page }: { page: Page }) { +import { expect } from '@playwright/test' + +export async function openQueryPresetDrawer({ page }: { page: Page }): Promise { await page.click('button#select-preset') + const drawer = page.locator('dialog[id^="list-drawer_0_"]') + await expect(drawer).toBeVisible() + await expect(drawer.locator('.collection-list--payload-query-presets')).toBeVisible() + return drawer } diff --git a/test/query-presets/payload-types.ts b/test/query-presets/payload-types.ts index 20fbd0a7b8..2f6ee94ee9 100644 --- a/test/query-presets/payload-types.ts +++ b/test/query-presets/payload-types.ts @@ -69,6 +69,7 @@ export interface Config { collections: { pages: Page; users: User; + posts: Post; 'payload-locked-documents': PayloadLockedDocument; 'payload-preferences': PayloadPreference; 'payload-migrations': PayloadMigration; @@ -78,13 +79,14 @@ export interface Config { collectionsSelect: { pages: PagesSelect | PagesSelect; users: UsersSelect | UsersSelect; + posts: PostsSelect | PostsSelect; 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect; 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; 'payload-query-presets': PayloadQueryPresetsSelect | PayloadQueryPresetsSelect; }; db: { - defaultIDType: number; + defaultIDType: string; }; globals: {}; globalsSelect: {}; @@ -120,7 +122,7 @@ export interface UserAuthOperations { * via the `definition` "pages". */ export interface Page { - id: number; + id: string; text?: string | null; updatedAt: string; createdAt: string; @@ -131,7 +133,7 @@ export interface Page { * via the `definition` "users". */ export interface User { - id: number; + id: string; name?: string | null; roles?: ('admin' | 'user' | 'anonymous')[] | null; updatedAt: string; @@ -145,25 +147,40 @@ export interface User { lockUntil?: string | null; password?: string | null; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "posts". + */ +export interface Post { + id: string; + text?: string | null; + updatedAt: string; + createdAt: string; + _status?: ('draft' | 'published') | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "payload-locked-documents". */ export interface PayloadLockedDocument { - id: number; + id: string; document?: | ({ relationTo: 'pages'; - value: number | Page; + value: string | Page; } | null) | ({ relationTo: 'users'; - value: number | User; + value: string | User; + } | null) + | ({ + relationTo: 'posts'; + value: string | Post; } | null); globalSlug?: string | null; user: { relationTo: 'users'; - value: number | User; + value: string | User; }; updatedAt: string; createdAt: string; @@ -173,10 +190,10 @@ export interface PayloadLockedDocument { * via the `definition` "payload-preferences". */ export interface PayloadPreference { - id: number; + id: string; user: { relationTo: 'users'; - value: number | User; + value: string | User; }; key?: string | null; value?: @@ -196,7 +213,7 @@ export interface PayloadPreference { * via the `definition` "payload-migrations". */ export interface PayloadMigration { - id: number; + id: string; name?: string | null; batch?: number | null; updatedAt: string; @@ -207,23 +224,23 @@ export interface PayloadMigration { * via the `definition` "payload-query-presets". */ export interface PayloadQueryPreset { - id: number; + id: string; title: string; isShared?: boolean | null; access?: { read?: { constraint?: ('everyone' | 'onlyMe' | 'specificUsers' | 'specificRoles') | null; - users?: (number | User)[] | null; + users?: (string | User)[] | null; roles?: ('admin' | 'user' | 'anonymous')[] | null; }; update?: { constraint?: ('everyone' | 'onlyMe' | 'specificUsers' | 'specificRoles') | null; - users?: (number | User)[] | null; + users?: (string | User)[] | null; roles?: ('admin' | 'user' | 'anonymous')[] | null; }; delete?: { constraint?: ('everyone' | 'onlyMe' | 'specificUsers') | null; - users?: (number | User)[] | null; + users?: (string | User)[] | null; }; }; where?: @@ -244,7 +261,7 @@ export interface PayloadQueryPreset { | number | boolean | null; - relatedCollection: 'pages'; + relatedCollection: 'pages' | 'posts'; updatedAt: string; createdAt: string; } @@ -275,6 +292,16 @@ export interface UsersSelect { loginAttempts?: T; lockUntil?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "posts_select". + */ +export interface PostsSelect { + text?: T; + updatedAt?: T; + createdAt?: T; + _status?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "payload-locked-documents_select". diff --git a/test/query-presets/slugs.ts b/test/query-presets/slugs.ts index 85c6ec36fd..b7fb1e104c 100644 --- a/test/query-presets/slugs.ts +++ b/test/query-presets/slugs.ts @@ -2,4 +2,6 @@ export const usersSlug = 'users' export const pagesSlug = 'pages' -export const collectionSlugs = [usersSlug, pagesSlug] +export const postsSlug = 'posts' + +export const collectionSlugs = [usersSlug, pagesSlug, postsSlug] diff --git a/tsconfig.base.json b/tsconfig.base.json index c9793d25c6..daa36c7211 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -31,7 +31,7 @@ } ], "paths": { - "@payload-config": ["./test/_community/config.ts"], + "@payload-config": ["./test/query-presets/config.ts"], "@payloadcms/admin-bar": ["./packages/admin-bar/src"], "@payloadcms/live-preview": ["./packages/live-preview/src"], "@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],