Skip to content

Commit 9da8543

Browse files
authored
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.
1 parent f4d526d commit 9da8543

File tree

10 files changed

+105
-2
lines changed

10 files changed

+105
-2
lines changed

docs/admin/collections.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ The following options are available:
4242
| **`components`** | Swap in your own React components to be used within this Collection. [More details](#custom-components). |
4343
| **`listSearchableFields`** | Specify which fields should be searched in the List search view. [More details](#list-searchable-fields). |
4444
| **`pagination`** | Set pagination-specific options for this Collection. [More details](#pagination). |
45+
| **`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. |
4546

4647
### Custom Components
4748

packages/next/src/views/List/index.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export const renderListView = async (
117117

118118
const page = isNumber(query?.page) ? Number(query.page) : 0
119119

120-
const whereQuery = mergeListSearchAndWhere({
120+
let whereQuery = mergeListSearchAndWhere({
121121
collectionConfig,
122122
search: typeof query?.search === 'string' ? query.search : undefined,
123123
where: (query?.where as Where) || undefined,
@@ -135,6 +135,21 @@ export const renderListView = async (
135135
? collectionConfig.defaultSort
136136
: undefined)
137137

138+
if (typeof collectionConfig.admin?.baseListFilter === 'function') {
139+
const baseListFilter = await collectionConfig.admin.baseListFilter({
140+
limit,
141+
page,
142+
req,
143+
sort,
144+
})
145+
146+
if (baseListFilter) {
147+
whereQuery = {
148+
and: [whereQuery, baseListFilter].filter(Boolean),
149+
}
150+
}
151+
}
152+
138153
const data = await payload.find({
139154
collection: collectionSlug,
140155
depth: 0,

packages/payload/src/collections/config/client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export type ServerOnlyCollectionProperties = keyof Pick<
2020

2121
export type ServerOnlyCollectionAdminProperties = keyof Pick<
2222
SanitizedCollectionConfig['admin'],
23-
'hidden'
23+
'baseListFilter' | 'hidden'
2424
>
2525

2626
export type ServerOnlyUploadProperties = keyof Pick<
@@ -75,6 +75,7 @@ const serverOnlyUploadProperties: Partial<ServerOnlyUploadProperties>[] = [
7575

7676
const serverOnlyCollectionAdminProperties: Partial<ServerOnlyCollectionAdminProperties>[] = [
7777
'hidden',
78+
'baseListFilter',
7879
// 'preview' is handled separately
7980
// `livePreview` is handled separately
8081
]

packages/payload/src/collections/config/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,14 @@ import type {
3939
TypedAuthOperations,
4040
TypedCollection,
4141
TypedCollectionSelect,
42+
TypedLocale,
4243
} from '../../index.js'
4344
import type {
4445
PayloadRequest,
4546
SelectType,
4647
Sort,
4748
TransformCollectionWithSelect,
49+
Where,
4850
} from '../../types/index.js'
4951
import type { SanitizedUploadConfig, UploadConfig } from '../../uploads/types.js'
5052
import type {
@@ -253,7 +255,16 @@ export type AfterForgotPasswordHook = (args: {
253255
context: RequestContext
254256
}) => any
255257

258+
export type BaseListFilter = (args: {
259+
limit: number
260+
locale?: TypedLocale
261+
page: number
262+
req: PayloadRequest
263+
sort: string
264+
}) => null | Promise<null | Where> | Where
265+
256266
export type CollectionAdminOptions = {
267+
baseListFilter?: BaseListFilter
257268
/**
258269
* Custom admin components
259270
*/

packages/payload/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,7 @@ export type {
804804
AfterRefreshHook as CollectionAfterRefreshHook,
805805
AuthCollection,
806806
AuthOperationsFromCollectionSlug,
807+
BaseListFilter,
807808
BeforeChangeHook as CollectionBeforeChangeHook,
808809
BeforeDeleteHook as CollectionBeforeDeleteHook,
809810
BeforeLoginHook as CollectionBeforeLoginHook,
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { CollectionConfig } from 'payload'
2+
3+
export const BaseListFilter: CollectionConfig = {
4+
slug: 'base-list-filters',
5+
admin: {
6+
baseListFilter: () => ({
7+
title: {
8+
not_equals: 'hide me',
9+
},
10+
}),
11+
useAsTitle: 'title',
12+
},
13+
fields: [
14+
{
15+
name: 'title',
16+
type: 'text',
17+
},
18+
],
19+
}

test/admin/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from 'path'
33
const filename = fileURLToPath(import.meta.url)
44
const dirname = path.dirname(filename)
55
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
6+
import { BaseListFilter } from './collections/BaseListFilter.js'
67
import { CustomFields } from './collections/CustomFields/index.js'
78
import { CustomIdRow } from './collections/CustomIdRow.js'
89
import { CustomIdTab } from './collections/CustomIdTab.js'
@@ -154,6 +155,7 @@ export default buildConfigWithDefaults({
154155
CustomIdTab,
155156
CustomIdRow,
156157
DisableDuplicate,
158+
BaseListFilter,
157159
],
158160
globals: [
159161
GlobalHidden,

test/admin/e2e/2/e2e.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ describe('admin2', () => {
4343
let page: Page
4444
let geoUrl: AdminUrlUtil
4545
let postsUrl: AdminUrlUtil
46+
let baseListFiltersUrl: AdminUrlUtil
4647
let customViewsUrl: AdminUrlUtil
4748

4849
let serverURL: string
@@ -61,6 +62,7 @@ describe('admin2', () => {
6162

6263
geoUrl = new AdminUrlUtil(serverURL, geoCollectionSlug)
6364
postsUrl = new AdminUrlUtil(serverURL, postsCollectionSlug)
65+
baseListFiltersUrl = new AdminUrlUtil(serverURL, 'base-list-filters')
6466
customViewsUrl = new AdminUrlUtil(serverURL, customViews1CollectionSlug)
6567

6668
const context = await browser.newContext()
@@ -777,6 +779,14 @@ describe('admin2', () => {
777779
).toHaveText('Title')
778780
})
779781
})
782+
783+
describe('base list filters', () => {
784+
test('should respect base list filters', async () => {
785+
await page.goto(baseListFiltersUrl.list)
786+
await page.waitForURL((url) => url.toString().startsWith(baseListFiltersUrl.list))
787+
await expect(page.locator(tableRowLocator)).toHaveCount(1)
788+
})
789+
})
780790
})
781791
})
782792

test/admin/payload-types.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface Config {
2727
customIdTab: CustomIdTab;
2828
customIdRow: CustomIdRow;
2929
'disable-duplicate': DisableDuplicate;
30+
'base-list-filters': BaseListFilter;
3031
'payload-locked-documents': PayloadLockedDocument;
3132
'payload-preferences': PayloadPreference;
3233
'payload-migrations': PayloadMigration;
@@ -49,6 +50,7 @@ export interface Config {
4950
customIdTab: CustomIdTabSelect<false> | CustomIdTabSelect<true>;
5051
customIdRow: CustomIdRowSelect<false> | CustomIdRowSelect<true>;
5152
'disable-duplicate': DisableDuplicateSelect<false> | DisableDuplicateSelect<true>;
53+
'base-list-filters': BaseListFiltersSelect<false> | BaseListFiltersSelect<true>;
5254
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
5355
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
5456
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
@@ -307,6 +309,16 @@ export interface DisableDuplicate {
307309
updatedAt: string;
308310
createdAt: string;
309311
}
312+
/**
313+
* This interface was referenced by `Config`'s JSON-Schema
314+
* via the `definition` "base-list-filters".
315+
*/
316+
export interface BaseListFilter {
317+
id: string;
318+
title?: string | null;
319+
updatedAt: string;
320+
createdAt: string;
321+
}
310322
/**
311323
* This interface was referenced by `Config`'s JSON-Schema
312324
* via the `definition` "payload-locked-documents".
@@ -377,6 +389,10 @@ export interface PayloadLockedDocument {
377389
| ({
378390
relationTo: 'disable-duplicate';
379391
value: string | DisableDuplicate;
392+
} | null)
393+
| ({
394+
relationTo: 'base-list-filters';
395+
value: string | BaseListFilter;
380396
} | null);
381397
globalSlug?: string | null;
382398
user: {
@@ -604,6 +620,15 @@ export interface DisableDuplicateSelect<T extends boolean = true> {
604620
updatedAt?: T;
605621
createdAt?: T;
606622
}
623+
/**
624+
* This interface was referenced by `Config`'s JSON-Schema
625+
* via the `definition` "base-list-filters_select".
626+
*/
627+
export interface BaseListFiltersSelect<T extends boolean = true> {
628+
title?: T;
629+
updatedAt?: T;
630+
createdAt?: T;
631+
}
607632
/**
608633
* This interface was referenced by `Config`'s JSON-Schema
609634
* via the `definition` "payload-locked-documents_select".

test/admin/seed.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,24 @@ export const seed = async (_payload) => {
2727
depth: 0,
2828
overrideAccess: true,
2929
}),
30+
() =>
31+
_payload.create({
32+
collection: 'base-list-filters',
33+
data: {
34+
title: 'show me',
35+
},
36+
depth: 0,
37+
overrideAccess: true,
38+
}),
39+
() =>
40+
_payload.create({
41+
collection: 'base-list-filters',
42+
data: {
43+
title: 'hide me',
44+
},
45+
depth: 0,
46+
overrideAccess: true,
47+
}),
3048
...[...Array(11)].map((_, i) => async () => {
3149
const postDoc = await _payload.create({
3250
collection: postsCollectionSlug,

0 commit comments

Comments
 (0)