feat: collection-level disableBulkEdit (#12850)

This commit is contained in:
Sasha
2025-06-19 12:18:29 +03:00
committed by GitHub
parent 11ac230905
commit a5ec55c02a
12 changed files with 113 additions and 4 deletions

View File

@@ -85,6 +85,7 @@ The following options are available:
| `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#defaultpopulate-collection-config-property). |
| `indexes` | Define compound indexes for this collection. This can be used to either speed up querying/sorting by 2 or more fields at the same time or to ensure uniqueness between several fields. |
| `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks |
| `disableBulkEdit` | Disable the bulk edit operation for the collection in the admin panel and the REST API |
_\* An asterisk denotes that a property is required._

View File

@@ -274,7 +274,7 @@ export const renderListView = async (
collectionSlug,
columnState,
disableBulkDelete,
disableBulkEdit,
disableBulkEdit: collectionConfig.disableBulkEdit ?? disableBulkEdit,
disableQueryPresets,
enableRowSelections,
hasCreatePermission,

View File

@@ -440,6 +440,10 @@ export type CollectionConfig<TSlug extends CollectionSlug = any> = {
* Default field to sort by in collection list view
*/
defaultSort?: Sort
/**
* Disable the bulk edit operation for the collection in the admin panel and the API
*/
disableBulkEdit?: boolean
/**
* When true, do not show the "Duplicate" button while editing documents within this collection and prevent `duplicate` from all APIs
*/

View File

@@ -15,7 +15,7 @@ import type {
import executeAccess from '../../auth/executeAccess.js'
import { combineQueries } from '../../database/combineQueries.js'
import { validateQueryPaths } from '../../database/queryValidation/validateQueryPaths.js'
import { APIError } from '../../errors/index.js'
import { APIError, Forbidden } from '../../errors/index.js'
import { type CollectionSlug, deepCopyObjectSimple } from '../../index.js'
import { generateFileData } from '../../uploads/generateFileData.js'
import { unlinkTempFiles } from '../../uploads/unlinkTempFiles.js'
@@ -63,6 +63,10 @@ export const updateOperation = async <
): Promise<BulkOperationResult<TSlug, TSelect>> => {
let args = incomingArgs
if (args.collection.config.disableBulkEdit && !args.overrideAccess) {
throw new APIError(`Collection ${args.collection.config.slug} has disabled bulk edit`, 403)
}
try {
const shouldCommit = !args.disableTransaction && (await initTransaction(args.req))

View File

@@ -264,7 +264,7 @@ function CollectionFolderViewInContext(props: CollectionFolderViewInContextProps
!smallBreak && (
<ListSelection
disableBulkDelete={disableBulkDelete}
disableBulkEdit={disableBulkEdit}
disableBulkEdit={collectionConfig.disableBulkEdit ?? disableBulkEdit}
key="list-selection"
/>
),

View File

@@ -0,0 +1,7 @@
import type { CollectionConfig } from 'payload'
export const DisableBulkEdit: CollectionConfig = {
slug: 'disable-bulk-edit',
fields: [],
disableBulkEdit: true,
}

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-restricted-exports */
import { fileURLToPath } from 'node:url'
import path from 'path'
@@ -8,6 +7,7 @@ import { BaseListFilter } from './collections/BaseListFilter.js'
import { CustomFields } from './collections/CustomFields/index.js'
import { CustomViews1 } from './collections/CustomViews1.js'
import { CustomViews2 } from './collections/CustomViews2.js'
import { DisableBulkEdit } from './collections/DisableBulkEdit.js'
import { DisableCopyToLocale } from './collections/DisableCopyToLocale.js'
import { DisableDuplicate } from './collections/DisableDuplicate.js'
import { EditMenuItems } from './collections/editMenuItems.js'
@@ -180,6 +180,7 @@ export default buildConfigWithDefaults({
ListDrawer,
Placeholder,
UseAsTitleGroupField,
DisableBulkEdit,
],
globals: [
GlobalHidden,

View File

@@ -66,6 +66,7 @@ describe('List View', () => {
let with300DocumentsUrl: AdminUrlUtil
let withListViewUrl: AdminUrlUtil
let placeholderUrl: AdminUrlUtil
let disableBulkEditUrl: AdminUrlUtil
let user: any
let serverURL: string
@@ -90,6 +91,7 @@ describe('List View', () => {
customViewsUrl = new AdminUrlUtil(serverURL, customViews1CollectionSlug)
withListViewUrl = new AdminUrlUtil(serverURL, listDrawerSlug)
placeholderUrl = new AdminUrlUtil(serverURL, placeholderCollectionSlug)
disableBulkEditUrl = new AdminUrlUtil(serverURL, 'disable-bulk-edit')
const context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
@@ -1302,6 +1304,16 @@ describe('List View', () => {
await page.locator('#confirm-delete-many-docs #confirm-action').click()
await expect(page.locator('.cell-_select')).toHaveCount(1)
})
test('should hide edit many from collection with disableBulkEdit: true', async () => {
await payload.create({ collection: 'disable-bulk-edit', data: {} })
await page.goto(disableBulkEditUrl.list)
// select one row
await page.locator('.row-1 .cell-_select input').check()
// ensure the edit many button is hidden
await expect(page.locator('.edit-many button')).toBeHidden()
})
})
describe('pagination', () => {

View File

@@ -92,6 +92,7 @@ export interface Config {
'with-list-drawer': WithListDrawer;
placeholder: Placeholder;
'use-as-title-group-field': UseAsTitleGroupField;
'disable-bulk-edit': DisableBulkEdit;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
@@ -123,6 +124,7 @@ export interface Config {
'with-list-drawer': WithListDrawerSelect<false> | WithListDrawerSelect<true>;
placeholder: PlaceholderSelect<false> | PlaceholderSelect<true>;
'use-as-title-group-field': UseAsTitleGroupFieldSelect<false> | UseAsTitleGroupFieldSelect<true>;
'disable-bulk-edit': DisableBulkEditSelect<false> | DisableBulkEditSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
@@ -547,6 +549,15 @@ export interface UseAsTitleGroupField {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "disable-bulk-edit".
*/
export interface DisableBulkEdit {
id: string;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
@@ -653,6 +664,10 @@ export interface PayloadLockedDocument {
| ({
relationTo: 'use-as-title-group-field';
value: string | UseAsTitleGroupField;
} | null)
| ({
relationTo: 'disable-bulk-edit';
value: string | DisableBulkEdit;
} | null);
globalSlug?: string | null;
user: {
@@ -1037,6 +1052,14 @@ export interface UseAsTitleGroupFieldSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "disable-bulk-edit_select".
*/
export interface DisableBulkEditSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".

View File

@@ -270,6 +270,16 @@ export default buildConfigWithDefaults({
path: `/${method}-test`,
})),
},
{
slug: 'disabled-bulk-edit-docs',
fields: [
{
name: 'text',
type: 'text',
},
],
disableBulkEdit: true,
},
],
endpoints: [
{

View File

@@ -1797,6 +1797,28 @@ describe('collections-rest', () => {
expect((await result.json()).message.startsWith('Route not found')).toBeTruthy()
}
})
it('should disable bulk edit for the collection with disableBulkEdit: true', async () => {
const res = await restClient.PATCH('/disabled-bulk-edit-docs?where[id][equals]=0', {})
expect(res.status).toBe(403)
await expect(
payload.update({
collection: 'disabled-bulk-edit-docs',
where: {},
data: {},
overrideAccess: false,
}),
).rejects.toBeInstanceOf(APIError)
await expect(
payload.update({
collection: 'disabled-bulk-edit-docs',
where: {},
data: {},
}),
).resolves.toBeTruthy()
})
})
async function createPost(overrides?: Partial<Post>) {

View File

@@ -75,6 +75,7 @@ export interface Config {
'custom-id-number': CustomIdNumber;
'error-on-hooks': ErrorOnHook;
endpoints: Endpoint;
'disabled-bulk-edit-docs': DisabledBulkEditDoc;
users: User;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
@@ -90,6 +91,7 @@ export interface Config {
'custom-id-number': CustomIdNumberSelect<false> | CustomIdNumberSelect<true>;
'error-on-hooks': ErrorOnHooksSelect<false> | ErrorOnHooksSelect<true>;
endpoints: EndpointsSelect<false> | EndpointsSelect<true>;
'disabled-bulk-edit-docs': DisabledBulkEditDocsSelect<false> | DisabledBulkEditDocsSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
@@ -247,6 +249,16 @@ export interface Endpoint {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "disabled-bulk-edit-docs".
*/
export interface DisabledBulkEditDoc {
id: string;
text?: string | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
@@ -303,6 +315,10 @@ export interface PayloadLockedDocument {
relationTo: 'endpoints';
value: string | Endpoint;
} | null)
| ({
relationTo: 'disabled-bulk-edit-docs';
value: string | DisabledBulkEditDoc;
} | null)
| ({
relationTo: 'users';
value: string | User;
@@ -446,6 +462,15 @@ export interface EndpointsSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "disabled-bulk-edit-docs_select".
*/
export interface DisabledBulkEditDocsSelect<T extends boolean = true> {
text?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".