feat: adds ability to define base filter for list view (#9177)

Adds the ability to define base list view filters, which is super
helpful when you're doing multi-tenant things in Payload.
This commit is contained in:
James Mikrut
2024-11-13 13:34:01 -05:00
committed by GitHub
parent f4d526d6e5
commit 9da85430a5
10 changed files with 105 additions and 2 deletions

View File

@@ -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). | | **`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). | | **`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). | | **`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 ### Custom Components

View File

@@ -117,7 +117,7 @@ export const renderListView = async (
const page = isNumber(query?.page) ? Number(query.page) : 0 const page = isNumber(query?.page) ? Number(query.page) : 0
const whereQuery = mergeListSearchAndWhere({ let whereQuery = mergeListSearchAndWhere({
collectionConfig, collectionConfig,
search: typeof query?.search === 'string' ? query.search : undefined, search: typeof query?.search === 'string' ? query.search : undefined,
where: (query?.where as Where) || undefined, where: (query?.where as Where) || undefined,
@@ -135,6 +135,21 @@ export const renderListView = async (
? collectionConfig.defaultSort ? collectionConfig.defaultSort
: undefined) : 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({ const data = await payload.find({
collection: collectionSlug, collection: collectionSlug,
depth: 0, depth: 0,

View File

@@ -20,7 +20,7 @@ export type ServerOnlyCollectionProperties = keyof Pick<
export type ServerOnlyCollectionAdminProperties = keyof Pick< export type ServerOnlyCollectionAdminProperties = keyof Pick<
SanitizedCollectionConfig['admin'], SanitizedCollectionConfig['admin'],
'hidden' 'baseListFilter' | 'hidden'
> >
export type ServerOnlyUploadProperties = keyof Pick< export type ServerOnlyUploadProperties = keyof Pick<
@@ -75,6 +75,7 @@ const serverOnlyUploadProperties: Partial<ServerOnlyUploadProperties>[] = [
const serverOnlyCollectionAdminProperties: Partial<ServerOnlyCollectionAdminProperties>[] = [ const serverOnlyCollectionAdminProperties: Partial<ServerOnlyCollectionAdminProperties>[] = [
'hidden', 'hidden',
'baseListFilter',
// 'preview' is handled separately // 'preview' is handled separately
// `livePreview` is handled separately // `livePreview` is handled separately
] ]

View File

@@ -39,12 +39,14 @@ import type {
TypedAuthOperations, TypedAuthOperations,
TypedCollection, TypedCollection,
TypedCollectionSelect, TypedCollectionSelect,
TypedLocale,
} from '../../index.js' } from '../../index.js'
import type { import type {
PayloadRequest, PayloadRequest,
SelectType, SelectType,
Sort, Sort,
TransformCollectionWithSelect, TransformCollectionWithSelect,
Where,
} from '../../types/index.js' } from '../../types/index.js'
import type { SanitizedUploadConfig, UploadConfig } from '../../uploads/types.js' import type { SanitizedUploadConfig, UploadConfig } from '../../uploads/types.js'
import type { import type {
@@ -253,7 +255,16 @@ export type AfterForgotPasswordHook = (args: {
context: RequestContext context: RequestContext
}) => any }) => any
export type BaseListFilter = (args: {
limit: number
locale?: TypedLocale
page: number
req: PayloadRequest
sort: string
}) => null | Promise<null | Where> | Where
export type CollectionAdminOptions = { export type CollectionAdminOptions = {
baseListFilter?: BaseListFilter
/** /**
* Custom admin components * Custom admin components
*/ */

View File

@@ -804,6 +804,7 @@ export type {
AfterRefreshHook as CollectionAfterRefreshHook, AfterRefreshHook as CollectionAfterRefreshHook,
AuthCollection, AuthCollection,
AuthOperationsFromCollectionSlug, AuthOperationsFromCollectionSlug,
BaseListFilter,
BeforeChangeHook as CollectionBeforeChangeHook, BeforeChangeHook as CollectionBeforeChangeHook,
BeforeDeleteHook as CollectionBeforeDeleteHook, BeforeDeleteHook as CollectionBeforeDeleteHook,
BeforeLoginHook as CollectionBeforeLoginHook, BeforeLoginHook as CollectionBeforeLoginHook,

View File

@@ -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',
},
],
}

View File

@@ -3,6 +3,7 @@ import path from 'path'
const filename = fileURLToPath(import.meta.url) const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename) const dirname = path.dirname(filename)
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { BaseListFilter } from './collections/BaseListFilter.js'
import { CustomFields } from './collections/CustomFields/index.js' import { CustomFields } from './collections/CustomFields/index.js'
import { CustomIdRow } from './collections/CustomIdRow.js' import { CustomIdRow } from './collections/CustomIdRow.js'
import { CustomIdTab } from './collections/CustomIdTab.js' import { CustomIdTab } from './collections/CustomIdTab.js'
@@ -154,6 +155,7 @@ export default buildConfigWithDefaults({
CustomIdTab, CustomIdTab,
CustomIdRow, CustomIdRow,
DisableDuplicate, DisableDuplicate,
BaseListFilter,
], ],
globals: [ globals: [
GlobalHidden, GlobalHidden,

View File

@@ -43,6 +43,7 @@ describe('admin2', () => {
let page: Page let page: Page
let geoUrl: AdminUrlUtil let geoUrl: AdminUrlUtil
let postsUrl: AdminUrlUtil let postsUrl: AdminUrlUtil
let baseListFiltersUrl: AdminUrlUtil
let customViewsUrl: AdminUrlUtil let customViewsUrl: AdminUrlUtil
let serverURL: string let serverURL: string
@@ -61,6 +62,7 @@ describe('admin2', () => {
geoUrl = new AdminUrlUtil(serverURL, geoCollectionSlug) geoUrl = new AdminUrlUtil(serverURL, geoCollectionSlug)
postsUrl = new AdminUrlUtil(serverURL, postsCollectionSlug) postsUrl = new AdminUrlUtil(serverURL, postsCollectionSlug)
baseListFiltersUrl = new AdminUrlUtil(serverURL, 'base-list-filters')
customViewsUrl = new AdminUrlUtil(serverURL, customViews1CollectionSlug) customViewsUrl = new AdminUrlUtil(serverURL, customViews1CollectionSlug)
const context = await browser.newContext() const context = await browser.newContext()
@@ -777,6 +779,14 @@ describe('admin2', () => {
).toHaveText('Title') ).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)
})
})
}) })
}) })

View File

@@ -27,6 +27,7 @@ export interface Config {
customIdTab: CustomIdTab; customIdTab: CustomIdTab;
customIdRow: CustomIdRow; customIdRow: CustomIdRow;
'disable-duplicate': DisableDuplicate; 'disable-duplicate': DisableDuplicate;
'base-list-filters': BaseListFilter;
'payload-locked-documents': PayloadLockedDocument; 'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference; 'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration; 'payload-migrations': PayloadMigration;
@@ -49,6 +50,7 @@ export interface Config {
customIdTab: CustomIdTabSelect<false> | CustomIdTabSelect<true>; customIdTab: CustomIdTabSelect<false> | CustomIdTabSelect<true>;
customIdRow: CustomIdRowSelect<false> | CustomIdRowSelect<true>; customIdRow: CustomIdRowSelect<false> | CustomIdRowSelect<true>;
'disable-duplicate': DisableDuplicateSelect<false> | DisableDuplicateSelect<true>; 'disable-duplicate': DisableDuplicateSelect<false> | DisableDuplicateSelect<true>;
'base-list-filters': BaseListFiltersSelect<false> | BaseListFiltersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>; 'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>; 'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>; 'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
@@ -307,6 +309,16 @@ export interface DisableDuplicate {
updatedAt: string; updatedAt: string;
createdAt: 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 * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents". * via the `definition` "payload-locked-documents".
@@ -377,6 +389,10 @@ export interface PayloadLockedDocument {
| ({ | ({
relationTo: 'disable-duplicate'; relationTo: 'disable-duplicate';
value: string | DisableDuplicate; value: string | DisableDuplicate;
} | null)
| ({
relationTo: 'base-list-filters';
value: string | BaseListFilter;
} | null); } | null);
globalSlug?: string | null; globalSlug?: string | null;
user: { user: {
@@ -604,6 +620,15 @@ export interface DisableDuplicateSelect<T extends boolean = true> {
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "base-list-filters_select".
*/
export interface BaseListFiltersSelect<T extends boolean = true> {
title?: T;
updatedAt?: T;
createdAt?: T;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select". * via the `definition` "payload-locked-documents_select".

View File

@@ -27,6 +27,24 @@ export const seed = async (_payload) => {
depth: 0, depth: 0,
overrideAccess: true, 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 () => { ...[...Array(11)].map((_, i) => async () => {
const postDoc = await _payload.create({ const postDoc = await _payload.create({
collection: postsCollectionSlug, collection: postsCollectionSlug,