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:
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<ServerOnlyUploadProperties>[] = [
|
||||
|
||||
const serverOnlyCollectionAdminProperties: Partial<ServerOnlyCollectionAdminProperties>[] = [
|
||||
'hidden',
|
||||
'baseListFilter',
|
||||
// 'preview' is handled separately
|
||||
// `livePreview` is handled separately
|
||||
]
|
||||
|
||||
@@ -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<null | Where> | Where
|
||||
|
||||
export type CollectionAdminOptions = {
|
||||
baseListFilter?: BaseListFilter
|
||||
/**
|
||||
* Custom admin components
|
||||
*/
|
||||
|
||||
@@ -804,6 +804,7 @@ export type {
|
||||
AfterRefreshHook as CollectionAfterRefreshHook,
|
||||
AuthCollection,
|
||||
AuthOperationsFromCollectionSlug,
|
||||
BaseListFilter,
|
||||
BeforeChangeHook as CollectionBeforeChangeHook,
|
||||
BeforeDeleteHook as CollectionBeforeDeleteHook,
|
||||
BeforeLoginHook as CollectionBeforeLoginHook,
|
||||
|
||||
19
test/admin/collections/BaseListFilter.ts
Normal file
19
test/admin/collections/BaseListFilter.ts
Normal 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',
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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<false> | CustomIdTabSelect<true>;
|
||||
customIdRow: CustomIdRowSelect<false> | CustomIdRowSelect<true>;
|
||||
'disable-duplicate': DisableDuplicateSelect<false> | DisableDuplicateSelect<true>;
|
||||
'base-list-filters': BaseListFiltersSelect<false> | BaseListFiltersSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
@@ -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<T extends boolean = true> {
|
||||
updatedAt?: 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
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user