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). |
|
| **`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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
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 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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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".
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user