diff --git a/docs/admin/collections.mdx b/docs/admin/collections.mdx index 6a82ad1651..00c7bedba2 100644 --- a/docs/admin/collections.mdx +++ b/docs/admin/collections.mdx @@ -42,6 +42,7 @@ The following options are available: | **`components`** | Swap in your own React components to be used within this Collection. [More details](#custom-components). | | **`listSearchableFields`** | Specify which fields should be searched in the List search view. [More details](#list-searchable-fields). | | **`pagination`** | Set pagination-specific options for this Collection. [More details](#pagination). | +| **`baseListFilter`** | You can define a default base filter for this collection's List view, which will be merged into any filters that the user performs. | ### Custom Components diff --git a/packages/next/src/views/List/index.tsx b/packages/next/src/views/List/index.tsx index 9e2219c605..af1e378f47 100644 --- a/packages/next/src/views/List/index.tsx +++ b/packages/next/src/views/List/index.tsx @@ -117,7 +117,7 @@ export const renderListView = async ( const page = isNumber(query?.page) ? Number(query.page) : 0 - const whereQuery = mergeListSearchAndWhere({ + let whereQuery = mergeListSearchAndWhere({ collectionConfig, search: typeof query?.search === 'string' ? query.search : undefined, where: (query?.where as Where) || undefined, @@ -135,6 +135,21 @@ export const renderListView = async ( ? collectionConfig.defaultSort : undefined) + if (typeof collectionConfig.admin?.baseListFilter === 'function') { + const baseListFilter = await collectionConfig.admin.baseListFilter({ + limit, + page, + req, + sort, + }) + + if (baseListFilter) { + whereQuery = { + and: [whereQuery, baseListFilter].filter(Boolean), + } + } + } + const data = await payload.find({ collection: collectionSlug, depth: 0, diff --git a/packages/payload/src/collections/config/client.ts b/packages/payload/src/collections/config/client.ts index ea67047c62..d835ab6600 100644 --- a/packages/payload/src/collections/config/client.ts +++ b/packages/payload/src/collections/config/client.ts @@ -20,7 +20,7 @@ export type ServerOnlyCollectionProperties = keyof Pick< export type ServerOnlyCollectionAdminProperties = keyof Pick< SanitizedCollectionConfig['admin'], - 'hidden' + 'baseListFilter' | 'hidden' > export type ServerOnlyUploadProperties = keyof Pick< @@ -75,6 +75,7 @@ const serverOnlyUploadProperties: Partial[] = [ const serverOnlyCollectionAdminProperties: Partial[] = [ 'hidden', + 'baseListFilter', // 'preview' is handled separately // `livePreview` is handled separately ] diff --git a/packages/payload/src/collections/config/types.ts b/packages/payload/src/collections/config/types.ts index b51e636306..1b5d4cf3a6 100644 --- a/packages/payload/src/collections/config/types.ts +++ b/packages/payload/src/collections/config/types.ts @@ -39,12 +39,14 @@ import type { TypedAuthOperations, TypedCollection, TypedCollectionSelect, + TypedLocale, } from '../../index.js' import type { PayloadRequest, SelectType, Sort, TransformCollectionWithSelect, + Where, } from '../../types/index.js' import type { SanitizedUploadConfig, UploadConfig } from '../../uploads/types.js' import type { @@ -253,7 +255,16 @@ export type AfterForgotPasswordHook = (args: { context: RequestContext }) => any +export type BaseListFilter = (args: { + limit: number + locale?: TypedLocale + page: number + req: PayloadRequest + sort: string +}) => null | Promise | Where + export type CollectionAdminOptions = { + baseListFilter?: BaseListFilter /** * Custom admin components */ diff --git a/packages/payload/src/index.ts b/packages/payload/src/index.ts index dfeed2003b..908e040135 100644 --- a/packages/payload/src/index.ts +++ b/packages/payload/src/index.ts @@ -804,6 +804,7 @@ export type { AfterRefreshHook as CollectionAfterRefreshHook, AuthCollection, AuthOperationsFromCollectionSlug, + BaseListFilter, BeforeChangeHook as CollectionBeforeChangeHook, BeforeDeleteHook as CollectionBeforeDeleteHook, BeforeLoginHook as CollectionBeforeLoginHook, diff --git a/test/admin/collections/BaseListFilter.ts b/test/admin/collections/BaseListFilter.ts new file mode 100644 index 0000000000..769cdce644 --- /dev/null +++ b/test/admin/collections/BaseListFilter.ts @@ -0,0 +1,19 @@ +import type { CollectionConfig } from 'payload' + +export const BaseListFilter: CollectionConfig = { + slug: 'base-list-filters', + admin: { + baseListFilter: () => ({ + title: { + not_equals: 'hide me', + }, + }), + useAsTitle: 'title', + }, + fields: [ + { + name: 'title', + type: 'text', + }, + ], +} diff --git a/test/admin/config.ts b/test/admin/config.ts index a771ed9574..85929030f1 100644 --- a/test/admin/config.ts +++ b/test/admin/config.ts @@ -3,6 +3,7 @@ import path from 'path' const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' +import { BaseListFilter } from './collections/BaseListFilter.js' import { CustomFields } from './collections/CustomFields/index.js' import { CustomIdRow } from './collections/CustomIdRow.js' import { CustomIdTab } from './collections/CustomIdTab.js' @@ -154,6 +155,7 @@ export default buildConfigWithDefaults({ CustomIdTab, CustomIdRow, DisableDuplicate, + BaseListFilter, ], globals: [ GlobalHidden, diff --git a/test/admin/e2e/2/e2e.spec.ts b/test/admin/e2e/2/e2e.spec.ts index 73e2631920..b0b7edbc96 100644 --- a/test/admin/e2e/2/e2e.spec.ts +++ b/test/admin/e2e/2/e2e.spec.ts @@ -43,6 +43,7 @@ describe('admin2', () => { let page: Page let geoUrl: AdminUrlUtil let postsUrl: AdminUrlUtil + let baseListFiltersUrl: AdminUrlUtil let customViewsUrl: AdminUrlUtil let serverURL: string @@ -61,6 +62,7 @@ describe('admin2', () => { geoUrl = new AdminUrlUtil(serverURL, geoCollectionSlug) postsUrl = new AdminUrlUtil(serverURL, postsCollectionSlug) + baseListFiltersUrl = new AdminUrlUtil(serverURL, 'base-list-filters') customViewsUrl = new AdminUrlUtil(serverURL, customViews1CollectionSlug) const context = await browser.newContext() @@ -777,6 +779,14 @@ describe('admin2', () => { ).toHaveText('Title') }) }) + + describe('base list filters', () => { + test('should respect base list filters', async () => { + await page.goto(baseListFiltersUrl.list) + await page.waitForURL((url) => url.toString().startsWith(baseListFiltersUrl.list)) + await expect(page.locator(tableRowLocator)).toHaveCount(1) + }) + }) }) }) diff --git a/test/admin/payload-types.ts b/test/admin/payload-types.ts index 45f424fcb9..577f229749 100644 --- a/test/admin/payload-types.ts +++ b/test/admin/payload-types.ts @@ -27,6 +27,7 @@ export interface Config { customIdTab: CustomIdTab; customIdRow: CustomIdRow; 'disable-duplicate': DisableDuplicate; + 'base-list-filters': BaseListFilter; 'payload-locked-documents': PayloadLockedDocument; 'payload-preferences': PayloadPreference; 'payload-migrations': PayloadMigration; @@ -49,6 +50,7 @@ export interface Config { customIdTab: CustomIdTabSelect | CustomIdTabSelect; customIdRow: CustomIdRowSelect | CustomIdRowSelect; 'disable-duplicate': DisableDuplicateSelect | DisableDuplicateSelect; + 'base-list-filters': BaseListFiltersSelect | BaseListFiltersSelect; 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect; 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; @@ -307,6 +309,16 @@ export interface DisableDuplicate { updatedAt: string; createdAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "base-list-filters". + */ +export interface BaseListFilter { + id: string; + title?: string | null; + updatedAt: string; + createdAt: string; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "payload-locked-documents". @@ -377,6 +389,10 @@ export interface PayloadLockedDocument { | ({ relationTo: 'disable-duplicate'; value: string | DisableDuplicate; + } | null) + | ({ + relationTo: 'base-list-filters'; + value: string | BaseListFilter; } | null); globalSlug?: string | null; user: { @@ -604,6 +620,15 @@ export interface DisableDuplicateSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "base-list-filters_select". + */ +export interface BaseListFiltersSelect { + title?: T; + updatedAt?: T; + createdAt?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "payload-locked-documents_select". diff --git a/test/admin/seed.ts b/test/admin/seed.ts index f0a732606d..d01bc51a71 100644 --- a/test/admin/seed.ts +++ b/test/admin/seed.ts @@ -27,6 +27,24 @@ export const seed = async (_payload) => { depth: 0, overrideAccess: true, }), + () => + _payload.create({ + collection: 'base-list-filters', + data: { + title: 'show me', + }, + depth: 0, + overrideAccess: true, + }), + () => + _payload.create({ + collection: 'base-list-filters', + data: { + title: 'hide me', + }, + depth: 0, + overrideAccess: true, + }), ...[...Array(11)].map((_, i) => async () => { const postDoc = await _payload.create({ collection: postsCollectionSlug,